Upload latest code and optimized prompts (v6)
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# Generated files
|
||||
output/
|
||||
output_*/
|
||||
output_v*/
|
||||
*.zip
|
||||
*.jpg
|
||||
*.png
|
||||
*.JPG
|
||||
*.PNG
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
|
||||
# Large assets
|
||||
素材/
|
||||
pic/
|
||||
data/
|
||||
34
README.md
Normal file
34
README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# AI Image Generator Flow
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Install Dependencies**:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Environment Variables**:
|
||||
A `.env` file has been created. Ensure the following keys are correct:
|
||||
* `R2_ACCOUNT_ID`: Your Cloudflare Account ID.
|
||||
* `R2_ACCESS_KEY_ID`: Your R2 Access Key ID.
|
||||
* `R2_SECRET_ACCESS_KEY`: Your R2 Secret Access Key.
|
||||
* `R2_BUCKET_NAME`: The name of your R2 bucket (Default: `ai-flow`).
|
||||
* `API_KEY`: Your Wuyin Keji API Key.
|
||||
|
||||
3. **Run Server**:
|
||||
```bash
|
||||
node server.js
|
||||
```
|
||||
Access at `http://localhost:3000`.
|
||||
|
||||
## Features
|
||||
|
||||
1. **Upload**: Uploads reference images to Cloudflare R2.
|
||||
2. **Prompt Generation**: Uses LLM to generate prompts based on `prompt.md` template.
|
||||
3. **Image Generation**: Uses NanoBanana to generate images from prompts.
|
||||
4. **History**: Saves generated images and prompts locally.
|
||||
5. **Batch Download**: Download all generated images.
|
||||
|
||||
## Notes
|
||||
* Ensure your R2 bucket allows public access or configure a custom domain in `.env` as `R2_PUBLIC_DOMAIN`.
|
||||
* The application saves state to browser `localStorage` to prevent data loss on refresh.
|
||||
20
check_buckets.js
Normal file
20
check_buckets.js
Normal file
@@ -0,0 +1,20 @@
|
||||
require('dotenv').config();
|
||||
const { S3Client, ListBucketsCommand } = require('@aws-sdk/client-s3');
|
||||
|
||||
const client = 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,
|
||||
},
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const data = await client.send(new ListBucketsCommand({}));
|
||||
console.log('Buckets:', data.Buckets);
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
}
|
||||
})();
|
||||
71
debug-api.js
Normal file
71
debug-api.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* API Debug Script
|
||||
*/
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
|
||||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||||
|
||||
async function debug() {
|
||||
console.log('🔍 Starting API Debug...');
|
||||
|
||||
// 1. Debug Vision
|
||||
console.log('\n--- 1. Testing Vision API ---');
|
||||
try {
|
||||
const res = await axios.post(`${API_BASE}/chat/index`, {
|
||||
key: API_KEY,
|
||||
model: 'gemini-1.5-flash',
|
||||
content: 'Describe this image briefly.',
|
||||
image_url: 'https://pub-777656770f4a44079545474665f00072.r2.dev/poc-1765607704915-IMG_6514.JPG' // 使用刚才日志里上传成功的图
|
||||
});
|
||||
console.log('Vision Response:', JSON.stringify(res.data, null, 2));
|
||||
} catch (e) {
|
||||
console.error('Vision Error:', e.message, e.response?.data);
|
||||
}
|
||||
|
||||
// 2. Debug Image Submit
|
||||
console.log('\n--- 2. Testing Image Submit ---');
|
||||
let taskId;
|
||||
try {
|
||||
const res = await axios.post(`${API_BASE}/img/nanoBanana-pro`, {
|
||||
key: API_KEY,
|
||||
prompt: 'A cute cat',
|
||||
aspectRatio: '1:1'
|
||||
});
|
||||
console.log('Submit Response:', JSON.stringify(res.data, null, 2));
|
||||
|
||||
// 尝试获取ID
|
||||
if (res.data.data && typeof res.data.data === 'string') {
|
||||
taskId = res.data.data;
|
||||
} else if (res.data.data?.id) {
|
||||
taskId = res.data.data.id;
|
||||
} else if (res.data.id) {
|
||||
taskId = res.data.id;
|
||||
}
|
||||
console.log('Extracted Task ID:', taskId);
|
||||
} catch (e) {
|
||||
console.error('Submit Error:', e.message, e.response?.data);
|
||||
}
|
||||
|
||||
// 3. Debug Poll
|
||||
if (taskId) {
|
||||
console.log('\n--- 3. Testing Poll ---');
|
||||
try {
|
||||
// 等几秒让它处理
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
const res = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId }
|
||||
});
|
||||
console.log('Poll Response:', JSON.stringify(res.data, null, 2));
|
||||
} catch (e) {
|
||||
console.error('Poll Error:', e.message, e.response?.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug();
|
||||
|
||||
|
||||
|
||||
2
design.md
Normal file
2
design.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## SKU颜色:纯蓝色
|
||||
### 需要设计4张主图(尺寸1600*1600,横向)和4张A+图(尺寸970*600,横向)
|
||||
23
golden-description.txt
Normal file
23
golden-description.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
EXACT PRODUCT APPEARANCE (AUTO-EXTRACTED BY VISION):
|
||||
|
||||
- Shape: 7-PETAL FLOWER/FAN shape, C-shaped opening
|
||||
- Color: PASTEL ICE BLUE (#C3E6E8)
|
||||
- Material: soft matte/satin synthetic fabric (likely water-resistant polyester/nylon) fabric with smooth, padded/quilted panels texture
|
||||
- Edge binding: Mint Green (slightly more saturated than body) ribbed knit elastic fabric around inner neck hole
|
||||
- Closure: white velcro (hook and loop) on large rectangular strip covering the entire bottom-left terminal segment
|
||||
- Logo: "TOUCHDOG®" embroidered on centered on the middle-right segment
|
||||
|
||||
UNIQUE FEATURES:
|
||||
- Scalloped outer edge resembling flower petals
|
||||
- Soft ribbed knit neckline for comfort
|
||||
- Radial stitching lines creating distinct padded zones
|
||||
- Large surface area velcro for adjustable sizing
|
||||
|
||||
SUMMARY FOR PROMPTS:
|
||||
A soft, padded pet recovery collar in pastel ice blue with a scalloped, flower-like shape. It features a comfortable mint-green ribbed knit neck opening, sectioned padding for flexibility, and a prominent white velcro closure strip.
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- ❌ NO printed patterns or colorful fabric designs
|
||||
- ❌ NO hard plastic transparent cones
|
||||
- ❌ NO fully circular/closed shapes (must have C-opening)
|
||||
- ❌ NO random brand logos or text
|
||||
332
lib/batch-processor.js
Normal file
332
lib/batch-processor.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* 批量处理器
|
||||
* 支持5000+ SKU的批量图片生成
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 批量处理配置
|
||||
*/
|
||||
const DEFAULT_CONFIG = {
|
||||
concurrency: 1, // 并发数(建议保持1避免API限流)
|
||||
retryCount: 3, // 失败重试次数
|
||||
retryDelay: 5000, // 重试延迟(毫秒)
|
||||
imageDelay: 1500, // 图片间延迟(毫秒)
|
||||
skuDelay: 3000, // SKU间延迟(毫秒)
|
||||
saveProgress: true, // 是否保存进度
|
||||
progressFile: 'batch-progress.json'
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量处理器类
|
||||
*/
|
||||
class BatchProcessor {
|
||||
constructor(serverUrl, config = {}) {
|
||||
this.serverUrl = serverUrl || 'http://localhost:3000';
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
this.progress = {
|
||||
total: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个SKU
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {Promise<object>} 处理结果
|
||||
*/
|
||||
async processSKU(sku) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
console.log(`Processing SKU: ${sku.sku_id}`);
|
||||
|
||||
const response = await axios.post(`${this.serverUrl}/api/generate-sku`, {
|
||||
sku
|
||||
}, {
|
||||
timeout: 600000 // 10分钟超时(12张图)
|
||||
});
|
||||
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
console.log(`SKU ${sku.sku_id} completed in ${duration.toFixed(1)}s`);
|
||||
|
||||
return {
|
||||
sku_id: sku.sku_id,
|
||||
status: 'success',
|
||||
duration,
|
||||
...response.data
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
console.error(`SKU ${sku.sku_id} failed after ${duration.toFixed(1)}s:`, error.message);
|
||||
|
||||
return {
|
||||
sku_id: sku.sku_id,
|
||||
status: 'failed',
|
||||
duration,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试的处理单个SKU
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {Promise<object>} 处理结果
|
||||
*/
|
||||
async processSKUWithRetry(sku) {
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= this.config.retryCount; attempt++) {
|
||||
try {
|
||||
const result = await this.processSKU(sku);
|
||||
|
||||
if (result.status === 'success') {
|
||||
return result;
|
||||
}
|
||||
|
||||
lastError = result.error;
|
||||
|
||||
if (attempt < this.config.retryCount) {
|
||||
console.log(`Retrying SKU ${sku.sku_id} (attempt ${attempt + 1}/${this.config.retryCount})...`);
|
||||
await this.delay(this.config.retryDelay);
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error.message;
|
||||
|
||||
if (attempt < this.config.retryCount) {
|
||||
console.log(`Retrying SKU ${sku.sku_id} (attempt ${attempt + 1}/${this.config.retryCount})...`);
|
||||
await this.delay(this.config.retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sku_id: sku.sku_id,
|
||||
status: 'failed',
|
||||
error: lastError,
|
||||
attempts: this.config.retryCount
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量处理SKU列表
|
||||
* @param {array} skus - SKU数据数组
|
||||
* @param {function} onProgress - 进度回调
|
||||
* @returns {Promise<object>} 批量处理结果
|
||||
*/
|
||||
async processBatch(skus, onProgress = null) {
|
||||
this.progress = {
|
||||
total: skus.length,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
startTime: Date.now(),
|
||||
results: []
|
||||
};
|
||||
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`Starting batch processing: ${skus.length} SKUs`);
|
||||
console.log(`${'='.repeat(60)}\n`);
|
||||
|
||||
// 加载之前的进度(如果有)
|
||||
const existingProgress = this.loadProgress();
|
||||
const processedIds = new Set(existingProgress.map(r => r.sku_id));
|
||||
|
||||
// 过滤掉已处理的SKU
|
||||
const pendingSkus = skus.filter(sku => !processedIds.has(sku.sku_id));
|
||||
this.progress.results = existingProgress;
|
||||
this.progress.completed = existingProgress.filter(r => r.status === 'success').length;
|
||||
this.progress.failed = existingProgress.filter(r => r.status === 'failed').length;
|
||||
|
||||
if (existingProgress.length > 0) {
|
||||
console.log(`Resuming from previous progress: ${existingProgress.length} already processed`);
|
||||
}
|
||||
|
||||
// 串行处理(避免API限流)
|
||||
for (let i = 0; i < pendingSkus.length; i++) {
|
||||
const sku = pendingSkus[i];
|
||||
const overallIndex = existingProgress.length + i + 1;
|
||||
|
||||
console.log(`\n[${overallIndex}/${skus.length}] Processing ${sku.sku_id}...`);
|
||||
|
||||
const result = await this.processSKUWithRetry(sku);
|
||||
this.progress.results.push(result);
|
||||
|
||||
if (result.status === 'success') {
|
||||
this.progress.completed++;
|
||||
} else {
|
||||
this.progress.failed++;
|
||||
}
|
||||
|
||||
// 保存进度
|
||||
if (this.config.saveProgress) {
|
||||
this.saveProgress();
|
||||
}
|
||||
|
||||
// 进度回调
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
current: overallIndex,
|
||||
total: skus.length,
|
||||
completed: this.progress.completed,
|
||||
failed: this.progress.failed,
|
||||
lastResult: result
|
||||
});
|
||||
}
|
||||
|
||||
// SKU间延迟
|
||||
if (i < pendingSkus.length - 1) {
|
||||
await this.delay(this.config.skuDelay);
|
||||
}
|
||||
}
|
||||
|
||||
this.progress.endTime = Date.now();
|
||||
this.progress.totalDuration = (this.progress.endTime - this.progress.startTime) / 1000;
|
||||
|
||||
console.log(`\n${'='.repeat(60)}`);
|
||||
console.log(`Batch processing complete!`);
|
||||
console.log(`Total: ${skus.length}, Success: ${this.progress.completed}, Failed: ${this.progress.failed}`);
|
||||
console.log(`Duration: ${this.progress.totalDuration.toFixed(1)}s`);
|
||||
console.log(`${'='.repeat(60)}\n`);
|
||||
|
||||
return {
|
||||
total: skus.length,
|
||||
completed: this.progress.completed,
|
||||
failed: this.progress.failed,
|
||||
duration: this.progress.totalDuration,
|
||||
results: this.progress.results
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
* @param {number} ms - 毫秒数
|
||||
*/
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存进度到文件
|
||||
*/
|
||||
saveProgress() {
|
||||
try {
|
||||
const progressPath = path.join(process.cwd(), this.config.progressFile);
|
||||
fs.writeFileSync(progressPath, JSON.stringify(this.progress.results, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Failed to save progress:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载之前的进度
|
||||
* @returns {array} 之前的处理结果
|
||||
*/
|
||||
loadProgress() {
|
||||
try {
|
||||
const progressPath = path.join(process.cwd(), this.config.progressFile);
|
||||
if (fs.existsSync(progressPath)) {
|
||||
const content = fs.readFileSync(progressPath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load progress:', error.message);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除进度文件
|
||||
*/
|
||||
clearProgress() {
|
||||
try {
|
||||
const progressPath = path.join(process.cwd(), this.config.progressFile);
|
||||
if (fs.existsSync(progressPath)) {
|
||||
fs.unlinkSync(progressPath);
|
||||
console.log('Progress cleared');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to clear progress:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成批处理报告
|
||||
* @returns {object} 报告数据
|
||||
*/
|
||||
generateReport() {
|
||||
const successResults = this.progress.results.filter(r => r.status === 'success');
|
||||
const failedResults = this.progress.results.filter(r => r.status === 'failed');
|
||||
|
||||
const totalImages = successResults.reduce((sum, r) => {
|
||||
return sum + (r.summary?.success || 0);
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
summary: {
|
||||
total_skus: this.progress.total,
|
||||
successful_skus: this.progress.completed,
|
||||
failed_skus: this.progress.failed,
|
||||
success_rate: ((this.progress.completed / this.progress.total) * 100).toFixed(1) + '%',
|
||||
total_images_generated: totalImages,
|
||||
total_duration: this.progress.totalDuration?.toFixed(1) + 's',
|
||||
average_time_per_sku: (this.progress.totalDuration / this.progress.total).toFixed(1) + 's'
|
||||
},
|
||||
failed_skus: failedResults.map(r => ({
|
||||
sku_id: r.sku_id,
|
||||
error: r.error
|
||||
})),
|
||||
successful_skus: successResults.map(r => ({
|
||||
sku_id: r.sku_id,
|
||||
images: r.summary
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件加载SKU列表
|
||||
* @param {string} filePath - 文件路径(JSON或CSV)
|
||||
* @returns {array} SKU数组
|
||||
*/
|
||||
function loadSKUsFromFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
if (ext === '.json') {
|
||||
const data = JSON.parse(content);
|
||||
return Array.isArray(data) ? data : [data];
|
||||
}
|
||||
|
||||
// 简单CSV解析(假设第一行是表头)
|
||||
if (ext === '.csv') {
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
const headers = lines[0].split(',').map(h => h.trim());
|
||||
|
||||
return lines.slice(1).map(line => {
|
||||
const values = line.split(',').map(v => v.trim());
|
||||
const obj = {};
|
||||
headers.forEach((h, i) => {
|
||||
obj[h] = values[i];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('Unsupported file format. Use .json or .csv');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BatchProcessor,
|
||||
loadSKUsFromFile,
|
||||
DEFAULT_CONFIG
|
||||
};
|
||||
|
||||
|
||||
|
||||
332
lib/brain.js
Normal file
332
lib/brain.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Brain决策逻辑
|
||||
* 调用LLM分析产品信息并生成12张图的规划
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
|
||||
// 读取Brain System Prompt
|
||||
const brainPromptPath = path.join(__dirname, '../prompts/brain-system.md');
|
||||
let brainSystemPrompt = '';
|
||||
|
||||
try {
|
||||
brainSystemPrompt = fs.readFileSync(brainPromptPath, 'utf-8');
|
||||
} catch (err) {
|
||||
console.error('Failed to load brain system prompt:', err.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用LLM生成图片规划
|
||||
* @param {object} sku - SKU数据
|
||||
* @param {object} options - 选项
|
||||
* @param {string} options.apiKey - API密钥
|
||||
* @param {string} options.apiUrl - API地址
|
||||
* @param {string} options.model - 模型名称
|
||||
* @returns {Promise<object>} Brain输出的图片规划
|
||||
*/
|
||||
async function generateImagePlan(sku, options = {}) {
|
||||
const {
|
||||
apiKey = process.env.API_KEY,
|
||||
apiUrl = 'https://api2img.shubiaobiao.com/v1/chat/completions',
|
||||
model = 'gemini-3-pro-preview'
|
||||
} = options;
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('API_KEY is required');
|
||||
}
|
||||
|
||||
// 构建用户消息
|
||||
const userMessage = `请根据以下产品信息,规划12张电商图片(6张主图 + 6张A+图)的内容策略。
|
||||
|
||||
## 产品信息
|
||||
|
||||
\`\`\`json
|
||||
${JSON.stringify(sku, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
请严格按照System Prompt中的输出格式返回JSON。`;
|
||||
|
||||
try {
|
||||
const response = await axios.post(apiUrl, {
|
||||
model: model,
|
||||
messages: [
|
||||
{ role: 'system', content: brainSystemPrompt },
|
||||
{ role: 'user', content: userMessage }
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 8000
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
timeout: 120000 // 2分钟超时
|
||||
});
|
||||
|
||||
// 解析响应
|
||||
let content = response.data.choices?.[0]?.message?.content;
|
||||
|
||||
if (!content) {
|
||||
throw new Error('Empty response from LLM');
|
||||
}
|
||||
|
||||
// 清理响应内容
|
||||
content = cleanLLMResponse(content);
|
||||
|
||||
// 解析JSON
|
||||
const plan = parseJSONFromResponse(content);
|
||||
|
||||
// 验证输出格式
|
||||
validatePlan(plan);
|
||||
|
||||
return plan;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Brain generation error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理LLM响应
|
||||
* @param {string} content - 原始响应内容
|
||||
* @returns {string} 清理后的内容
|
||||
*/
|
||||
function cleanLLMResponse(content) {
|
||||
// 移除 <think> 块
|
||||
content = content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
||||
|
||||
// 移除可能的markdown标记
|
||||
content = content.replace(/^```json\s*/i, '');
|
||||
content = content.replace(/\s*```$/i, '');
|
||||
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中解析JSON
|
||||
* @param {string} content - 响应内容
|
||||
* @returns {object} 解析后的JSON对象
|
||||
*/
|
||||
function parseJSONFromResponse(content) {
|
||||
// 尝试直接解析
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
// 尝试提取JSON块
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
} catch (e2) {
|
||||
// 尝试修复常见问题
|
||||
let fixed = jsonMatch[0];
|
||||
|
||||
// 修复尾部逗号
|
||||
fixed = fixed.replace(/,\s*([}\]])/g, '$1');
|
||||
|
||||
// 修复未闭合的字符串
|
||||
fixed = fixed.replace(/:\s*"([^"]*?)(?=\s*[,}\]])/g, ': "$1"');
|
||||
|
||||
try {
|
||||
return JSON.parse(fixed);
|
||||
} catch (e3) {
|
||||
throw new Error('Failed to parse JSON from LLM response: ' + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('No valid JSON found in LLM response');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Brain输出的计划
|
||||
* @param {object} plan - 图片规划
|
||||
*/
|
||||
function validatePlan(plan) {
|
||||
if (!plan.analysis) {
|
||||
throw new Error('Missing analysis in plan');
|
||||
}
|
||||
|
||||
if (!plan.images || !Array.isArray(plan.images)) {
|
||||
throw new Error('Missing or invalid images array in plan');
|
||||
}
|
||||
|
||||
if (plan.images.length !== 12) {
|
||||
console.warn(`Expected 12 images, got ${plan.images.length}`);
|
||||
}
|
||||
|
||||
// 检查必要字段
|
||||
const requiredFields = ['id', 'type', 'ai_prompt'];
|
||||
for (const image of plan.images) {
|
||||
for (const field of requiredFields) {
|
||||
if (!image[field]) {
|
||||
throw new Error(`Missing ${field} in image ${image.id || 'unknown'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查图片ID是否正确
|
||||
const expectedIds = [
|
||||
'Main_01', 'Main_02', 'Main_03', 'Main_04', 'Main_05', 'Main_06',
|
||||
'APlus_01', 'APlus_02', 'APlus_03', 'APlus_04', 'APlus_05', 'APlus_06'
|
||||
];
|
||||
|
||||
const actualIds = plan.images.map(img => img.id);
|
||||
const missingIds = expectedIds.filter(id => !actualIds.includes(id));
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
console.warn(`Missing image IDs: ${missingIds.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认的图片规划(当Brain调用失败时使用)
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {object} 默认的图片规划
|
||||
*/
|
||||
function generateDefaultPlan(sku) {
|
||||
const topSellingPoints = sku.selling_points.slice(0, 3).map(sp => sp.key);
|
||||
const productColor = sku.color.name;
|
||||
const productName = sku.product_name;
|
||||
|
||||
return {
|
||||
analysis: {
|
||||
product_category: 'Pet Recovery Cone',
|
||||
core_selling_points: topSellingPoints,
|
||||
background_strategy: 'light',
|
||||
logo_color: 'red',
|
||||
visual_style: 'Warm, caring home environment'
|
||||
},
|
||||
images: [
|
||||
{
|
||||
id: 'Main_01',
|
||||
type: 'Hero Scene + Key Benefits',
|
||||
purpose: '首图决定点击率,展示产品使用状态+核心卖点',
|
||||
layout_description: `上60%:猫咪佩戴${productColor}${productName}的居家场景。下40%:浅色Banner展示3个核心卖点。`,
|
||||
ai_prompt: `Professional Amazon main image, 1:1 square format. A beautiful cat wearing a ${productColor} soft cone collar in a cozy modern home interior. Natural soft lighting, warm atmosphere. Bottom section shows a light blue rounded banner with "DESIGNED FOR COMFORTABLE RECOVERY" text and three small product detail images highlighting key features. Clean, professional pet product photography style. --ar 1:1`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_02',
|
||||
type: 'Product Detail + Craftsmanship',
|
||||
purpose: '展示产品结构和工艺细节',
|
||||
layout_description: `产品平铺俯视图,配合2-3个细节放大框展示材质和工艺`,
|
||||
ai_prompt: `Professional product flat lay image, 1:1 square format. ${productColor} soft cone collar displayed from above on white background. Two circular detail callouts showing: 1) waterproof PU outer layer texture, 2) soft cotton inner lining. Clean product photography, even lighting, high detail. Brand logo "TOUCHDOG®" visible on right petal. --ar 1:1`,
|
||||
logo_placement: { position: 'none', type: 'none', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_03',
|
||||
type: 'Function Demonstration',
|
||||
purpose: '展示可调节绑带等功能特性',
|
||||
layout_description: `主场景展示佩戴状态,配合细节图展示调节功能`,
|
||||
ai_prompt: `Professional Amazon feature demonstration image, 1:1 square format. Main image shows cat wearing ${productColor} cone, looking comfortable. Detail callouts show: adjustable velcro strap being secured, snug fit around neck. Text overlay "ADJUSTABLE STRAP FOR A SECURE FIT". Light blue background with paw print decorations. --ar 1:1`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_04',
|
||||
type: 'Use Cases Grid',
|
||||
purpose: '展示4种适用场景',
|
||||
layout_description: `四宫格布局,展示术后护理、滴药、剪指甲、梳毛四个场景`,
|
||||
ai_prompt: `Professional Amazon use case image, 1:1 square format. Four-panel grid showing: 1) cat resting after surgery wearing cone, 2) owner applying eye drops to cat, 3) owner trimming cat's nails, 4) cat being groomed. Each panel labeled: "POSTOPERATIVE CARE", "EYE MEDICATION", "NAIL TRIMMING", "GROOMING". Light blue header banner "APPLICABLE SCENARIOS". --ar 1:1`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_05',
|
||||
type: 'Size & Specifications',
|
||||
purpose: '展示尺寸规格信息',
|
||||
layout_description: `产品尺寸标注图配合尺码对照表`,
|
||||
ai_prompt: `Professional Amazon size guide image, 1:1 square format. ${productColor} cone collar with dimension annotations showing depth measurement (${sku.specs.depth_cm}cm). Clean size chart showing XS to XL with neck circumference ranges. Professional infographic style, clear readable text. Light background. --ar 1:1`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'Main_06',
|
||||
type: 'Brand & Benefits Summary',
|
||||
purpose: '强化品牌认知和卖点汇总',
|
||||
layout_description: `品牌Logo突出展示,配合核心卖点图标`,
|
||||
ai_prompt: `Professional Amazon brand summary image, 1:1 square format. Touchdog brand logo prominently displayed. Key benefit icons with text: "65g Ultra Light", "Waterproof PU", "Breathable Cotton", "Adjustable Fit", "Foldable Design". Cat wearing ${productColor} cone as background element. Premium brand aesthetic. --ar 1:1`,
|
||||
logo_placement: { position: 'center', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_01',
|
||||
type: 'Brand Banner',
|
||||
purpose: '品牌形象首图',
|
||||
layout_description: `品牌名+产品名+生活场景大图`,
|
||||
ai_prompt: `Professional Amazon A+ banner image, 970x600px landscape. Beautiful cat wearing ${productColor} Touchdog soft cone collar in warm modern home interior. Large "TOUCHDOG" brand text and "CAT SOFT CONE COLLAR" product name overlaid. Lifestyle photography, natural lighting, cozy atmosphere. --ar 3:2`,
|
||||
logo_placement: { position: 'top-left', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_02',
|
||||
type: 'Competitor Comparison',
|
||||
purpose: '竞品对比展示优势',
|
||||
layout_description: `左侧我们的产品(彩色+优点),右侧传统产品(灰色+缺点)`,
|
||||
ai_prompt: `Professional Amazon A+ comparison image, 970x600px landscape. Left side (colored): Touchdog ${productColor} soft cone with green checkmark, labeled "OUR" with benefits "CLOUD-LIGHT COMFORT", "WIDER & CLEARER", "FOLDABLE & PORTABLE". Right side (grayscale): Generic plastic cone with red X, labeled "OTHER" with drawbacks "HEAVY & BULKY", "BLOCKS VISION & MOVEMENT", "HARD TO STORE". Center shows cat wearing our product. Warm beige background with paw prints. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' },
|
||||
is_comparison: true
|
||||
},
|
||||
{
|
||||
id: 'APlus_03',
|
||||
type: 'Benefits Grid',
|
||||
purpose: '核心卖点展示',
|
||||
layout_description: `三宫格展示3个核心卖点`,
|
||||
ai_prompt: `Professional Amazon A+ benefits image, 970x600px landscape. Three-panel layout showing key features: 1) "STURDY AND BREATHABLE" with fabric texture close-up, 2) "EASY TO CLEAN" with dimension annotation ${sku.specs.depth_cm}cm, 3) "REINFORCED STITCHING" with stitching detail. Header "ENGINEERED FOR UNCOMPROMISED COMFORT". Warm beige background. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_04',
|
||||
type: 'Features Grid',
|
||||
purpose: '功能场景展示',
|
||||
layout_description: `四宫格展示功能:易清洁、不影响进食、可翻折、360°舒适`,
|
||||
ai_prompt: `Professional Amazon A+ features image, 970x600px landscape. Four vertical panels: 1) "HYGIENIC & EASY TO CLEAN" showing water-resistant surface, 2) "UNRESTRICTED EATING/DRINKING" showing cat eating while wearing cone, 3) "REVERSIBLE WEAR" showing flip-over design, 4) "360° COMFORT" showing cat sleeping peacefully. Warm beige background, consistent styling. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_05',
|
||||
type: 'Material & Craftsmanship',
|
||||
purpose: '材质工艺展示',
|
||||
layout_description: `材质剖面和工艺细节展示`,
|
||||
ai_prompt: `Professional Amazon A+ material image, 970x600px landscape. Close-up details of ${productColor} cone showing: waterproof PU outer layer with water droplets, soft cotton inner lining texture, reinforced stitching seams, embroidered TOUCHDOG logo. Technical infographic style with material callouts. Light background. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
},
|
||||
{
|
||||
id: 'APlus_06',
|
||||
type: 'Size Guide',
|
||||
purpose: '尺寸选择指南',
|
||||
layout_description: `完整尺码表+测量指南`,
|
||||
ai_prompt: `Professional Amazon A+ size guide image, 970x600px landscape. Comprehensive size chart showing all sizes (XS-XL) with neck circumference and depth measurements in both cm and inches. Illustration showing how to measure pet's neck. Clear, readable table format. Helpful sizing tips. Professional infographic design. --ar 3:2`,
|
||||
logo_placement: { position: 'bottom-right', type: 'combined', color: 'red' }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 主入口:生成图片规划
|
||||
* @param {object} sku - SKU数据
|
||||
* @param {object} options - 选项
|
||||
* @returns {Promise<object>} 图片规划
|
||||
*/
|
||||
async function planImages(sku, options = {}) {
|
||||
try {
|
||||
// 尝试调用LLM生成计划
|
||||
const plan = await generateImagePlan(sku, options);
|
||||
console.log('Brain generated plan successfully');
|
||||
return plan;
|
||||
} catch (error) {
|
||||
console.error('Brain failed, using default plan:', error.message);
|
||||
// 失败时使用默认计划
|
||||
return generateDefaultPlan(sku);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
planImages,
|
||||
generateImagePlan,
|
||||
generateDefaultPlan,
|
||||
validatePlan
|
||||
};
|
||||
|
||||
|
||||
|
||||
369
lib/constraint-injector.js
Normal file
369
lib/constraint-injector.js
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 约束注入器
|
||||
* 自动将各类约束追加到AI生图Prompt中
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 读取约束模板
|
||||
const constraintsDir = path.join(__dirname, '../prompts/constraints');
|
||||
|
||||
/**
|
||||
* 简单的模板引擎,替换 {{variable}} 占位符
|
||||
* @param {string} template - 模板字符串
|
||||
* @param {object} data - 数据对象
|
||||
* @returns {string} 替换后的字符串
|
||||
*/
|
||||
function renderTemplate(template, data) {
|
||||
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
||||
const keys = key.trim().split('.');
|
||||
let value = data;
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object') {
|
||||
// 处理数组索引 selling_points[0]
|
||||
const arrayMatch = k.match(/^(\w+)\[(\d+)\]$/);
|
||||
if (arrayMatch) {
|
||||
value = value[arrayMatch[1]]?.[parseInt(arrayMatch[2])];
|
||||
} else {
|
||||
value = value[k];
|
||||
}
|
||||
} else {
|
||||
return match; // 保留原占位符
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(', ');
|
||||
}
|
||||
|
||||
return value !== undefined ? String(value) : match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Handlebars风格的循环 {{#each array}}...{{/each}}
|
||||
* @param {string} template - 模板字符串
|
||||
* @param {object} data - 数据对象
|
||||
* @returns {string} 处理后的字符串
|
||||
*/
|
||||
function processEachBlocks(template, data) {
|
||||
const eachRegex = /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g;
|
||||
|
||||
return template.replace(eachRegex, (match, arrayName, content) => {
|
||||
const array = data[arrayName];
|
||||
if (!Array.isArray(array)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return array.map(item => {
|
||||
// 处理 {{#if field}} 条件
|
||||
let processed = content.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (m, field, ifContent) => {
|
||||
return item[field] ? ifContent : '';
|
||||
});
|
||||
|
||||
// 替换当前项的字段
|
||||
processed = processed.replace(/\{\{(\w+)\}\}/g, (m, field) => {
|
||||
return item[field] !== undefined ? String(item[field]) : m;
|
||||
});
|
||||
|
||||
return processed;
|
||||
}).join('\n');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成产品一致性约束
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {string} 约束文本
|
||||
*/
|
||||
function generateProductConsistencyConstraint(sku) {
|
||||
const template = `
|
||||
=== PRODUCT CONSISTENCY REQUIREMENTS (CRITICAL - DO NOT IGNORE) ===
|
||||
|
||||
The product shown in this image must EXACTLY match the reference images provided.
|
||||
|
||||
PRODUCT SPECIFICATIONS:
|
||||
- Product Type: ${sku.product_name}
|
||||
- Color: ${sku.color.hex} (${sku.color.name})
|
||||
- Color Series: ${sku.color.series || 'Standard'}
|
||||
- Edge/Binding Color: ${sku.color.edge_color || sku.color.hex}
|
||||
|
||||
STRUCTURAL REQUIREMENTS:
|
||||
- Shape: Soft cone collar with petal-like segments (NOT rigid plastic cone)
|
||||
- Segments: Multiple soft fabric petals radiating from center opening
|
||||
- Opening: Circular neck opening with ribbed cotton binding
|
||||
- Closure: Velcro strap closure system
|
||||
|
||||
BRAND LOGO ON PRODUCT:
|
||||
- Text: "TOUCHDOG®" (UPPERCASE with ® symbol)
|
||||
- Position: On one petal segment, typically right side (2-3 o'clock position when viewed from front)
|
||||
- Style: Embroidered/stitched into fabric, slightly recessed
|
||||
- Color: Darker or contrasting shade matching the product color
|
||||
|
||||
TEXTURE & MATERIAL APPEARANCE:
|
||||
- Outer Layer: Smooth, slightly glossy waterproof PU fabric
|
||||
- Inner Layer: Visible soft cotton lining at edges
|
||||
- Stitching: Reinforced seams, visible quilted pattern on petals
|
||||
- Edge Binding: Ribbed cotton binding in ${sku.color.edge_color || 'matching color'}
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- DO NOT alter the product shape or structure
|
||||
- DO NOT change the color from specified hex value
|
||||
- DO NOT move or resize the embroidered logo
|
||||
- DO NOT add any elements not present in reference images
|
||||
- DO NOT stylize, reimagine, or "improve" the product design
|
||||
- DO NOT generate rigid plastic cone - must be SOFT fabric cone
|
||||
|
||||
=== END PRODUCT CONSISTENCY REQUIREMENTS ===
|
||||
`;
|
||||
return template.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成卖点合规约束
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {string} 约束文本
|
||||
*/
|
||||
function generateSellingPointConstraint(sku) {
|
||||
const sellingPointsList = sku.selling_points.map(sp => {
|
||||
let line = `- ${sp.title_en}`;
|
||||
if (sp.value) line += ` (Value: ${sp.value})`;
|
||||
if (sp.description_en) line += `: ${sp.description_en}`;
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
const visualPromptsList = sku.selling_points
|
||||
.filter(sp => sp.visual_prompt)
|
||||
.map(sp => `- ${sp.key}: ${sp.visual_prompt}`)
|
||||
.join('\n');
|
||||
|
||||
const useCasesList = sku.use_cases
|
||||
.map(uc => `- ${uc.name_en}`)
|
||||
.join('\n');
|
||||
|
||||
const template = `
|
||||
=== SELLING POINTS COMPLIANCE (LEGAL REQUIREMENT) ===
|
||||
|
||||
ALL product claims, features, and specifications shown in this image must come EXACTLY from the provided data.
|
||||
|
||||
APPROVED SELLING POINTS (use ONLY these):
|
||||
${sellingPointsList}
|
||||
|
||||
APPROVED SPECIFICATIONS (use EXACT values):
|
||||
- Weight: ${sku.specs.weight}
|
||||
- Depth: ${sku.specs.depth_cm}cm
|
||||
- Available Sizes: ${sku.specs.sizes.join(', ')}
|
||||
- Outer Material: ${sku.specs.materials?.outer || 'Waterproof PU'}
|
||||
- Inner Material: ${sku.specs.materials?.inner || 'Cotton blend'}
|
||||
|
||||
APPROVED USE CASES:
|
||||
${useCasesList}
|
||||
|
||||
PROHIBITED ACTIONS:
|
||||
- DO NOT invent new features or benefits not listed above
|
||||
- DO NOT modify numerical values (no rounding, no approximation)
|
||||
- DO NOT use superlatives: "best", "first", "only", "most", "#1", "leading"
|
||||
- DO NOT make comparative claims without data: "better than", "superior to"
|
||||
- DO NOT add health claims not approved by regulations
|
||||
- DO NOT promise specific outcomes: "guaranteed", "100% effective"
|
||||
|
||||
VISUAL REPRESENTATION OF SELLING POINTS:
|
||||
${visualPromptsList}
|
||||
|
||||
=== END SELLING POINTS COMPLIANCE ===
|
||||
`;
|
||||
return template.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成竞品合规约束(仅用于竞品对比图)
|
||||
* @param {object} sku - SKU数据
|
||||
* @returns {string} 约束文本
|
||||
*/
|
||||
function generateCompetitorConstraint(sku) {
|
||||
const productCategory = sku.product_name.includes('Cone') ? 'Plastic Cone' : 'Product';
|
||||
|
||||
const template = `
|
||||
=== COMPETITOR COMPARISON COMPLIANCE (LEGAL RED LINE) ===
|
||||
|
||||
This is a product comparison image. You MUST follow these legal requirements:
|
||||
|
||||
OUR PRODUCT SIDE (Left/Colored):
|
||||
- Show the Touchdog ${sku.product_name} in FULL COLOR
|
||||
- Product color: ${sku.color.hex} (${sku.color.name})
|
||||
- Use green checkmark icon (✓) to indicate positive
|
||||
- Label as "OUR" or "TOUCHDOG"
|
||||
- List ONLY the approved selling points from provided data
|
||||
- Product must match reference images exactly
|
||||
|
||||
COMPETITOR SIDE (Right/Grayscale):
|
||||
- Label as "OTHER" or "Traditional ${productCategory}" ONLY
|
||||
- DO NOT use any competitor brand names
|
||||
- DO NOT use any competitor logos or trademarks
|
||||
- DO NOT show any identifiable competitor product designs
|
||||
- Show a GENERIC, UNBRANDED version of traditional product
|
||||
- Apply GRAYSCALE filter to entire competitor side
|
||||
- Use red X icon to indicate negative
|
||||
|
||||
APPROVED COMPETITOR DISADVANTAGES (generic industry facts only):
|
||||
- "Heavy & Bulky"
|
||||
- "Blocks Vision & Movement"
|
||||
- "Hard to Store"
|
||||
- "Uncomfortable for Extended Wear"
|
||||
- "Difficult to Clean"
|
||||
- "Rigid and Inflexible"
|
||||
|
||||
VISUAL TREATMENT:
|
||||
- Our side: Vibrant, colorful, positive imagery
|
||||
- Competitor side: Muted, grayscale, neutral imagery
|
||||
- Clear visual contrast between the two sides
|
||||
- Center divider or clear separation between sides
|
||||
|
||||
STRICTLY PROHIBITED:
|
||||
- ❌ Any specific competitor brand name
|
||||
- ❌ Any competitor logo or trademark symbol
|
||||
- ❌ Any identifiable competitor product photo
|
||||
- ❌ Claims like "better than [Brand]"
|
||||
- ❌ Using competitor's actual product images
|
||||
|
||||
=== END COMPETITOR COMPARISON COMPLIANCE ===
|
||||
`;
|
||||
return template.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成品牌VI约束
|
||||
* @param {object} sku - SKU数据
|
||||
* @param {object} logoPlacement - Logo放置信息
|
||||
* @returns {string} 约束文本
|
||||
*/
|
||||
function generateBrandVIConstraint(sku, logoPlacement = {}) {
|
||||
const position = logoPlacement.position || 'bottom-right';
|
||||
const logoType = logoPlacement.type || 'combined';
|
||||
const logoColor = logoPlacement.color || 'red';
|
||||
|
||||
const template = `
|
||||
=== BRAND VI REQUIREMENTS ===
|
||||
|
||||
EMBROIDERED LOGO ON PRODUCT:
|
||||
- Text: "TOUCHDOG®" (UPPERCASE with ® symbol)
|
||||
- Position: Right petal segment, 2-3 o'clock position
|
||||
- Style: Embroidered, stitched into fabric
|
||||
- Color: Slightly darker shade of product color (${sku.color.hex})
|
||||
|
||||
E-COMMERCE IMAGE BRAND LOGO:
|
||||
- Position: ${position}
|
||||
- Type: ${logoType === 'combined' ? 'Graphic icon + "touchdog®" text + "wow pretty"' : logoType}
|
||||
- Color: ${logoColor === 'red' ? 'Red (#E60012)' : 'White'}
|
||||
|
||||
LOGO SPECIFICATIONS:
|
||||
- Clear space: Minimum 1/4 of logo height on all sides
|
||||
- Minimum size: Combined logo ≥ 46px height
|
||||
- Tagline: "wow pretty" (for international markets)
|
||||
|
||||
PROHIBITED LOGO MODIFICATIONS:
|
||||
- ❌ NO tilting or rotation
|
||||
- ❌ NO outlines or strokes
|
||||
- ❌ NO gradient fills
|
||||
- ❌ NO drop shadows
|
||||
- ❌ NO proportion changes
|
||||
- ❌ NO underlines
|
||||
|
||||
BRAND TYPOGRAPHY:
|
||||
- Headlines: Clean sans-serif, bold weight
|
||||
- Body text: Clean sans-serif, medium weight
|
||||
- Style: Clean, modern, professional
|
||||
|
||||
=== END BRAND VI REQUIREMENTS ===
|
||||
`;
|
||||
return template.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入所有约束到Prompt
|
||||
* @param {string} originalPrompt - 原始Prompt
|
||||
* @param {object} sku - SKU数据
|
||||
* @param {object} options - 选项
|
||||
* @param {boolean} options.isComparison - 是否是竞品对比图
|
||||
* @param {object} options.logoPlacement - Logo放置信息
|
||||
* @returns {string} 注入约束后的完整Prompt
|
||||
*/
|
||||
function injectConstraints(originalPrompt, sku, options = {}) {
|
||||
const { isComparison = false, logoPlacement = {} } = options;
|
||||
|
||||
const constraints = [];
|
||||
|
||||
// 始终添加产品一致性约束
|
||||
constraints.push(generateProductConsistencyConstraint(sku));
|
||||
|
||||
// 始终添加卖点合规约束
|
||||
constraints.push(generateSellingPointConstraint(sku));
|
||||
|
||||
// 如果是竞品对比图,添加竞品合规约束
|
||||
if (isComparison) {
|
||||
constraints.push(generateCompetitorConstraint(sku));
|
||||
}
|
||||
|
||||
// 始终添加品牌VI约束
|
||||
constraints.push(generateBrandVIConstraint(sku, logoPlacement));
|
||||
|
||||
// 添加参考图提示
|
||||
if (sku.ref_images && sku.ref_images.length > 0) {
|
||||
constraints.push(`
|
||||
=== REFERENCE IMAGES ===
|
||||
The following reference images must be used to ensure product consistency:
|
||||
${sku.ref_images.map((url, i) => `- Reference ${i + 1}: ${url}`).join('\n')}
|
||||
Product in generated image must match these reference images exactly.
|
||||
=== END REFERENCE IMAGES ===
|
||||
`);
|
||||
}
|
||||
|
||||
// 组合最终Prompt
|
||||
return `${originalPrompt}
|
||||
|
||||
${constraints.join('\n\n')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取图片的宽高比参数
|
||||
* @param {string} imageId - 图片ID (Main_01 或 APlus_01)
|
||||
* @returns {string} 宽高比参数
|
||||
*/
|
||||
function getAspectRatio(imageId) {
|
||||
if (imageId.startsWith('Main_')) {
|
||||
return '1:1';
|
||||
} else if (imageId.startsWith('APlus_')) {
|
||||
return '3:2';
|
||||
}
|
||||
return '1:1';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片尺寸
|
||||
* @param {string} imageId - 图片ID
|
||||
* @returns {object} 尺寸信息
|
||||
*/
|
||||
function getImageSize(imageId) {
|
||||
if (imageId.startsWith('Main_')) {
|
||||
return { width: 1600, height: 1600, label: '1600x1600' };
|
||||
} else if (imageId.startsWith('APlus_')) {
|
||||
return { width: 970, height: 600, label: '970x600' };
|
||||
}
|
||||
return { width: 1600, height: 1600, label: '1600x1600' };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
injectConstraints,
|
||||
generateProductConsistencyConstraint,
|
||||
generateSellingPointConstraint,
|
||||
generateCompetitorConstraint,
|
||||
generateBrandVIConstraint,
|
||||
getAspectRatio,
|
||||
getImageSize,
|
||||
renderTemplate,
|
||||
processEachBlocks
|
||||
};
|
||||
|
||||
|
||||
|
||||
338
lib/image-processor.js
Normal file
338
lib/image-processor.js
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* 图片处理工具模块
|
||||
* 使用Sharp库实现:抠图、合成、叠加文字/标注
|
||||
*/
|
||||
|
||||
const sharp = require('sharp');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 调整图片大小,保持比例
|
||||
*/
|
||||
async function resizeImage(inputPath, width, height, fit = 'contain') {
|
||||
const buffer = await sharp(inputPath)
|
||||
.resize(width, height, { fit, background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
||||
.toBuffer();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片转换为PNG(保持透明度)
|
||||
*/
|
||||
async function toPng(inputPath) {
|
||||
return await sharp(inputPath).png().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建纯色背景
|
||||
*/
|
||||
async function createSolidBackground(width, height, color = '#FFFFFF') {
|
||||
// 解析颜色
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
|
||||
return await sharp({
|
||||
create: {
|
||||
width,
|
||||
height,
|
||||
channels: 3,
|
||||
background: { r, g, b }
|
||||
}
|
||||
}).jpeg().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建渐变背景
|
||||
*/
|
||||
async function createGradientBackground(width, height, color1 = '#F5EDE4', color2 = '#FFFFFF') {
|
||||
// 创建简单的渐变效果(从上到下)
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${color1};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${color2};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#grad)"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return await sharp(Buffer.from(svg)).jpeg().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 合成多个图层
|
||||
* @param {Buffer} baseImage - 底图
|
||||
* @param {Array} layers - 图层数组 [{buffer, left, top, width?, height?}]
|
||||
*/
|
||||
async function compositeImages(baseImage, layers) {
|
||||
let composite = sharp(baseImage);
|
||||
|
||||
const compositeInputs = [];
|
||||
|
||||
for (const layer of layers) {
|
||||
let input = layer.buffer;
|
||||
|
||||
// 如果需要调整大小
|
||||
if (layer.width || layer.height) {
|
||||
input = await sharp(layer.buffer)
|
||||
.resize(layer.width, layer.height, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
compositeInputs.push({
|
||||
input,
|
||||
left: layer.left || 0,
|
||||
top: layer.top || 0
|
||||
});
|
||||
}
|
||||
|
||||
return await composite.composite(compositeInputs).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在图片上叠加文字
|
||||
*/
|
||||
async function addTextOverlay(baseImage, texts) {
|
||||
// texts: [{text, x, y, fontSize, color, fontWeight, align}]
|
||||
|
||||
const metadata = await sharp(baseImage).metadata();
|
||||
const width = metadata.width;
|
||||
const height = metadata.height;
|
||||
|
||||
// 创建SVG文字层
|
||||
const textElements = texts.map(t => {
|
||||
const fontSize = t.fontSize || 40;
|
||||
const color = t.color || '#333333';
|
||||
const fontWeight = t.fontWeight || 'bold';
|
||||
const textAnchor = t.align === 'center' ? 'middle' : (t.align === 'right' ? 'end' : 'start');
|
||||
|
||||
return `<text x="${t.x}" y="${t.y}" font-size="${fontSize}" fill="${color}"
|
||||
font-weight="${fontWeight}" font-family="Arial, sans-serif" text-anchor="${textAnchor}">${escapeXml(t.text)}</text>`;
|
||||
}).join('\n');
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
${textElements}
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return await sharp(baseImage)
|
||||
.composite([{ input: Buffer.from(svg), top: 0, left: 0 }])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建圆形裁剪的图片(用于细节放大镜效果)
|
||||
*/
|
||||
async function createCircularCrop(inputBuffer, diameter) {
|
||||
const circle = Buffer.from(`
|
||||
<svg width="${diameter}" height="${diameter}">
|
||||
<circle cx="${diameter/2}" cy="${diameter/2}" r="${diameter/2}" fill="white"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const resized = await sharp(inputBuffer)
|
||||
.resize(diameter, diameter, { fit: 'cover' })
|
||||
.toBuffer();
|
||||
|
||||
return await sharp(resized)
|
||||
.composite([{ input: circle, blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加圆形边框
|
||||
*/
|
||||
async function addCircleBorder(inputBuffer, diameter, borderWidth = 3, borderColor = '#2D4A3E') {
|
||||
const r = parseInt(borderColor.slice(1, 3), 16);
|
||||
const g = parseInt(borderColor.slice(3, 5), 16);
|
||||
const b = parseInt(borderColor.slice(5, 7), 16);
|
||||
|
||||
const borderSvg = Buffer.from(`
|
||||
<svg width="${diameter}" height="${diameter}">
|
||||
<circle cx="${diameter/2}" cy="${diameter/2}" r="${diameter/2 - borderWidth/2}"
|
||||
fill="none" stroke="rgb(${r},${g},${b})" stroke-width="${borderWidth}"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
return await sharp(inputBuffer)
|
||||
.composite([{ input: borderSvg, top: 0, left: 0 }])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带箭头的标注线
|
||||
*/
|
||||
async function createArrowLine(width, height, fromX, fromY, toX, toY, color = '#2D4A3E') {
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
|
||||
// 计算箭头角度
|
||||
const angle = Math.atan2(toY - fromY, toX - fromX);
|
||||
const arrowLength = 15;
|
||||
const arrowAngle = Math.PI / 6;
|
||||
|
||||
const arrow1X = toX - arrowLength * Math.cos(angle - arrowAngle);
|
||||
const arrow1Y = toY - arrowLength * Math.sin(angle - arrowAngle);
|
||||
const arrow2X = toX - arrowLength * Math.cos(angle + arrowAngle);
|
||||
const arrow2Y = toY - arrowLength * Math.sin(angle + arrowAngle);
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<line x1="${fromX}" y1="${fromY}" x2="${toX}" y2="${toY}"
|
||||
stroke="rgb(${r},${g},${b})" stroke-width="2"/>
|
||||
<polygon points="${toX},${toY} ${arrow1X},${arrow1Y} ${arrow2X},${arrow2Y}"
|
||||
fill="rgb(${r},${g},${b})"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return Buffer.from(svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标题横幅
|
||||
*/
|
||||
async function createTitleBanner(width, height, text, bgColor = '#2D4A3E', textColor = '#FFFFFF') {
|
||||
const fontSize = Math.floor(height * 0.5);
|
||||
|
||||
const svg = `
|
||||
<svg width="${width}" height="${height}">
|
||||
<rect width="100%" height="100%" fill="${bgColor}" rx="5" ry="5"/>
|
||||
<text x="${width/2}" y="${height/2 + fontSize/3}" font-size="${fontSize}"
|
||||
fill="${textColor}" font-weight="bold" font-family="Arial, sans-serif"
|
||||
text-anchor="middle">"${escapeXml(text)}"</text>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return await sharp(Buffer.from(svg)).png().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片转为灰度(用于竞品对比)
|
||||
*/
|
||||
async function toGrayscale(inputBuffer) {
|
||||
return await sharp(inputBuffer).grayscale().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整图片亮度/对比度
|
||||
*/
|
||||
async function adjustBrightness(inputBuffer, brightness = 1.0) {
|
||||
return await sharp(inputBuffer)
|
||||
.modulate({ brightness })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加阴影效果
|
||||
*/
|
||||
async function addShadow(inputBuffer, offsetX = 5, offsetY = 5, blur = 10, opacity = 0.3) {
|
||||
const metadata = await sharp(inputBuffer).metadata();
|
||||
const width = metadata.width + offsetX + blur * 2;
|
||||
const height = metadata.height + offsetY + blur * 2;
|
||||
|
||||
// 创建阴影层
|
||||
const shadowBuffer = await sharp(inputBuffer)
|
||||
.greyscale()
|
||||
.modulate({ brightness: 0 })
|
||||
.blur(blur)
|
||||
.toBuffer();
|
||||
|
||||
// 创建透明背景
|
||||
const background = await sharp({
|
||||
create: {
|
||||
width,
|
||||
height,
|
||||
channels: 4,
|
||||
background: { r: 255, g: 255, b: 255, alpha: 0 }
|
||||
}
|
||||
}).png().toBuffer();
|
||||
|
||||
// 合成
|
||||
return await sharp(background)
|
||||
.composite([
|
||||
{ input: shadowBuffer, left: offsetX + blur, top: offsetY + blur, blend: 'over' },
|
||||
{ input: inputBuffer, left: blur, top: blur }
|
||||
])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪图片的特定区域
|
||||
*/
|
||||
async function cropRegion(inputBuffer, left, top, width, height) {
|
||||
return await sharp(inputBuffer)
|
||||
.extract({ left, top, width, height })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:转义XML特殊字符
|
||||
*/
|
||||
function escapeXml(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存图片到文件
|
||||
*/
|
||||
async function saveImage(buffer, outputPath, format = 'jpeg', quality = 90) {
|
||||
let pipeline = sharp(buffer);
|
||||
|
||||
if (format === 'jpeg' || format === 'jpg') {
|
||||
pipeline = pipeline.jpeg({ quality });
|
||||
} else if (format === 'png') {
|
||||
pipeline = pipeline.png();
|
||||
}
|
||||
|
||||
await pipeline.toFile(outputPath);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取图片为Buffer
|
||||
*/
|
||||
async function readImage(imagePath) {
|
||||
return fs.readFileSync(imagePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片尺寸
|
||||
*/
|
||||
async function getImageSize(inputPath) {
|
||||
const metadata = await sharp(inputPath).metadata();
|
||||
return { width: metadata.width, height: metadata.height };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resizeImage,
|
||||
toPng,
|
||||
createSolidBackground,
|
||||
createGradientBackground,
|
||||
compositeImages,
|
||||
addTextOverlay,
|
||||
createCircularCrop,
|
||||
addCircleBorder,
|
||||
createArrowLine,
|
||||
createTitleBanner,
|
||||
toGrayscale,
|
||||
adjustBrightness,
|
||||
addShadow,
|
||||
cropRegion,
|
||||
saveImage,
|
||||
readImage,
|
||||
getImageSize
|
||||
};
|
||||
|
||||
|
||||
629
lib/template-config.js
Normal file
629
lib/template-config.js
Normal file
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* 可配置的套路模板系统
|
||||
* 运营人员可以自定义每张图的布局和内容
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// 默认套路配置(基于真实交付成品分析)
|
||||
// ============================================================
|
||||
|
||||
const DEFAULT_MAIN_TEMPLATES = {
|
||||
// Main_01: 场景首图 + 卖点文字
|
||||
Main_01: {
|
||||
type: 'lifestyle_with_features',
|
||||
name: '场景首图+卖点',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
scene: 'left-center', // 场景图位置
|
||||
title: 'center-bottom', // 标题位置
|
||||
features: 'bottom-row' // 卖点位置
|
||||
},
|
||||
config: {
|
||||
title: 'DESIGNED FOR COMFORTABLE RECOVERY',
|
||||
titleStyle: 'curved-banner', // curved-banner | simple | none
|
||||
features: [
|
||||
{ icon: 'egg', text: 'LIGHTER THAN AN EGG' },
|
||||
{ icon: 'water', text: 'WATERPROOF & EASY WIPE' },
|
||||
{ icon: 'cloud', text: 'BREATHABLE COTTON LINING' }
|
||||
],
|
||||
background: 'warm-home'
|
||||
}
|
||||
},
|
||||
|
||||
// Main_02: 白底平铺 + 局部放大(关键改进!)
|
||||
Main_02: {
|
||||
type: 'product_with_callouts',
|
||||
name: '白底平铺+细节放大',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
product: 'center',
|
||||
callouts: 'corners' // 放大镜效果在角落
|
||||
},
|
||||
config: {
|
||||
title: 'DURABLE WATERPROOF PU LAYER',
|
||||
titlePosition: 'top',
|
||||
callouts: [
|
||||
{
|
||||
position: 'bottom-left',
|
||||
target: 'material-edge',
|
||||
label: 'DURABLE WATERPROOF PU LAYER',
|
||||
hasArrow: true
|
||||
},
|
||||
{
|
||||
position: 'bottom-right',
|
||||
target: 'neck-binding',
|
||||
label: 'DOUBLE-LAYER COMFORT',
|
||||
hasArrow: true
|
||||
}
|
||||
],
|
||||
background: 'white'
|
||||
}
|
||||
},
|
||||
|
||||
// Main_03: 功能调节展示
|
||||
Main_03: {
|
||||
type: 'feature_showcase',
|
||||
name: '功能调节展示',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
mainScene: 'left',
|
||||
featureCircles: 'right-column'
|
||||
},
|
||||
config: {
|
||||
title: 'ADJUSTABLE STRAP FOR A SECURE FIT',
|
||||
titlePosition: 'top-left',
|
||||
mainScene: {
|
||||
type: 'cat-wearing',
|
||||
background: 'lifestyle'
|
||||
},
|
||||
featureCircles: [
|
||||
{ label: 'SECURE THE ADJUSTABLE BELLY STRAP', showDetail: 'velcro-close-up' },
|
||||
{ label: 'ADJUST FOR A SNUG FIT', showDetail: 'full-product-view' }
|
||||
],
|
||||
background: 'matching-color' // 使用产品主色作为背景
|
||||
}
|
||||
},
|
||||
|
||||
// Main_04: 多场景使用(4宫格)
|
||||
Main_04: {
|
||||
type: 'multi_scenario_grid',
|
||||
name: '多场景使用',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
grid: '2x2',
|
||||
captionPosition: 'below-each'
|
||||
},
|
||||
config: {
|
||||
scenarios: [
|
||||
{ scene: 'standing', caption: '• HYGIENIC & EASY TO CLEAN', subtext: 'WATERPROOF OUTER LAYER' },
|
||||
{ scene: 'eating', caption: '• UNRESTRICTED EATING/DRINKING', subtext: 'SPECIALLY DESIGNED OPENING' },
|
||||
{ scene: 'playing', caption: '• REVERSIBLE WEAR', subtext: 'FLIP-OVER DESIGN' },
|
||||
{ scene: 'sleeping', caption: '• 360° COMFORT', subtext: 'FREE MOVEMENT' }
|
||||
],
|
||||
background: 'warm-beige'
|
||||
}
|
||||
},
|
||||
|
||||
// Main_05: 尺寸图
|
||||
Main_05: {
|
||||
type: 'size_chart',
|
||||
name: '尺寸图',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
product: 'top-center',
|
||||
table: 'bottom'
|
||||
},
|
||||
config: {
|
||||
title: 'PRODUCT SIZE',
|
||||
measurements: ['NECK', 'WIDTH'],
|
||||
sizeChart: {
|
||||
XS: { neck: '5.6-6.8IN', depth: '3.2IN' },
|
||||
S: { neck: '7.2-8.4IN', depth: '4IN' },
|
||||
M: { neck: '8.8-10.4IN', depth: '5IN' },
|
||||
L: { neck: '10.8-12.4IN', depth: '6IN' },
|
||||
XL: { neck: '12.8-14.4IN', depth: '7IN' }
|
||||
},
|
||||
footer: 'NOTE: ALWAYS MEASURE YOUR PET\'S NECK BEFORE SELECTING A SIZE'
|
||||
}
|
||||
},
|
||||
|
||||
// Main_06: 多角度展示
|
||||
Main_06: {
|
||||
type: 'multiple_angles',
|
||||
name: '多角度展示',
|
||||
aspectRatio: '1:1',
|
||||
layout: {
|
||||
images: 'side-by-side',
|
||||
divider: 'curved-line'
|
||||
},
|
||||
config: {
|
||||
angles: [
|
||||
{ view: 'front', petType: 'cat' },
|
||||
{ view: 'side', petType: 'cat' }
|
||||
],
|
||||
background: 'warm-interior'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_APLUS_TEMPLATES = {
|
||||
// APlus_01: 品牌横幅
|
||||
APlus_01: {
|
||||
type: 'brand_banner',
|
||||
name: '品牌横幅',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
brandName: 'TOUCHDOG',
|
||||
productName: 'CAT SOFT CONE COLLAR',
|
||||
brandStyle: 'playful-curved', // 品牌字体风格
|
||||
brandColor: '#E8A87C', // 珊瑚色
|
||||
scene: 'cat-on-furniture',
|
||||
background: 'warm-home'
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_02: 对比图(关键改进!)
|
||||
APlus_02: {
|
||||
type: 'comparison',
|
||||
name: '对比图',
|
||||
aspectRatio: '3:2',
|
||||
layout: {
|
||||
left: 'our-product',
|
||||
right: 'competitor',
|
||||
center: 'none' // 不要中间的产品图!
|
||||
},
|
||||
config: {
|
||||
leftSide: {
|
||||
label: 'OUR',
|
||||
labelBg: '#E8876C',
|
||||
checkmark: true,
|
||||
colorful: true,
|
||||
sellingPoints: [
|
||||
'CLOUD-LIGHT COMFORT',
|
||||
'WIDER & CLEARER',
|
||||
'FOLDABLE & PORTABLE'
|
||||
]
|
||||
},
|
||||
rightSide: {
|
||||
label: 'OTHER',
|
||||
labelBg: '#808080',
|
||||
xmark: true,
|
||||
grayscale: true,
|
||||
weaknesses: [
|
||||
'HEAVY & BULKY',
|
||||
'BLOCKS VISION & MOVEMENT',
|
||||
'HARD TO STORE'
|
||||
]
|
||||
},
|
||||
background: 'warm-beige'
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_03: 功能细节
|
||||
APlus_03: {
|
||||
type: 'feature_details',
|
||||
name: '功能细节',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
title: 'ENGINEERED FOR UNCOMPROMISED COMFORT',
|
||||
detailImages: [
|
||||
{ focus: 'inner-lining', caption: 'STURDY AND BREATHABLE', subtext: 'DURABLE AND COMFORTABLE' },
|
||||
{ focus: 'wearing-with-size', caption: 'EASY TO CLEAN, STYLISH', subtext: 'AND ATTRACTIVE' },
|
||||
{ focus: 'stitching-detail', caption: 'REINFORCED STITCHING', subtext: 'AND DURABLE FABRIC' }
|
||||
],
|
||||
background: 'warm-beige'
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_04: 多场景横版
|
||||
APlus_04: {
|
||||
type: 'multi_scenario_horizontal',
|
||||
name: '多场景横版',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
scenarios: [
|
||||
{ scene: 'standing', caption: '• HYGIENIC & EASY TO CLEAN' },
|
||||
{ scene: 'eating', caption: '• UNRESTRICTED EATING/DRINKING' },
|
||||
{ scene: 'playing', caption: '• REVERSIBLE WEAR' },
|
||||
{ scene: 'sleeping', caption: '• 360° COMFORT' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_05: 多角度横版
|
||||
APlus_05: {
|
||||
type: 'multiple_angles_horizontal',
|
||||
name: '多角度横版',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
angles: ['front', 'side'],
|
||||
dividerStyle: 'curved'
|
||||
}
|
||||
},
|
||||
|
||||
// APlus_06: 尺寸表横版
|
||||
APlus_06: {
|
||||
type: 'size_chart_horizontal',
|
||||
name: '尺寸表横版',
|
||||
aspectRatio: '3:2',
|
||||
config: {
|
||||
title: 'PRODUCT SIZE',
|
||||
layout: 'product-left-table-right'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 从配置生成Prompt的函数
|
||||
// ============================================================
|
||||
|
||||
function generatePromptFromConfig(templateConfig, product, skuInfo) {
|
||||
const { type, config, layout } = templateConfig;
|
||||
|
||||
// 基础产品描述
|
||||
const productDesc = product.goldenDescription || 'Pet recovery cone collar';
|
||||
|
||||
switch (type) {
|
||||
case 'lifestyle_with_features':
|
||||
return generateLifestyleWithFeatures(productDesc, config, skuInfo);
|
||||
|
||||
case 'product_with_callouts':
|
||||
return generateProductWithCallouts(productDesc, config, skuInfo);
|
||||
|
||||
case 'feature_showcase':
|
||||
return generateFeatureShowcase(productDesc, config, skuInfo);
|
||||
|
||||
case 'multi_scenario_grid':
|
||||
return generateMultiScenarioGrid(productDesc, config, skuInfo);
|
||||
|
||||
case 'size_chart':
|
||||
return generateSizeChart(productDesc, config, skuInfo);
|
||||
|
||||
case 'multiple_angles':
|
||||
return generateMultipleAngles(productDesc, config, skuInfo);
|
||||
|
||||
case 'brand_banner':
|
||||
return generateBrandBanner(productDesc, config, skuInfo);
|
||||
|
||||
case 'comparison':
|
||||
return generateComparison(productDesc, config, skuInfo);
|
||||
|
||||
case 'feature_details':
|
||||
return generateFeatureDetails(productDesc, config, skuInfo);
|
||||
|
||||
default:
|
||||
return generateGenericPrompt(productDesc, config, skuInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 各类型Prompt生成函数
|
||||
// ============================================================
|
||||
|
||||
function generateLifestyleWithFeatures(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - LIFESTYLE WITH FEATURE TEXT]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
SCENE:
|
||||
- Beautiful ${skuInfo.petType || 'cat'} wearing the product comfortably
|
||||
- Warm, cozy home interior background (soft focus)
|
||||
- Product clearly visible, occupies 50-60% of frame
|
||||
- Pet looks comfortable and relaxed
|
||||
|
||||
TEXT OVERLAY REQUIREMENTS:
|
||||
- ${config.titleStyle === 'curved-banner' ? 'CURVED BANNER in muted blue (#8BB8C4) across center' : 'SIMPLE TITLE at center-bottom'}
|
||||
- TITLE TEXT: "${config.title}" in white, clean sans-serif font
|
||||
- BOTTOM ROW: 3 feature boxes in rounded rectangles
|
||||
${config.features.map((f, i) => ` - Box ${i+1}: "${f.text}" with ${f.icon} icon`).join('\n')}
|
||||
|
||||
STYLE:
|
||||
- Professional Amazon product photography
|
||||
- Warm color palette
|
||||
- Clean, readable text
|
||||
- Subtle paw print watermarks
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product must match reference image EXACTLY in shape, color, and material.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateProductWithCallouts(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - PRODUCT WITH DETAIL CALLOUTS]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- TOP: Title "${config.title}" in dark green banner with white text
|
||||
- CENTER: Product C-shape flat lay view on white/light background
|
||||
- CALLOUT CIRCLES: Magnifying glass style detail views with arrows pointing to product
|
||||
|
||||
CALLOUT DETAILS:
|
||||
${config.callouts.map(c => `- ${c.position.toUpperCase()}: Circular magnified view of ${c.target}
|
||||
Label: "${c.label}"
|
||||
Arrow pointing from circle to corresponding area on product`).join('\n')}
|
||||
|
||||
STYLE:
|
||||
- Clean product photography with infographic elements
|
||||
- Dark green (#2D4A3E) for title banner
|
||||
- Thin lines/arrows connecting callouts to product
|
||||
- Professional Amazon listing style
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product must show C-shaped opening clearly. Match reference image EXACTLY.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateFeatureShowcase(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - FEATURE SHOWCASE]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- BACKGROUND: Solid color matching product main color (use product's primary color)
|
||||
- TOP-LEFT: Large title "${config.title}" in dark text
|
||||
- LEFT SIDE (60%): Main lifestyle scene - ${skuInfo.petType || 'cat'} wearing product, walking through doorway/arch
|
||||
- RIGHT SIDE (40%): 2 feature detail circles stacked vertically
|
||||
|
||||
FEATURE CIRCLES:
|
||||
${config.featureCircles.map((f, i) => `${i+1}. Circle showing: ${f.showDetail}
|
||||
Button/label below: "${f.label}"`).join('\n')}
|
||||
|
||||
STYLE:
|
||||
- Lifestyle photography meets infographic
|
||||
- Rounded rectangle buttons for labels
|
||||
- Paw print decorations scattered on background
|
||||
- Modern, clean design
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product color and shape must match reference image EXACTLY.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateMultiScenarioGrid(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - MULTI-SCENARIO 2x2 GRID]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT: 2x2 grid of scenes, each in rounded rectangle frame
|
||||
|
||||
${config.scenarios.map((s, i) => `SCENE ${i+1} (${['TOP-LEFT', 'TOP-RIGHT', 'BOTTOM-LEFT', 'BOTTOM-RIGHT'][i]}):
|
||||
- ${skuInfo.petType || 'Cat'} ${s.scene} while wearing product
|
||||
- Caption below: "${s.caption}"
|
||||
- Subtext: "${s.subtext}"`).join('\n\n')}
|
||||
|
||||
BACKGROUND: Warm beige (#F5EDE4) with subtle paw print watermarks
|
||||
|
||||
STYLE:
|
||||
- Each scene in rounded rectangle with slight shadow
|
||||
- Captions in dark text, clean sans-serif
|
||||
- Professional lifestyle photography
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product in ALL 4 scenes must be identical and match reference image.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateSizeChart(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - SIZE CHART INFOGRAPHIC]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- TOP: Title "${config.title}" in bold dark text
|
||||
- CENTER: Product flat lay with measurement arrows and labels
|
||||
- Arrow across neck opening: "NECK"
|
||||
- Arrow across width: "WIDTH"
|
||||
- BOTTOM: Size chart table
|
||||
|
||||
SIZE CHART TABLE (clean, rounded corners):
|
||||
| SIZE | NECK CIRCUMFERENCE | DEPTH |
|
||||
${Object.entries(config.sizeChart).map(([size, dims]) =>
|
||||
`| ${size} | ${dims.neck} | ${dims.depth} |`).join('\n')}
|
||||
|
||||
FOOTER TEXT: "${config.footer}"
|
||||
|
||||
BACKGROUND: Warm beige with subtle paw prints
|
||||
|
||||
STYLE:
|
||||
- Clean infographic design
|
||||
- Table with alternating row colors
|
||||
- Professional product photography
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateMultipleAngles(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON MAIN IMAGE - MULTIPLE ANGLES]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- Split view with decorative curved divider in center
|
||||
- LEFT: ${skuInfo.petType || 'Cat'} wearing product, ${config.angles[0].view} view
|
||||
- RIGHT: Same ${skuInfo.petType || 'cat'} or similar, ${config.angles[1].view} view
|
||||
|
||||
SCENE:
|
||||
- Warm home interior background
|
||||
- Both pets look comfortable
|
||||
- Product clearly visible from different angles
|
||||
|
||||
STYLE:
|
||||
- Lifestyle photography
|
||||
- Soft warm lighting
|
||||
- Decorative curved line or wave between images
|
||||
- NO text overlay
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
|
||||
CRITICAL: Product must be IDENTICAL in both views, matching reference image exactly.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateBrandBanner(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON A+ BRAND BANNER - HORIZONTAL]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT (970x600px aspect ratio):
|
||||
- LEFT 40%: Brand text area with decorative elements
|
||||
- RIGHT 60%: Lifestyle scene
|
||||
|
||||
LEFT SIDE:
|
||||
- Brand name "${config.brandName}" in playful, slightly curved font
|
||||
- Color: ${config.brandColor} (coral/salmon)
|
||||
- Decorative paw prints around text
|
||||
- Product name "${config.productName}" below in smaller gray text
|
||||
|
||||
RIGHT SIDE:
|
||||
- ${skuInfo.petType || 'Cat'} wearing product on modern furniture
|
||||
- Warm cozy interior background
|
||||
|
||||
STYLE:
|
||||
- Professional Amazon A+ content
|
||||
- Warm, inviting color palette
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateComparison(productDesc, config, skuInfo) {
|
||||
const { leftSide, rightSide } = config;
|
||||
|
||||
return `
|
||||
[AMAZON A+ COMPARISON IMAGE - SPLIT SCREEN]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT: Two-column comparison, NO center product image
|
||||
|
||||
LEFT SIDE (OUR PRODUCT):
|
||||
- GREEN CHECKMARK icon at top
|
||||
- "${leftSide.label}" label on ${leftSide.labelBg} background
|
||||
- ${skuInfo.petType || 'Cat'} wearing OUR PRODUCT (must match reference exactly!)
|
||||
- Cat looks HAPPY and comfortable
|
||||
- FULL COLOR, warm tones
|
||||
- Selling points in white text on colored background:
|
||||
${leftSide.sellingPoints.map(p => ` • ${p}`).join('\n')}
|
||||
|
||||
RIGHT SIDE (COMPETITOR):
|
||||
- RED X-MARK icon at top
|
||||
- "${rightSide.label}" label on ${rightSide.labelBg} background
|
||||
- ${skuInfo.petType || 'Cat'} wearing HARD PLASTIC transparent cone
|
||||
- Cat looks SAD/uncomfortable
|
||||
- GRAYSCALE/desaturated
|
||||
- Weaknesses in gray text:
|
||||
${rightSide.weaknesses.map(p => ` • ${p}`).join('\n')}
|
||||
|
||||
BACKGROUND: Warm beige (#F5EDE4) with paw print watermarks
|
||||
|
||||
STYLE:
|
||||
- Clear visual contrast between sides
|
||||
- Professional comparison layout
|
||||
- Clean readable text
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
|
||||
CRITICAL:
|
||||
- LEFT product must match reference image EXACTLY (our soft cone)
|
||||
- RIGHT shows generic hard plastic cone (NOT our product)
|
||||
- NO product image in the center
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateFeatureDetails(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON A+ FEATURE DETAILS - HORIZONTAL]
|
||||
|
||||
PRODUCT (MUST MATCH REFERENCE IMAGE EXACTLY):
|
||||
${productDesc}
|
||||
|
||||
LAYOUT:
|
||||
- TOP: Large title "${config.title}" in bold dark font
|
||||
- MIDDLE: 3 detail images in rounded rectangles, evenly spaced
|
||||
|
||||
${config.detailImages.map((d, i) => `DETAIL ${i+1}:
|
||||
- Focus: ${d.focus}
|
||||
- Caption: "${d.caption}"
|
||||
- Subtext: "${d.subtext}"`).join('\n\n')}
|
||||
|
||||
BACKGROUND: Warm beige (#F5EDE4) with subtle paw print watermarks
|
||||
|
||||
STYLE:
|
||||
- Professional product detail photography
|
||||
- Clean modern typography
|
||||
- Rounded rectangle frames for each detail
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function generateGenericPrompt(productDesc, config, skuInfo) {
|
||||
return `
|
||||
[AMAZON PRODUCT IMAGE]
|
||||
|
||||
PRODUCT:
|
||||
${productDesc}
|
||||
|
||||
REQUIREMENTS:
|
||||
- Professional product photography
|
||||
- Match reference image exactly
|
||||
- High quality, clear details
|
||||
- 8K resolution
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 导出
|
||||
// ============================================================
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_MAIN_TEMPLATES,
|
||||
DEFAULT_APLUS_TEMPLATES,
|
||||
generatePromptFromConfig,
|
||||
|
||||
// 生成所有12张图的Prompts
|
||||
generateAllPrompts: (product, skuInfo, customConfig = {}) => {
|
||||
const mainTemplates = { ...DEFAULT_MAIN_TEMPLATES, ...customConfig.main };
|
||||
const aplusTemplates = { ...DEFAULT_APLUS_TEMPLATES, ...customConfig.aplus };
|
||||
|
||||
const prompts = [];
|
||||
|
||||
// 主图6张
|
||||
for (const [id, template] of Object.entries(mainTemplates)) {
|
||||
prompts.push({
|
||||
id,
|
||||
name: template.name,
|
||||
aspectRatio: template.aspectRatio || '1:1',
|
||||
type: template.type,
|
||||
prompt: generatePromptFromConfig(template, product, skuInfo)
|
||||
});
|
||||
}
|
||||
|
||||
// A+图6张
|
||||
for (const [id, template] of Object.entries(aplusTemplates)) {
|
||||
prompts.push({
|
||||
id,
|
||||
name: template.name,
|
||||
aspectRatio: template.aspectRatio || '3:2',
|
||||
type: template.type,
|
||||
prompt: generatePromptFromConfig(template, product, skuInfo)
|
||||
});
|
||||
}
|
||||
|
||||
return prompts;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
198
lib/vision-extractor.js
Normal file
198
lib/vision-extractor.js
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
|
||||
|
||||
3811
package-lock.json
generated
Normal file
3811
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "ai-image-generator",
|
||||
"version": "1.0.0",
|
||||
"description": "AI Image Generation SPA",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.400.0",
|
||||
"axios": "^1.6.0",
|
||||
"canvas": "^3.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
}
|
||||
395
poc-workflow-v2.js
Normal file
395
poc-workflow-v2.js
Normal file
@@ -0,0 +1,395 @@
|
||||
/**
|
||||
* POC Workflow V2: 强化产品一致性
|
||||
* 核心改进:
|
||||
* 1. 极致详细的产品描述 (Golden Description)
|
||||
* 2. 移除AI生成logo(后期处理)
|
||||
* 3. 分级策略:A级严格复制,B级可控创意
|
||||
* 4. 对比图特殊处理
|
||||
*/
|
||||
|
||||
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_v2');
|
||||
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-v2-${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: 黄金产品描述 (Golden Product Description)
|
||||
// ============================================================
|
||||
const GOLDEN_PRODUCT_DESC = `
|
||||
EXACT PRODUCT APPEARANCE (MUST MATCH PRECISELY):
|
||||
- Shape: 8-PETAL FLOWER/FAN shape, C-shaped opening (like Pac-Man), NOT a full circle
|
||||
- Color: ICE BLUE / Light aqua blue (#C5E8ED approximate)
|
||||
- Material: Glossy waterproof PU fabric with visible stitching lines between petals
|
||||
- Edge binding: TEAL/TURQUOISE color binding around the inner neck hole
|
||||
- Closure: White velcro strap on one petal end
|
||||
- Logo: "TOUCHDOG®" embroidered in matching blue thread on one petal (small, subtle)
|
||||
- Texture: Smooth, slightly shiny, NOT matte, NOT cotton fabric
|
||||
- Structure: Soft but structured, maintains petal shape, NOT floppy
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- ❌ NO printed patterns or colorful fabric designs
|
||||
- ❌ NO hard plastic transparent cones
|
||||
- ❌ NO fully circular/closed shapes
|
||||
- ❌ NO matte cotton or fleece textures
|
||||
- ❌ NO random brand logos or text
|
||||
`;
|
||||
|
||||
// ============================================================
|
||||
// 🔥 核心改进2: 分级Prompt策略
|
||||
// ============================================================
|
||||
|
||||
// A级:严格产品复制 (必须和参考图几乎一致)
|
||||
function createPromptA_Strict(sceneDesc) {
|
||||
return `
|
||||
[PRODUCT REPRODUCTION - HIGHEST FIDELITY]
|
||||
${GOLDEN_PRODUCT_DESC}
|
||||
|
||||
SCENE: ${sceneDesc}
|
||||
|
||||
INSTRUCTION: Reproduce the EXACT product from reference image.
|
||||
The product shape, color, and material must be IDENTICAL to reference.
|
||||
Only change the background/scene as described.
|
||||
DO NOT add any brand logo overlays or text graphics.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// B级:可控创意 (产品保持一致,场景可发挥)
|
||||
function createPromptB_Controlled(sceneDesc, extraInstructions = '') {
|
||||
return `
|
||||
[CONTROLLED CREATIVE - PRODUCT CONSISTENCY REQUIRED]
|
||||
${GOLDEN_PRODUCT_DESC}
|
||||
|
||||
SCENE: ${sceneDesc}
|
||||
|
||||
${extraInstructions}
|
||||
|
||||
INSTRUCTION: Keep product appearance EXACTLY as described above.
|
||||
Creative freedom allowed ONLY for background, lighting, and composition.
|
||||
DO NOT modify product shape, color, or material.
|
||||
DO NOT add any brand logo overlays.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// C级:对比图专用 (左边严格,右边自由)
|
||||
function createPromptC_Comparison() {
|
||||
return `
|
||||
[COMPARISON IMAGE - SPLIT SCREEN]
|
||||
|
||||
LEFT SIDE (OUR PRODUCT - STRICT):
|
||||
${GOLDEN_PRODUCT_DESC}
|
||||
- Happy, comfortable cat/dog wearing the ICE BLUE 8-PETAL soft cone
|
||||
- Bright, colorful, warm lighting
|
||||
- GREEN CHECKMARK overlay
|
||||
|
||||
RIGHT SIDE (GENERIC COMPETITOR - FLEXIBLE):
|
||||
- Generic transparent HARD PLASTIC cone (the traditional "lamp shade" type)
|
||||
- Sad, uncomfortable looking cat/dog
|
||||
- Grayscale / desaturated colors
|
||||
- RED X-MARK overlay
|
||||
|
||||
LAYOUT: Side by side, clear visual contrast
|
||||
TEXT HEADER: "Soft & Comfortable" vs "Hard & Uncomfortable"
|
||||
|
||||
CRITICAL: Left side product must be ICE BLUE 8-PETAL FLOWER shape, NOT any other design!
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// 生图任务提交
|
||||
async function submitImageTask(prompt, refImageUrl, aspectRatio = '1:1') {
|
||||
try {
|
||||
const payload = {
|
||||
key: API_KEY,
|
||||
prompt: prompt,
|
||||
img_url: refImageUrl, // 强参考图
|
||||
aspectRatio: aspectRatio,
|
||||
imageSize: '1K'
|
||||
};
|
||||
|
||||
console.log(` 提交任务...`);
|
||||
|
||||
const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload);
|
||||
const taskId = response.data.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;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId }
|
||||
});
|
||||
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.status === 2 && data.image_url) {
|
||||
return data.image_url;
|
||||
} else if (data && data.status === 3) {
|
||||
throw new Error('Generation failed: ' + (data.fail_reason || 'Unknown'));
|
||||
}
|
||||
|
||||
process.stdout.write('.');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
attempts++;
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('Generation failed')) throw error;
|
||||
console.error('Poll error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw new Error('Timeout waiting for image');
|
||||
}
|
||||
|
||||
// 生成单张图(含重试)
|
||||
async function generateSingleImage(item, refUrls) {
|
||||
console.log(`\n🎨 [${item.level}级] ${item.id}: ${item.name}`);
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 2;
|
||||
|
||||
// 根据类型选择最佳参考图
|
||||
let refUrl = refUrls.flat; // 默认用平铺图
|
||||
if (item.useWornRef) {
|
||||
refUrl = refUrls.worn;
|
||||
}
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
if (retryCount > 0) console.log(` 重试第 ${retryCount} 次...`);
|
||||
|
||||
const taskId = await submitImageTask(item.prompt, refUrl, item.aspectRatio);
|
||||
console.log(` Task ID: ${taskId}`);
|
||||
|
||||
const imageUrl = await pollImageResult(taskId);
|
||||
console.log('\n ✓ 生成成功');
|
||||
|
||||
// 下载保存
|
||||
const imgRes = await axios.get(imageUrl, { responseType: 'arraybuffer' });
|
||||
fs.writeFileSync(path.join(OUTPUT_DIR, `${item.id}.jpg`), imgRes.data);
|
||||
console.log(' ✓ 保存本地');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n ✗ 失败: ${error.message}`);
|
||||
retryCount++;
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
}
|
||||
console.error(` ❌ ${item.id} 最终失败`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主流程
|
||||
// ============================================================
|
||||
async function main() {
|
||||
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
// 1. 上传核心参考图
|
||||
console.log('📤 上传核心参考图...');
|
||||
|
||||
const flatImgPath = path.join(MATERIAL_DIR, 'IMG_5683.png'); // 平铺图(最清晰产品细节)
|
||||
const wornImgPath = path.join(MATERIAL_DIR, 'IMG_6514.JPG'); // 佩戴图
|
||||
|
||||
const refUrls = {
|
||||
flat: await uploadToR2(flatImgPath),
|
||||
worn: await uploadToR2(wornImgPath)
|
||||
};
|
||||
|
||||
console.log(' 平铺图:', refUrls.flat);
|
||||
console.log(' 佩戴图:', refUrls.worn);
|
||||
|
||||
// 2. 定义优化后的图片任务
|
||||
const tasks = [
|
||||
// ========== A级:严格复制 ==========
|
||||
{
|
||||
id: 'Main_01_Hero',
|
||||
name: '场景首图',
|
||||
level: 'A',
|
||||
aspectRatio: '1:1',
|
||||
useWornRef: true,
|
||||
prompt: createPromptA_Strict(`
|
||||
Cute white fluffy cat wearing the product.
|
||||
Cone opens OUTWARD around face (like flower petals spreading out).
|
||||
Cat looks comfortable and relaxed.
|
||||
Warm, cozy home interior background (soft focus).
|
||||
Professional Amazon product photography, 8K quality.
|
||||
`)
|
||||
},
|
||||
{
|
||||
id: 'Main_02_Flat',
|
||||
name: '平铺白底图',
|
||||
level: 'A',
|
||||
aspectRatio: '1:1',
|
||||
useWornRef: false,
|
||||
prompt: createPromptA_Strict(`
|
||||
Product flat lay on PURE WHITE background.
|
||||
Shot from directly above (bird's eye view).
|
||||
Show the 8-petal flower/fan shape clearly.
|
||||
C-shaped opening (gap between velcro ends) visible.
|
||||
Clean studio lighting, no shadows.
|
||||
Product photography style.
|
||||
`)
|
||||
},
|
||||
|
||||
// ========== B级:可控创意 ==========
|
||||
{
|
||||
id: 'Main_03_Function',
|
||||
name: '功能演示',
|
||||
level: 'B',
|
||||
aspectRatio: '1:1',
|
||||
useWornRef: false,
|
||||
prompt: createPromptB_Controlled(`
|
||||
Close-up of hands adjusting the white velcro strap.
|
||||
Show the velcro closure mechanism clearly.
|
||||
Demonstrate "adjustable fit" feature.
|
||||
Clean, bright lighting.
|
||||
`)
|
||||
},
|
||||
{
|
||||
id: 'Main_04_Scenarios',
|
||||
name: '多场景使用',
|
||||
level: 'B',
|
||||
aspectRatio: '1:1',
|
||||
useWornRef: true,
|
||||
prompt: createPromptB_Controlled(`
|
||||
2x2 grid showing different usage scenarios:
|
||||
- Cat eating from bowl while wearing cone (cone allows eating)
|
||||
- Cat drinking water comfortably
|
||||
- Cat sleeping peacefully
|
||||
- Cat walking around home
|
||||
All showing the ICE BLUE 8-PETAL cone product.
|
||||
`)
|
||||
},
|
||||
{
|
||||
id: 'APlus_01_Banner',
|
||||
name: 'A+横幅',
|
||||
level: 'B',
|
||||
aspectRatio: '3:2',
|
||||
useWornRef: true,
|
||||
prompt: createPromptB_Controlled(`
|
||||
Wide banner shot for Amazon A+ content.
|
||||
Beautiful modern living room scene.
|
||||
White cat wearing the ICE BLUE 8-petal cone walking on soft rug.
|
||||
Left side has empty space for text overlay.
|
||||
Warm, inviting home atmosphere.
|
||||
Professional lifestyle photography.
|
||||
`)
|
||||
},
|
||||
{
|
||||
id: 'APlus_03_Detail',
|
||||
name: '材质细节',
|
||||
level: 'B',
|
||||
aspectRatio: '3:2',
|
||||
useWornRef: false,
|
||||
prompt: createPromptB_Controlled(`
|
||||
Extreme close-up macro photography.
|
||||
Focus on the ICE BLUE glossy PU material texture.
|
||||
Water droplets beading on the waterproof surface.
|
||||
Show the neat stitching between petals.
|
||||
Show the TEAL edge binding around neck hole.
|
||||
High-end product detail photography.
|
||||
`)
|
||||
},
|
||||
{
|
||||
id: 'APlus_04_Waterproof',
|
||||
name: '防水演示',
|
||||
level: 'B',
|
||||
aspectRatio: '3:2',
|
||||
useWornRef: false,
|
||||
prompt: createPromptB_Controlled(`
|
||||
Action shot demonstrating waterproof feature.
|
||||
Water being poured onto the ICE BLUE 8-petal cone.
|
||||
Water droplets rolling off the glossy PU surface.
|
||||
Dynamic splash photography style.
|
||||
Bright lighting, white background.
|
||||
`)
|
||||
},
|
||||
|
||||
// ========== C级:对比图 ==========
|
||||
{
|
||||
id: 'APlus_02_Comparison',
|
||||
name: '对比图',
|
||||
level: 'C',
|
||||
aspectRatio: '3:2',
|
||||
useWornRef: true,
|
||||
prompt: createPromptC_Comparison()
|
||||
},
|
||||
];
|
||||
|
||||
// 3. 执行生成
|
||||
console.log('\n🚀 开始V2优化版生成...\n');
|
||||
console.log('=' .repeat(50));
|
||||
|
||||
let successCount = 0;
|
||||
for (const task of tasks) {
|
||||
const success = await generateSingleImage(task, refUrls);
|
||||
if (success) successCount++;
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log(`✅ 完成! 成功 ${successCount}/${tasks.length} 张`);
|
||||
console.log(`📁 输出目录: ${OUTPUT_DIR}`);
|
||||
|
||||
// 4. 说明跳过的图
|
||||
console.log('\n⚠️ 以下图片建议后期处理,不适合纯AI生成:');
|
||||
console.log(' - Main_05_Size (尺寸图) → 用设计软件+产品抠图');
|
||||
console.log(' - Main_06_Brand (品牌图) → 用设计软件+官方logo');
|
||||
console.log(' - APlus_05_Storage (收纳图) → 需要真实折叠产品素材');
|
||||
console.log(' - APlus_06_Guide (选购指南) → 用设计软件模板');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
|
||||
482
poc-workflow-v3.js
Normal file
482
poc-workflow-v3.js
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* POC Workflow V3: 通用化工作流
|
||||
*
|
||||
* 特性:
|
||||
* 1. 素材目录作为参数,支持一键切换SKU
|
||||
* 2. Vision自动提取产品特征
|
||||
* 3. 基于真实交付成品的12张图模板
|
||||
* 4. 带文字排版的A+/主图
|
||||
*
|
||||
* 用法:
|
||||
* node poc-workflow-v3.js --material-dir="./素材/素材/已有的素材" --sku-name="Touchdog软质伊丽莎白圈"
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { generateAllPrompts } = require('./prompts/image-templates');
|
||||
|
||||
// ============================================================
|
||||
// 配置
|
||||
// ============================================================
|
||||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 命令行参数解析
|
||||
// ============================================================
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const config = {
|
||||
materialDir: path.join(__dirname, '素材/素材/已有的素材'),
|
||||
skuName: 'Touchdog 软质伊丽莎白圈',
|
||||
brandName: 'TOUCHDOG',
|
||||
productName: 'CAT SOFT CONE COLLAR',
|
||||
outputDir: null, // 自动生成
|
||||
sellingPoints: [
|
||||
'CLOUD-LIGHT COMFORT',
|
||||
'WIDER & CLEARER',
|
||||
'FOLDABLE & PORTABLE',
|
||||
'WATERPROOF & EASY CLEAN'
|
||||
],
|
||||
competitorWeaknesses: [
|
||||
'HEAVY & BULKY',
|
||||
'BLOCKS VISION & MOVEMENT',
|
||||
'HARD TO STORE'
|
||||
],
|
||||
features: [
|
||||
{ title: 'STURDY AND BREATHABLE', desc: ', DURABLE AND COMFORTABLE' },
|
||||
{ title: 'EASY TO CLEAN, STYLISH', desc: 'AND ATTRACTIVE' },
|
||||
{ title: 'REINFORCED STITCHING PROCESS', desc: 'AND DURABLE FABRIC' }
|
||||
],
|
||||
sizeChart: {
|
||||
XS: { neck: '5.6-6.8IN', depth: '3.2IN' },
|
||||
S: { neck: '7.2-8.4IN', depth: '4IN' },
|
||||
M: { neck: '8.8-10.4IN', depth: '5IN' },
|
||||
L: { neck: '10.8-12.4IN', depth: '6IN' },
|
||||
XL: { neck: '12.8-14.4IN', depth: '7IN' }
|
||||
}
|
||||
};
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--material-dir=')) {
|
||||
config.materialDir = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--sku-name=')) {
|
||||
config.skuName = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--brand-name=')) {
|
||||
config.brandName = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--product-name=')) {
|
||||
config.productName = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--output-dir=')) {
|
||||
config.outputDir = arg.split('=')[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 自动生成输出目录
|
||||
if (!config.outputDir) {
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
const safeName = config.skuName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_').slice(0, 30);
|
||||
config.outputDir = path.join(__dirname, `output_v3_${safeName}_${timestamp}`);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具函数
|
||||
// ============================================================
|
||||
|
||||
async function uploadToR2(filePath) {
|
||||
const fileName = `v3-${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',
|
||||
}));
|
||||
|
||||
return 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}`;
|
||||
}
|
||||
|
||||
// 扫描素材目录,找到关键图片
|
||||
async function scanMaterials(materialDir) {
|
||||
console.log('📁 扫描素材目录:', materialDir);
|
||||
|
||||
const files = fs.readdirSync(materialDir);
|
||||
const images = files.filter(f => /\.(jpg|jpeg|png)$/i.test(f));
|
||||
|
||||
console.log(` 找到 ${images.length} 张图片`);
|
||||
|
||||
// 识别图片类型
|
||||
let flatImage = null; // 平铺图
|
||||
let wornImage = null; // 佩戴图
|
||||
|
||||
for (const img of images) {
|
||||
const imgPath = path.join(materialDir, img);
|
||||
// 简单启发式:文件名包含特定关键词
|
||||
if (img.includes('5683') || img.toLowerCase().includes('flat')) {
|
||||
flatImage = imgPath;
|
||||
} else if (img.includes('6514') || img.toLowerCase().includes('worn') || img.toLowerCase().includes('wear')) {
|
||||
wornImage = imgPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到,用第一张和第二张
|
||||
if (!flatImage && images.length > 0) {
|
||||
flatImage = path.join(materialDir, images[0]);
|
||||
}
|
||||
if (!wornImage && images.length > 1) {
|
||||
wornImage = path.join(materialDir, images[1]);
|
||||
} else if (!wornImage) {
|
||||
wornImage = flatImage;
|
||||
}
|
||||
|
||||
console.log(' 平铺图:', flatImage ? path.basename(flatImage) : '未找到');
|
||||
console.log(' 佩戴图:', wornImage ? path.basename(wornImage) : '未找到');
|
||||
|
||||
return { flatImage, wornImage, allImages: images.map(i => path.join(materialDir, i)) };
|
||||
}
|
||||
|
||||
// Vision API提取产品特征
|
||||
const VISION_EXTRACT_PROMPT = `Analyze this pet recovery cone product image in EXTREME detail.
|
||||
|
||||
Output ONLY a valid JSON object (no markdown, no explanation):
|
||||
|
||||
{
|
||||
"color": {
|
||||
"primary": "<hex code>",
|
||||
"name": "<descriptive name>",
|
||||
"secondary": "<accent colors>"
|
||||
},
|
||||
"shape": {
|
||||
"type": "<flower/fan/cone>",
|
||||
"petal_count": <number>,
|
||||
"opening": "<C-shaped/full-circle>",
|
||||
"description": "<detailed shape>"
|
||||
},
|
||||
"material": {
|
||||
"type": "<PU/fabric/plastic>",
|
||||
"finish": "<glossy/matte/satin>",
|
||||
"texture": "<smooth/quilted>"
|
||||
},
|
||||
"edge_binding": {
|
||||
"color": "<color>",
|
||||
"material": "<ribbed/fabric>"
|
||||
},
|
||||
"closure": {
|
||||
"type": "<velcro/button>",
|
||||
"color": "<color>",
|
||||
"position": "<location>"
|
||||
},
|
||||
"logo": {
|
||||
"text": "<brand>",
|
||||
"style": "<embroidered/printed>",
|
||||
"position": "<location>"
|
||||
},
|
||||
"unique_features": ["<list>"],
|
||||
"overall_description": "<2-3 sentence summary>"
|
||||
}`;
|
||||
|
||||
async function extractProductFeatures(imageUrl) {
|
||||
console.log('\n🔍 Vision分析产品特征...');
|
||||
|
||||
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: 90000 });
|
||||
|
||||
const content = response.data.data?.choices?.[0]?.message?.content ||
|
||||
response.data.data?.content;
|
||||
|
||||
if (!content) {
|
||||
throw new Error('Vision响应为空');
|
||||
}
|
||||
|
||||
// 提取JSON
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
console.log(' ✓ 提取成功');
|
||||
return parsed;
|
||||
}
|
||||
|
||||
throw new Error('无法解析JSON');
|
||||
} catch (error) {
|
||||
console.error(' ✗ Vision分析失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 将Vision结果转换为Golden Description
|
||||
function buildGoldenDescription(visionResult) {
|
||||
if (!visionResult) {
|
||||
return `
|
||||
PRODUCT: Pet recovery cone collar
|
||||
- Soft, flexible material
|
||||
- C-shaped opening design
|
||||
- Velcro closure
|
||||
- Comfortable for pets
|
||||
`;
|
||||
}
|
||||
|
||||
const r = visionResult;
|
||||
return `
|
||||
EXACT PRODUCT APPEARANCE (AUTO-EXTRACTED):
|
||||
- Shape: ${r.shape?.petal_count || '?'}-PETAL ${r.shape?.type?.toUpperCase() || 'FLOWER'} shape, ${r.shape?.opening || 'C-shaped'} opening
|
||||
- Color: ${r.color?.name?.toUpperCase() || 'UNKNOWN'} (${r.color?.primary || '#???'})
|
||||
- Material: ${r.material?.finish || 'Soft'} ${r.material?.type || 'fabric'} with ${r.material?.texture || 'smooth'} texture
|
||||
- Edge binding: ${r.edge_binding?.color || 'Contrasting 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 end'}
|
||||
- Logo: "${r.logo?.text || 'TOUCHDOG'}" ${r.logo?.style || 'embroidered'}
|
||||
|
||||
UNIQUE FEATURES:
|
||||
${(r.unique_features || []).map(f => `- ${f}`).join('\n') || '- Soft, comfortable design'}
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- ❌ NO printed patterns or colorful fabric designs
|
||||
- ❌ NO hard plastic transparent cones
|
||||
- ❌ NO fully circular/closed shapes (must have C-opening)
|
||||
- ❌ NO random brand logos
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// 生图任务
|
||||
async function submitImageTask(prompt, refImageUrl, aspectRatio = '1:1') {
|
||||
const payload = {
|
||||
key: API_KEY,
|
||||
prompt: prompt,
|
||||
img_url: refImageUrl,
|
||||
aspectRatio: aspectRatio,
|
||||
imageSize: '1K'
|
||||
};
|
||||
|
||||
const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload);
|
||||
const taskId = response.data.data?.id;
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('No task ID returned');
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
async function pollImageResult(taskId) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 90;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
const response = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId }
|
||||
});
|
||||
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.status === 2 && data.image_url) {
|
||||
return data.image_url;
|
||||
} else if (data && data.status === 3) {
|
||||
throw new Error('Generation failed: ' + (data.fail_reason || 'Unknown'));
|
||||
}
|
||||
|
||||
process.stdout.write('.');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
attempts++;
|
||||
}
|
||||
throw new Error('Timeout');
|
||||
}
|
||||
|
||||
async function generateSingleImage(item, refUrl, outputDir) {
|
||||
console.log(`\n🎨 [${item.id}] ${item.name}`);
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 2;
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
if (retryCount > 0) console.log(` 重试 ${retryCount}/${maxRetries}...`);
|
||||
|
||||
console.log(' 提交任务...');
|
||||
const taskId = await submitImageTask(item.prompt, refUrl, item.aspectRatio);
|
||||
console.log(` Task ID: ${taskId}`);
|
||||
|
||||
const imageUrl = await pollImageResult(taskId);
|
||||
console.log('\n ✓ 生成成功');
|
||||
|
||||
// 下载保存
|
||||
const imgRes = await axios.get(imageUrl, { responseType: 'arraybuffer' });
|
||||
const outputPath = path.join(outputDir, `${item.id}.jpg`);
|
||||
fs.writeFileSync(outputPath, imgRes.data);
|
||||
console.log(` ✓ 保存: ${item.id}.jpg`);
|
||||
|
||||
return { id: item.id, success: true, path: outputPath };
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n ✗ 失败: ${error.message}`);
|
||||
retryCount++;
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
}
|
||||
|
||||
return { id: item.id, success: false, error: 'Max retries exceeded' };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主流程
|
||||
// ============================================================
|
||||
|
||||
async function main() {
|
||||
const config = parseArgs();
|
||||
|
||||
console.log('='.repeat(70));
|
||||
console.log('🚀 POC Workflow V3 - 通用化工作流');
|
||||
console.log('='.repeat(70));
|
||||
console.log('\n📋 配置:');
|
||||
console.log(' 素材目录:', config.materialDir);
|
||||
console.log(' SKU名称:', config.skuName);
|
||||
console.log(' 品牌:', config.brandName);
|
||||
console.log(' 输出目录:', config.outputDir);
|
||||
|
||||
// 创建输出目录
|
||||
if (!fs.existsSync(config.outputDir)) {
|
||||
fs.mkdirSync(config.outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 1. 扫描素材
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('📁 阶段1: 扫描素材');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const materials = await scanMaterials(config.materialDir);
|
||||
|
||||
if (!materials.flatImage) {
|
||||
console.error('❌ 未找到素材图片,请检查素材目录');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. 上传素材到R2
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('📤 阶段2: 上传素材');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const flatImageUrl = await uploadToR2(materials.flatImage);
|
||||
console.log(' 平铺图URL:', flatImageUrl);
|
||||
|
||||
let wornImageUrl = flatImageUrl;
|
||||
if (materials.wornImage && materials.wornImage !== materials.flatImage) {
|
||||
wornImageUrl = await uploadToR2(materials.wornImage);
|
||||
console.log(' 佩戴图URL:', wornImageUrl);
|
||||
}
|
||||
|
||||
// 3. Vision分析
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('🔍 阶段3: Vision分析产品特征');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const visionResult = await extractProductFeatures(flatImageUrl);
|
||||
const goldenDescription = buildGoldenDescription(visionResult);
|
||||
|
||||
console.log('\n生成的Golden Description:');
|
||||
console.log(goldenDescription.substring(0, 500) + '...');
|
||||
|
||||
// 保存Vision结果
|
||||
fs.writeFileSync(
|
||||
path.join(config.outputDir, 'vision-analysis.json'),
|
||||
JSON.stringify(visionResult, null, 2)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(config.outputDir, 'golden-description.txt'),
|
||||
goldenDescription
|
||||
);
|
||||
|
||||
// 4. 生成12张图的Prompts
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('📝 阶段4: 生成Prompts');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const product = { goldenDescription };
|
||||
const skuInfo = {
|
||||
brandName: config.brandName,
|
||||
productName: config.productName,
|
||||
sellingPoints: config.sellingPoints,
|
||||
competitorWeaknesses: config.competitorWeaknesses,
|
||||
features: config.features,
|
||||
sizeChart: config.sizeChart
|
||||
};
|
||||
|
||||
const prompts = generateAllPrompts(product, skuInfo);
|
||||
console.log(` 生成了 ${prompts.length} 个Prompt`);
|
||||
|
||||
// 保存prompts供参考
|
||||
fs.writeFileSync(
|
||||
path.join(config.outputDir, 'prompts.json'),
|
||||
JSON.stringify(prompts.map(p => ({ id: p.id, name: p.name, aspectRatio: p.aspectRatio })), null, 2)
|
||||
);
|
||||
|
||||
// 5. 批量生成图片
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('🎨 阶段5: 批量生成图片');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const results = [];
|
||||
for (const promptItem of prompts) {
|
||||
// 根据图片类型选择参考图
|
||||
const refUrl = promptItem.id.includes('Main_02') ? flatImageUrl : wornImageUrl;
|
||||
|
||||
const result = await generateSingleImage(promptItem, refUrl, config.outputDir);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// 6. 总结
|
||||
console.log('\n' + '='.repeat(70));
|
||||
console.log('📊 生成完成');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
console.log(`\n✅ 成功: ${successCount}/${results.length}`);
|
||||
|
||||
if (successCount < results.length) {
|
||||
console.log('\n❌ 失败的图片:');
|
||||
results.filter(r => !r.success).forEach(r => {
|
||||
console.log(` - ${r.id}: ${r.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n📁 输出目录: ${config.outputDir}`);
|
||||
|
||||
// 保存结果摘要
|
||||
fs.writeFileSync(
|
||||
path.join(config.outputDir, 'results.json'),
|
||||
JSON.stringify({
|
||||
config,
|
||||
visionResult,
|
||||
results,
|
||||
summary: {
|
||||
total: results.length,
|
||||
success: successCount,
|
||||
failed: results.length - successCount
|
||||
}
|
||||
}, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
|
||||
399
poc-workflow-v4.js
Normal file
399
poc-workflow-v4.js
Normal file
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* POC Workflow V4: 完整优化版
|
||||
*
|
||||
* 改进:
|
||||
* 1. Vision超时重试机制
|
||||
* 2. 可配置的套路模板系统
|
||||
* 3. 基于真实交付成品的Prompt
|
||||
* 4. 强化产品一致性约束
|
||||
*
|
||||
* 用法:
|
||||
* node poc-workflow-v4.js --material-dir="./素材/素材/已有的素材" --sku-name="Touchdog冰蓝色伊丽莎白圈"
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { extractProductFeatures, buildGoldenDescription } = require('./lib/vision-extractor');
|
||||
const { generateAllPrompts } = require('./lib/template-config');
|
||||
|
||||
// ============================================================
|
||||
// 配置
|
||||
// ============================================================
|
||||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 命令行参数解析
|
||||
// ============================================================
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const config = {
|
||||
materialDir: path.join(__dirname, '素材/素材/已有的素材'),
|
||||
skuName: 'Touchdog 软质伊丽莎白圈',
|
||||
brandName: 'TOUCHDOG',
|
||||
productName: 'CAT SOFT CONE COLLAR',
|
||||
petType: 'cat',
|
||||
outputDir: null
|
||||
};
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--material-dir=')) {
|
||||
config.materialDir = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--sku-name=')) {
|
||||
config.skuName = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--brand-name=')) {
|
||||
config.brandName = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--product-name=')) {
|
||||
config.productName = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--output-dir=')) {
|
||||
config.outputDir = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--pet-type=')) {
|
||||
config.petType = arg.split('=')[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 自动生成输出目录
|
||||
if (!config.outputDir) {
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
|
||||
const safeName = config.skuName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_').slice(0, 20);
|
||||
config.outputDir = path.join(__dirname, `output_v4_${safeName}_${timestamp}`);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具函数
|
||||
// ============================================================
|
||||
|
||||
async function uploadToR2(filePath) {
|
||||
const fileName = `v4-${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',
|
||||
}));
|
||||
|
||||
return 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}`;
|
||||
}
|
||||
|
||||
// 扫描素材目录
|
||||
async function scanMaterials(materialDir) {
|
||||
console.log('📁 扫描素材目录:', materialDir);
|
||||
|
||||
if (!fs.existsSync(materialDir)) {
|
||||
throw new Error(`素材目录不存在: ${materialDir}`);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(materialDir);
|
||||
const images = files.filter(f => /\.(jpg|jpeg|png)$/i.test(f));
|
||||
|
||||
console.log(` 找到 ${images.length} 张图片: ${images.join(', ')}`);
|
||||
|
||||
// 识别图片类型
|
||||
let flatImage = null; // 平铺图
|
||||
let wornImage = null; // 佩戴图
|
||||
|
||||
for (const img of images) {
|
||||
const imgPath = path.join(materialDir, img);
|
||||
const imgLower = img.toLowerCase();
|
||||
|
||||
// 平铺图识别
|
||||
if (img.includes('5683') || imgLower.includes('flat') || imgLower.includes('1.png')) {
|
||||
flatImage = imgPath;
|
||||
}
|
||||
// 佩戴图识别
|
||||
if (img.includes('6514') || imgLower.includes('worn') || imgLower.includes('wear') || imgLower.includes('3.png')) {
|
||||
wornImage = imgPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到,用启发式规则
|
||||
if (!flatImage) {
|
||||
// 优先选png格式(通常更清晰)
|
||||
const pngImages = images.filter(i => i.toLowerCase().endsWith('.png'));
|
||||
flatImage = pngImages.length > 0
|
||||
? path.join(materialDir, pngImages[0])
|
||||
: path.join(materialDir, images[0]);
|
||||
}
|
||||
|
||||
if (!wornImage) {
|
||||
const jpgImages = images.filter(i => i.toLowerCase().endsWith('.jpg') || i.toLowerCase().endsWith('.jpeg'));
|
||||
wornImage = jpgImages.length > 0
|
||||
? path.join(materialDir, jpgImages[0])
|
||||
: flatImage;
|
||||
}
|
||||
|
||||
console.log(' ✓ 平铺图:', path.basename(flatImage));
|
||||
console.log(' ✓ 佩戴图:', path.basename(wornImage));
|
||||
|
||||
return { flatImage, wornImage, allImages: images.map(i => path.join(materialDir, i)) };
|
||||
}
|
||||
|
||||
// 生图任务
|
||||
async function submitImageTask(prompt, refImageUrl, aspectRatio = '1:1') {
|
||||
const payload = {
|
||||
key: API_KEY,
|
||||
prompt: prompt,
|
||||
img_url: refImageUrl,
|
||||
aspectRatio: aspectRatio,
|
||||
imageSize: '1K'
|
||||
};
|
||||
|
||||
const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload, {
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const taskId = response.data.data?.id;
|
||||
if (!taskId) {
|
||||
console.error('Submit response:', response.data);
|
||||
throw new Error('No task ID returned');
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
async function pollImageResult(taskId) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 90;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId },
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.status === 2 && data.image_url) {
|
||||
return data.image_url;
|
||||
} else if (data && data.status === 3) {
|
||||
throw new Error('Generation failed: ' + (data.fail_reason || 'Unknown'));
|
||||
}
|
||||
|
||||
process.stdout.write('.');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
attempts++;
|
||||
} catch (error) {
|
||||
if (error.message.includes('Generation failed')) throw error;
|
||||
// 网络错误,继续重试
|
||||
process.stdout.write('x');
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
throw new Error('Timeout after ' + maxAttempts + ' attempts');
|
||||
}
|
||||
|
||||
async function generateSingleImage(item, refUrl, outputDir) {
|
||||
console.log(`\n🎨 [${item.id}] ${item.name}`);
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 2;
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
if (retryCount > 0) console.log(` 重试 ${retryCount}/${maxRetries}...`);
|
||||
|
||||
console.log(' 提交任务...');
|
||||
const taskId = await submitImageTask(item.prompt, refUrl, item.aspectRatio);
|
||||
console.log(` Task ID: ${taskId}`);
|
||||
|
||||
const imageUrl = await pollImageResult(taskId);
|
||||
console.log('\n ✓ 生成成功');
|
||||
|
||||
// 下载保存
|
||||
const imgRes = await axios.get(imageUrl, { responseType: 'arraybuffer', timeout: 30000 });
|
||||
const outputPath = path.join(outputDir, `${item.id}.jpg`);
|
||||
fs.writeFileSync(outputPath, imgRes.data);
|
||||
console.log(` ✓ 保存: ${item.id}.jpg`);
|
||||
|
||||
return { id: item.id, success: true, path: outputPath };
|
||||
|
||||
} catch (error) {
|
||||
console.error(`\n ✗ 失败: ${error.message}`);
|
||||
retryCount++;
|
||||
if (retryCount <= maxRetries) {
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { id: item.id, success: false, error: 'Max retries exceeded' };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主流程
|
||||
// ============================================================
|
||||
|
||||
async function main() {
|
||||
const config = parseArgs();
|
||||
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('🚀 POC Workflow V4 - 完整优化版');
|
||||
console.log('═'.repeat(70));
|
||||
console.log('\n📋 配置:');
|
||||
console.log(' 素材目录:', config.materialDir);
|
||||
console.log(' SKU名称:', config.skuName);
|
||||
console.log(' 品牌:', config.brandName);
|
||||
console.log(' 输出目录:', config.outputDir);
|
||||
|
||||
// 创建输出目录
|
||||
if (!fs.existsSync(config.outputDir)) {
|
||||
fs.mkdirSync(config.outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// ========== 阶段1: 扫描素材 ==========
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('📁 阶段1: 扫描素材');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const materials = await scanMaterials(config.materialDir);
|
||||
|
||||
if (!materials.flatImage) {
|
||||
console.error('❌ 未找到素材图片');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ========== 阶段2: 上传素材 ==========
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('📤 阶段2: 上传素材到云存储');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const flatImageUrl = await uploadToR2(materials.flatImage);
|
||||
console.log(' 平铺图:', flatImageUrl);
|
||||
|
||||
let wornImageUrl = flatImageUrl;
|
||||
if (materials.wornImage && materials.wornImage !== materials.flatImage) {
|
||||
wornImageUrl = await uploadToR2(materials.wornImage);
|
||||
console.log(' 佩戴图:', wornImageUrl);
|
||||
}
|
||||
|
||||
// ========== 阶段3: Vision分析 ==========
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('🔍 阶段3: Vision分析产品特征 (超时重试机制)');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const visionResult = await extractProductFeatures(flatImageUrl, {
|
||||
maxRetries: 3,
|
||||
timeout: 150000, // 150秒
|
||||
retryDelay: 8000,
|
||||
cacheDir: config.outputDir,
|
||||
cacheKey: path.basename(materials.flatImage).replace(/\.[^.]+$/, '')
|
||||
});
|
||||
|
||||
const goldenDescription = buildGoldenDescription(visionResult);
|
||||
|
||||
console.log('\n📝 Golden Description预览:');
|
||||
console.log('─'.repeat(50));
|
||||
console.log(goldenDescription.substring(0, 600) + '...');
|
||||
|
||||
// 保存分析结果
|
||||
fs.writeFileSync(
|
||||
path.join(config.outputDir, 'vision-analysis.json'),
|
||||
JSON.stringify(visionResult, null, 2)
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(config.outputDir, 'golden-description.txt'),
|
||||
goldenDescription
|
||||
);
|
||||
|
||||
// ========== 阶段4: 生成Prompts ==========
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('📝 阶段4: 生成12张图的Prompts (可配置模板系统)');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const product = { goldenDescription };
|
||||
const skuInfo = {
|
||||
brandName: config.brandName,
|
||||
productName: config.productName,
|
||||
petType: config.petType
|
||||
};
|
||||
|
||||
const prompts = generateAllPrompts(product, skuInfo);
|
||||
console.log(` ✓ 生成了 ${prompts.length} 个Prompt`);
|
||||
|
||||
// 保存prompts供参考
|
||||
fs.writeFileSync(
|
||||
path.join(config.outputDir, 'all-prompts.json'),
|
||||
JSON.stringify(prompts.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
aspectRatio: p.aspectRatio,
|
||||
prompt: p.prompt
|
||||
})), null, 2)
|
||||
);
|
||||
|
||||
// ========== 阶段5: 批量生成 ==========
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('🎨 阶段5: 批量生成图片');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const results = [];
|
||||
for (const promptItem of prompts) {
|
||||
// Main_02用平铺图参考,其他用佩戴图
|
||||
const refUrl = promptItem.id === 'Main_02' ? flatImageUrl : wornImageUrl;
|
||||
|
||||
const result = await generateSingleImage(promptItem, refUrl, config.outputDir);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// ========== 阶段6: 总结 ==========
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('📊 生成完成');
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
console.log(`\n✅ 成功: ${successCount}/${results.length}`);
|
||||
|
||||
if (successCount < results.length) {
|
||||
console.log('\n❌ 失败的图片:');
|
||||
results.filter(r => !r.success).forEach(r => {
|
||||
console.log(` - ${r.id}: ${r.error}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n📁 输出目录: ${config.outputDir}`);
|
||||
|
||||
// 保存结果摘要
|
||||
fs.writeFileSync(
|
||||
path.join(config.outputDir, 'results-summary.json'),
|
||||
JSON.stringify({
|
||||
config,
|
||||
visionSuccess: !!visionResult,
|
||||
results,
|
||||
summary: {
|
||||
total: results.length,
|
||||
success: successCount,
|
||||
failed: results.length - successCount
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
}, null, 2)
|
||||
);
|
||||
|
||||
console.log('\n💡 提示: 查看 all-prompts.json 了解每张图的Prompt详情');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
|
||||
687
poc-workflow-v5.js
Normal file
687
poc-workflow-v5.js
Normal file
@@ -0,0 +1,687 @@
|
||||
/**
|
||||
* POC Workflow V5: P图合成工作流
|
||||
*
|
||||
* 核心策略:
|
||||
* - 产品部分:使用原始素材(100%一致)
|
||||
* - 背景/场景:AI生成(无产品)
|
||||
* - 文字排版:AI生成或代码叠加
|
||||
* - 最终图片:代码合成各层
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const imageProcessor = require('./lib/image-processor');
|
||||
|
||||
// ============================================================
|
||||
// 配置
|
||||
// ============================================================
|
||||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||||
|
||||
// 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(buffer, filename) {
|
||||
const fileName = `v5-${Date.now()}-${filename}`;
|
||||
|
||||
await r2Client.send(new PutObjectCommand({
|
||||
Bucket: process.env.R2_BUCKET_NAME || 'ai-flow',
|
||||
Key: fileName,
|
||||
Body: buffer,
|
||||
ContentType: filename.endsWith('.png') ? 'image/png' : 'image/jpeg',
|
||||
}));
|
||||
|
||||
return 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}`;
|
||||
}
|
||||
|
||||
async function uploadFileToR2(filePath) {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return await uploadToR2(buffer, path.basename(filePath));
|
||||
}
|
||||
|
||||
// 生图任务
|
||||
async function submitImageTask(prompt, refImageUrl = null, aspectRatio = '1:1') {
|
||||
const payload = {
|
||||
key: API_KEY,
|
||||
prompt: prompt,
|
||||
aspectRatio: aspectRatio,
|
||||
imageSize: '1K'
|
||||
};
|
||||
|
||||
if (refImageUrl) {
|
||||
payload.img_url = refImageUrl;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload, { timeout: 30000 });
|
||||
const taskId = response.data.data?.id;
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('No task ID returned');
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
async function pollImageResult(taskId) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 90;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId },
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.status === 2 && data.image_url) {
|
||||
return data.image_url;
|
||||
} else if (data && data.status === 3) {
|
||||
throw new Error('Generation failed: ' + (data.fail_reason || 'Unknown'));
|
||||
}
|
||||
|
||||
process.stdout.write('.');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
attempts++;
|
||||
} catch (error) {
|
||||
if (error.message.includes('Generation failed')) throw error;
|
||||
process.stdout.write('x');
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
throw new Error('Timeout');
|
||||
}
|
||||
|
||||
async function downloadImage(url) {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 30000 });
|
||||
return Buffer.from(response.data);
|
||||
}
|
||||
|
||||
async function generateAIImage(prompt, refImageUrl = null, aspectRatio = '1:1') {
|
||||
console.log(' 提交AI任务...');
|
||||
const taskId = await submitImageTask(prompt, refImageUrl, aspectRatio);
|
||||
console.log(` Task ID: ${taskId}`);
|
||||
|
||||
const imageUrl = await pollImageResult(taskId);
|
||||
console.log('\n ✓ AI生成完成');
|
||||
|
||||
return await downloadImage(imageUrl);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 12张图的生成逻辑
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Main_01: 场景首图+卖点
|
||||
* 策略:原图叠加到AI生成的背景上,再由AI添加文字
|
||||
*/
|
||||
async function generateMain01(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_01] 场景首图+卖点');
|
||||
|
||||
// 直接用原佩戴图,让AI基于它编辑(添加文字和调整背景)
|
||||
const wornBuffer = fs.readFileSync(materials.wornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'worn.jpg');
|
||||
|
||||
const prompt = `
|
||||
Edit this image of a cat wearing a pet recovery cone:
|
||||
1. KEEP the cat and product EXACTLY as shown - DO NOT modify them
|
||||
2. Enhance the background to be a warm, cozy home interior
|
||||
3. Add soft, warm lighting effects
|
||||
4. Add text overlay:
|
||||
- A curved blue banner across the middle-left with text "DESIGNED FOR COMFORTABLE RECOVERY"
|
||||
- 3 feature boxes at the bottom:
|
||||
* "LIGHTER THAN AN EGG" with egg icon
|
||||
* "WATERPROOF & EASY WIPE" with water droplet icon
|
||||
* "BREATHABLE COTTON LINING" with cloud icon
|
||||
5. Add subtle paw print decorations
|
||||
|
||||
Style: Professional Amazon product photography with text overlay
|
||||
Output: 1:1 square image, high quality
|
||||
`.trim();
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '1:1');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'Main_01.jpg'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_02: 白底平铺+细节放大
|
||||
* 策略:使用原平铺图,代码添加标注和放大镜效果
|
||||
*/
|
||||
async function generateMain02(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_02] 白底平铺+细节放大');
|
||||
|
||||
// 读取原图
|
||||
const flatBuffer = fs.readFileSync(materials.flatImage);
|
||||
const metadata = await sharp(flatBuffer).metadata();
|
||||
|
||||
// 目标尺寸 1600x1600
|
||||
const targetSize = 1600;
|
||||
|
||||
// 调整原图大小,留出空间给标题和标注
|
||||
const productSize = Math.floor(targetSize * 0.65);
|
||||
const productBuffer = await sharp(flatBuffer)
|
||||
.resize(productSize, productSize, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 1 } })
|
||||
.toBuffer();
|
||||
|
||||
// 创建白色背景
|
||||
const background = await sharp({
|
||||
create: {
|
||||
width: targetSize,
|
||||
height: targetSize,
|
||||
channels: 3,
|
||||
background: { r: 255, g: 255, b: 255 }
|
||||
}
|
||||
}).jpeg().toBuffer();
|
||||
|
||||
// 创建标题横幅
|
||||
const bannerHeight = 80;
|
||||
const bannerSvg = `
|
||||
<svg width="${targetSize}" height="${bannerHeight}">
|
||||
<rect width="100%" height="100%" fill="#2D4A3E" rx="0" ry="0"/>
|
||||
<text x="${targetSize/2}" y="${bannerHeight/2 + 12}" font-size="32"
|
||||
fill="white" font-weight="bold" font-family="Arial, sans-serif"
|
||||
text-anchor="middle">"DURABLE WATERPROOF PU LAYER"</text>
|
||||
</svg>
|
||||
`;
|
||||
const bannerBuffer = await sharp(Buffer.from(bannerSvg)).png().toBuffer();
|
||||
|
||||
// 创建两个放大镜效果的细节图
|
||||
const detailSize = 180;
|
||||
|
||||
// 从原图裁剪细节区域(左下角材质,右下角内圈)
|
||||
const origWidth = metadata.width;
|
||||
const origHeight = metadata.height;
|
||||
|
||||
// 细节1:左下角区域
|
||||
const detail1 = await sharp(flatBuffer)
|
||||
.extract({
|
||||
left: Math.floor(origWidth * 0.1),
|
||||
top: Math.floor(origHeight * 0.6),
|
||||
width: Math.floor(origWidth * 0.25),
|
||||
height: Math.floor(origHeight * 0.25)
|
||||
})
|
||||
.resize(detailSize, detailSize, { fit: 'cover' })
|
||||
.toBuffer();
|
||||
|
||||
// 细节2:中心内圈区域
|
||||
const detail2 = await sharp(flatBuffer)
|
||||
.extract({
|
||||
left: Math.floor(origWidth * 0.35),
|
||||
top: Math.floor(origHeight * 0.35),
|
||||
width: Math.floor(origWidth * 0.3),
|
||||
height: Math.floor(origHeight * 0.3)
|
||||
})
|
||||
.resize(detailSize, detailSize, { fit: 'cover' })
|
||||
.toBuffer();
|
||||
|
||||
// 创建圆形遮罩
|
||||
const circleMask = Buffer.from(`
|
||||
<svg width="${detailSize}" height="${detailSize}">
|
||||
<circle cx="${detailSize/2}" cy="${detailSize/2}" r="${detailSize/2}" fill="white"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const detail1Circle = await sharp(detail1)
|
||||
.composite([{ input: circleMask, blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const detail2Circle = await sharp(detail2)
|
||||
.composite([{ input: circleMask, blend: 'dest-in' }])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
// 添加绿色边框
|
||||
const borderSvg = Buffer.from(`
|
||||
<svg width="${detailSize}" height="${detailSize}">
|
||||
<circle cx="${detailSize/2}" cy="${detailSize/2}" r="${detailSize/2 - 2}"
|
||||
fill="none" stroke="#2D4A3E" stroke-width="4"/>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const detail1WithBorder = await sharp(detail1Circle)
|
||||
.composite([{ input: borderSvg }])
|
||||
.toBuffer();
|
||||
|
||||
const detail2WithBorder = await sharp(detail2Circle)
|
||||
.composite([{ input: borderSvg }])
|
||||
.toBuffer();
|
||||
|
||||
// 创建标签文字
|
||||
const label1Svg = Buffer.from(`
|
||||
<svg width="350" height="40">
|
||||
<text x="0" y="28" font-size="22" fill="#2D4A3E" font-weight="bold"
|
||||
font-family="Arial, sans-serif">DURABLE WATERPROOF PU LAYER</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const label2Svg = Buffer.from(`
|
||||
<svg width="300" height="40">
|
||||
<text x="0" y="28" font-size="22" fill="#2D4A3E" font-weight="bold"
|
||||
font-family="Arial, sans-serif">DOUBLE-LAYER COMFORT</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
// 合成所有图层
|
||||
const productTop = bannerHeight + 50;
|
||||
const productLeft = (targetSize - productSize) / 2;
|
||||
|
||||
const composite = await sharp(background)
|
||||
.composite([
|
||||
{ input: bannerBuffer, top: 0, left: 0 },
|
||||
{ input: productBuffer, top: productTop, left: productLeft },
|
||||
{ input: detail1WithBorder, top: targetSize - detailSize - 120, left: 80 },
|
||||
{ input: await sharp(label1Svg).png().toBuffer(), top: targetSize - 80, left: 80 },
|
||||
{ input: detail2WithBorder, top: targetSize - detailSize - 120, left: targetSize - detailSize - 80 },
|
||||
{ input: await sharp(label2Svg).png().toBuffer(), top: targetSize - 80, left: targetSize - 300 - 80 }
|
||||
])
|
||||
.jpeg({ quality: 95 })
|
||||
.toBuffer();
|
||||
|
||||
await imageProcessor.saveImage(composite, path.join(outputDir, 'Main_02.jpg'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_03-06 和 APlus_01-06: 使用原图让AI编辑
|
||||
* 策略:传入原图,让AI保持产品不变,只做背景和文字编辑
|
||||
*/
|
||||
async function generateImageWithOriginal(id, name, materials, prompt, aspectRatio, outputDir) {
|
||||
console.log(`\n🎨 [${id}] ${name}`);
|
||||
|
||||
// 选择合适的参考图
|
||||
const refImage = id.includes('02') || id.includes('05') || id.includes('06')
|
||||
? materials.flatImage
|
||||
: materials.wornImage;
|
||||
|
||||
const refBuffer = fs.readFileSync(refImage);
|
||||
const refUrl = await uploadToR2(refBuffer, `ref-${id}.jpg`);
|
||||
|
||||
const result = await generateAIImage(prompt, refUrl, aspectRatio);
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, `${id}.jpg`));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* APlus_02: 对比图 - 特殊处理
|
||||
* 左边用原图,右边AI生成竞品
|
||||
*/
|
||||
async function generateAPlus02(materials, config, outputDir) {
|
||||
console.log('\n🎨 [APlus_02] 对比图');
|
||||
|
||||
// 用原佩戴图作为参考
|
||||
const wornBuffer = fs.readFileSync(materials.wornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'comparison-ref.jpg');
|
||||
|
||||
const prompt = `
|
||||
Create an Amazon A+ comparison image (landscape 3:2 ratio):
|
||||
|
||||
LEFT SIDE (colorful, positive):
|
||||
- Show a happy cat/dog wearing the EXACT same soft cone collar from the reference image
|
||||
- The cone collar must match the reference EXACTLY in color, shape, and material
|
||||
- Green checkmark icon
|
||||
- Label "OUR" on coral/orange background
|
||||
- 3 selling points in white text:
|
||||
• CLOUD-LIGHT COMFORT
|
||||
• WIDER & CLEARER
|
||||
• FOLDABLE & PORTABLE
|
||||
|
||||
RIGHT SIDE (grayscale, negative):
|
||||
- Show a sad cat wearing a HARD PLASTIC transparent cone (traditional lampshade style)
|
||||
- Red X icon
|
||||
- Label "OTHER" on gray background
|
||||
- Grayscale/desaturated colors
|
||||
- 3 weaknesses in gray text:
|
||||
• HEAVY & BULKY
|
||||
• BLOCKS VISION & MOVEMENT
|
||||
• HARD TO STORE
|
||||
|
||||
CRITICAL:
|
||||
- LEFT product MUST match the reference image exactly (soft fabric cone)
|
||||
- RIGHT product should be a generic hard plastic cone (NOT our product)
|
||||
- NO product image in the center
|
||||
- Background: warm beige with paw print watermarks
|
||||
`.trim();
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '3:2');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_02.jpg'));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主流程
|
||||
// ============================================================
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let materialDir = path.join(__dirname, '素材/素材/已有的素材');
|
||||
let skuName = 'Touchdog冰蓝色伊丽莎白圈';
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--material-dir=')) {
|
||||
materialDir = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--sku-name=')) {
|
||||
skuName = arg.split('=')[1];
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
|
||||
const safeName = skuName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_').slice(0, 20);
|
||||
const outputDir = path.join(__dirname, `output_v5_${safeName}_${timestamp}`);
|
||||
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('🚀 POC Workflow V5 - P图合成工作流');
|
||||
console.log('═'.repeat(70));
|
||||
console.log('\n📋 核心策略: 保持原素材产品不变,AI只编辑背景和文字');
|
||||
console.log(' 素材目录:', materialDir);
|
||||
console.log(' 输出目录:', outputDir);
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 扫描素材
|
||||
const files = fs.readdirSync(materialDir);
|
||||
const images = files.filter(f => /\.(jpg|jpeg|png)$/i.test(f));
|
||||
|
||||
// 识别平铺图和佩戴图
|
||||
let flatImage = images.find(i => i.includes('5683') || i.includes('1.png'));
|
||||
let wornImage = images.find(i => i.includes('6514') || i.includes('3.png') || i.includes('3.jpg'));
|
||||
|
||||
if (!flatImage) flatImage = images.find(i => i.endsWith('.png')) || images[0];
|
||||
if (!wornImage) wornImage = images.find(i => i.endsWith('.jpg') || i.endsWith('.JPG')) || flatImage;
|
||||
|
||||
const materials = {
|
||||
flatImage: path.join(materialDir, flatImage),
|
||||
wornImage: path.join(materialDir, wornImage)
|
||||
};
|
||||
|
||||
console.log('\n📁 素材:');
|
||||
console.log(' 平铺图:', flatImage);
|
||||
console.log(' 佩戴图:', wornImage);
|
||||
|
||||
const config = { brandName: 'TOUCHDOG', productName: 'CAT SOFT CONE COLLAR' };
|
||||
const results = [];
|
||||
|
||||
// ========== 生成12张图 ==========
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('🎨 开始生成12张图');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
try {
|
||||
// Main_01: 场景首图
|
||||
results.push({ id: 'Main_01', success: await generateMain01(materials, config, outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_01 失败:', e.message);
|
||||
results.push({ id: 'Main_01', success: false, error: e.message });
|
||||
}
|
||||
|
||||
try {
|
||||
// Main_02: 白底平铺+细节(代码合成)
|
||||
results.push({ id: 'Main_02', success: await generateMain02(materials, config, outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_02 失败:', e.message);
|
||||
results.push({ id: 'Main_02', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// Main_03: 功能调节展示
|
||||
try {
|
||||
const prompt03 = `
|
||||
Edit this image to create an Amazon product feature showcase:
|
||||
1. KEEP the pet and product EXACTLY as shown
|
||||
2. Use a background color that matches the product's main color
|
||||
3. Add title at top-left: "ADJUSTABLE STRAP FOR A SECURE FIT"
|
||||
4. Add 2 circular detail callouts on the right side:
|
||||
- One showing velcro closure detail, label: "SECURE THE ADJUSTABLE STRAP"
|
||||
- One showing the product from another angle, label: "ADJUST FOR A SNUG FIT"
|
||||
5. Add paw print decorations
|
||||
Style: Professional Amazon infographic, 1:1 square
|
||||
`.trim();
|
||||
results.push({ id: 'Main_03', success: await generateImageWithOriginal('Main_03', '功能调节展示', materials, prompt03, '1:1', outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_03 失败:', e.message);
|
||||
results.push({ id: 'Main_03', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// Main_04: 多场景使用
|
||||
try {
|
||||
const prompt04 = `
|
||||
Create a 2x2 grid Amazon product image showing 4 usage scenarios:
|
||||
Based on the reference image, show the SAME product (keep it identical) in 4 scenes:
|
||||
|
||||
TOP-LEFT: Cat standing, wearing the product
|
||||
Caption: "• HYGIENIC & EASY TO CLEAN"
|
||||
|
||||
TOP-RIGHT: Cat eating from bowl while wearing product
|
||||
Caption: "• UNRESTRICTED EATING/DRINKING"
|
||||
|
||||
BOTTOM-LEFT: Cat playing/stretching with product
|
||||
Caption: "• REVERSIBLE WEAR"
|
||||
|
||||
BOTTOM-RIGHT: Cat sleeping peacefully with product
|
||||
Caption: "• 360° COMFORT"
|
||||
|
||||
CRITICAL: The product in ALL 4 scenes must match the reference image EXACTLY
|
||||
Background: Warm beige with paw print watermarks
|
||||
Style: Rounded rectangle frames for each scene
|
||||
`.trim();
|
||||
results.push({ id: 'Main_04', success: await generateImageWithOriginal('Main_04', '多场景使用', materials, prompt04, '1:1', outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_04 失败:', e.message);
|
||||
results.push({ id: 'Main_04', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// Main_05: 尺寸图(代码生成)
|
||||
try {
|
||||
console.log('\n🎨 [Main_05] 尺寸图');
|
||||
|
||||
const flatBuffer = fs.readFileSync(materials.flatImage);
|
||||
const targetSize = 1600;
|
||||
const productSize = Math.floor(targetSize * 0.5);
|
||||
|
||||
const productBuffer = await sharp(flatBuffer)
|
||||
.resize(productSize, productSize, { fit: 'contain', background: { r: 245, g: 237, b: 228, alpha: 1 } })
|
||||
.toBuffer();
|
||||
|
||||
// 创建米色背景
|
||||
const background = await sharp({
|
||||
create: { width: targetSize, height: targetSize, channels: 3, background: { r: 245, g: 237, b: 228 } }
|
||||
}).jpeg().toBuffer();
|
||||
|
||||
// 标题
|
||||
const titleSvg = Buffer.from(`
|
||||
<svg width="${targetSize}" height="100">
|
||||
<text x="${targetSize/2}" y="70" font-size="48" fill="#333" font-weight="bold"
|
||||
font-family="Arial, sans-serif" text-anchor="middle">PRODUCT SIZE</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
// 尺寸表
|
||||
const tableSvg = Buffer.from(`
|
||||
<svg width="800" height="400">
|
||||
<rect x="0" y="0" width="800" height="50" fill="#E8D5C4" rx="5"/>
|
||||
<text x="100" y="35" font-size="20" fill="#333" font-weight="bold" font-family="Arial">SIZE</text>
|
||||
<text x="350" y="35" font-size="20" fill="#333" font-weight="bold" font-family="Arial">NECK</text>
|
||||
<text x="600" y="35" font-size="20" fill="#333" font-weight="bold" font-family="Arial">DEPTH</text>
|
||||
|
||||
<rect x="0" y="55" width="800" height="45" fill="#FFF"/>
|
||||
<text x="100" y="85" font-size="18" fill="#333" font-family="Arial">XS</text>
|
||||
<text x="350" y="85" font-size="18" fill="#333" font-family="Arial">5.6-6.8IN</text>
|
||||
<text x="600" y="85" font-size="18" fill="#333" font-family="Arial">3.2IN</text>
|
||||
|
||||
<rect x="0" y="105" width="800" height="45" fill="#F5F5F5"/>
|
||||
<text x="100" y="135" font-size="18" fill="#333" font-family="Arial">S</text>
|
||||
<text x="350" y="135" font-size="18" fill="#333" font-family="Arial">7.2-8.4IN</text>
|
||||
<text x="600" y="135" font-size="18" fill="#333" font-family="Arial">4IN</text>
|
||||
|
||||
<rect x="0" y="155" width="800" height="45" fill="#FFF"/>
|
||||
<text x="100" y="185" font-size="18" fill="#333" font-family="Arial">M</text>
|
||||
<text x="350" y="185" font-size="18" fill="#333" font-family="Arial">8.8-10.4IN</text>
|
||||
<text x="600" y="185" font-size="18" fill="#333" font-family="Arial">5IN</text>
|
||||
|
||||
<rect x="0" y="205" width="800" height="45" fill="#F5F5F5"/>
|
||||
<text x="100" y="235" font-size="18" fill="#333" font-family="Arial">L</text>
|
||||
<text x="350" y="235" font-size="18" fill="#333" font-family="Arial">10.8-12.4IN</text>
|
||||
<text x="600" y="235" font-size="18" fill="#333" font-family="Arial">6IN</text>
|
||||
|
||||
<rect x="0" y="255" width="800" height="45" fill="#FFF"/>
|
||||
<text x="100" y="285" font-size="18" fill="#333" font-family="Arial">XL</text>
|
||||
<text x="350" y="285" font-size="18" fill="#333" font-family="Arial">12.8-14.4IN</text>
|
||||
<text x="600" y="285" font-size="18" fill="#333" font-family="Arial">7IN</text>
|
||||
|
||||
<text x="400" y="340" font-size="14" fill="#E8876C" font-family="Arial" text-anchor="middle">
|
||||
NOTE: ALWAYS MEASURE YOUR PET'S NECK BEFORE SELECTING A SIZE
|
||||
</text>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
const composite = await sharp(background)
|
||||
.composite([
|
||||
{ input: await sharp(titleSvg).png().toBuffer(), top: 20, left: 0 },
|
||||
{ input: productBuffer, top: 120, left: (targetSize - productSize) / 2 },
|
||||
{ input: await sharp(tableSvg).png().toBuffer(), top: targetSize - 450, left: (targetSize - 800) / 2 }
|
||||
])
|
||||
.jpeg({ quality: 95 })
|
||||
.toBuffer();
|
||||
|
||||
await imageProcessor.saveImage(composite, path.join(outputDir, 'Main_05.jpg'));
|
||||
results.push({ id: 'Main_05', success: true });
|
||||
} catch (e) {
|
||||
console.error('Main_05 失败:', e.message);
|
||||
results.push({ id: 'Main_05', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// Main_06: 多角度展示
|
||||
try {
|
||||
const prompt06 = `
|
||||
Edit this image to show multiple angles:
|
||||
1. Split the image into two parts with a curved decorative divider
|
||||
2. LEFT: Keep the cat wearing product as shown
|
||||
3. RIGHT: Show the same cat from a different angle (side view)
|
||||
4. CRITICAL: The product must be IDENTICAL in both views
|
||||
5. Warm home interior background
|
||||
6. NO text overlay
|
||||
Style: Professional lifestyle photography, 1:1 square
|
||||
`.trim();
|
||||
results.push({ id: 'Main_06', success: await generateImageWithOriginal('Main_06', '多角度展示', materials, prompt06, '1:1', outputDir) });
|
||||
} catch (e) {
|
||||
console.error('Main_06 失败:', e.message);
|
||||
results.push({ id: 'Main_06', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// APlus_01: 品牌横幅
|
||||
try {
|
||||
const prompt_a01 = `
|
||||
Create an Amazon A+ brand banner (landscape 3:2):
|
||||
1. LEFT 40%: Brand text area
|
||||
- "TOUCHDOG" in playful coral/salmon color (#E8A87C) with paw prints
|
||||
- "CAT SOFT CONE COLLAR" below in gray
|
||||
2. RIGHT 60%: Show the cat wearing the product EXACTLY as in reference
|
||||
3. Warm cozy home background
|
||||
4. The product must match the reference EXACTLY
|
||||
`.trim();
|
||||
results.push({ id: 'APlus_01', success: await generateImageWithOriginal('APlus_01', '品牌横幅', materials, prompt_a01, '3:2', outputDir) });
|
||||
} catch (e) {
|
||||
console.error('APlus_01 失败:', e.message);
|
||||
results.push({ id: 'APlus_01', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// APlus_02: 对比图
|
||||
try {
|
||||
results.push({ id: 'APlus_02', success: await generateAPlus02(materials, config, outputDir) });
|
||||
} catch (e) {
|
||||
console.error('APlus_02 失败:', e.message);
|
||||
results.push({ id: 'APlus_02', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// APlus_03-06: 其他A+图
|
||||
const aplusPrompts = {
|
||||
'APlus_03': {
|
||||
name: '功能细节',
|
||||
prompt: `Create Amazon A+ feature details (3:2):
|
||||
Title: "ENGINEERED FOR UNCOMPROMISED COMFORT"
|
||||
Show 3 detail images with captions:
|
||||
1. Inner lining close-up: "STURDY AND BREATHABLE"
|
||||
2. Pet wearing with size marker: "EASY TO CLEAN, STYLISH"
|
||||
3. Stitching detail: "REINFORCED STITCHING"
|
||||
Product must match reference EXACTLY. Warm beige background.`
|
||||
},
|
||||
'APlus_04': {
|
||||
name: '多场景横版',
|
||||
prompt: `Create Amazon A+ horizontal 4-scene image (3:2):
|
||||
4 scenes in a row, each showing the SAME product (match reference):
|
||||
1. Cat standing: "HYGIENIC & EASY TO CLEAN"
|
||||
2. Cat eating: "UNRESTRICTED EATING"
|
||||
3. Cat playing: "REVERSIBLE WEAR"
|
||||
4. Cat sleeping: "360° COMFORT"
|
||||
Warm beige background with paw prints.`
|
||||
},
|
||||
'APlus_05': {
|
||||
name: '多角度横版',
|
||||
prompt: `Create Amazon A+ multiple angles image (3:2):
|
||||
Show 2 views side by side with curved divider:
|
||||
LEFT: Front view of cat wearing product
|
||||
RIGHT: Side view of same product
|
||||
Product must match reference EXACTLY. NO text. Warm background.`
|
||||
},
|
||||
'APlus_06': {
|
||||
name: '尺寸表横版',
|
||||
prompt: `Create Amazon A+ size chart (3:2):
|
||||
LEFT: Product flat lay with dimension arrows (NECK, WIDTH)
|
||||
RIGHT: Size chart table (XS to XL)
|
||||
Title: "PRODUCT SIZE"
|
||||
Product must match reference EXACTLY. Warm beige background.`
|
||||
}
|
||||
};
|
||||
|
||||
for (const [id, info] of Object.entries(aplusPrompts)) {
|
||||
try {
|
||||
results.push({ id, success: await generateImageWithOriginal(id, info.name, materials, info.prompt, '3:2', outputDir) });
|
||||
} catch (e) {
|
||||
console.error(`${id} 失败:`, e.message);
|
||||
results.push({ id, success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 总结 ==========
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('📊 生成完成');
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
console.log(`\n✅ 成功: ${successCount}/${results.length}`);
|
||||
|
||||
if (successCount < results.length) {
|
||||
console.log('\n❌ 失败:');
|
||||
results.filter(r => !r.success).forEach(r => console.log(` - ${r.id}: ${r.error}`));
|
||||
}
|
||||
|
||||
console.log(`\n📁 输出目录: ${outputDir}`);
|
||||
|
||||
fs.writeFileSync(path.join(outputDir, 'results.json'), JSON.stringify(results, null, 2));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
|
||||
876
poc-workflow-v6.js
Normal file
876
poc-workflow-v6.js
Normal file
@@ -0,0 +1,876 @@
|
||||
/**
|
||||
* POC Workflow V6: 优化Prompt控制策略
|
||||
*
|
||||
* 核心改进:
|
||||
* 1. 修复素材选择(使用猫咪图而非狗狗图)
|
||||
* 2. 优化Prompt - 更精准地指示AI保持主体不变
|
||||
* 3. 分类处理不同复杂度的图片
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const imageProcessor = require('./lib/image-processor');
|
||||
|
||||
// ============================================================
|
||||
// 配置
|
||||
// ============================================================
|
||||
const API_KEY = 'G9rXx3Ag2Xfa7Gs8zou6t6HqeZ';
|
||||
const API_BASE = 'https://api.wuyinkeji.com/api';
|
||||
|
||||
// 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(buffer, filename) {
|
||||
const fileName = `v6-${Date.now()}-${filename}`;
|
||||
|
||||
await r2Client.send(new PutObjectCommand({
|
||||
Bucket: process.env.R2_BUCKET_NAME || 'ai-flow',
|
||||
Key: fileName,
|
||||
Body: buffer,
|
||||
ContentType: filename.endsWith('.png') ? 'image/png' : 'image/jpeg',
|
||||
}));
|
||||
|
||||
return 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}`;
|
||||
}
|
||||
|
||||
async function submitImageTask(prompt, refImageUrl = null, aspectRatio = '1:1') {
|
||||
const payload = {
|
||||
key: API_KEY,
|
||||
prompt: prompt,
|
||||
aspectRatio: aspectRatio,
|
||||
imageSize: '1K'
|
||||
};
|
||||
|
||||
if (refImageUrl) {
|
||||
payload.img_url = refImageUrl;
|
||||
}
|
||||
|
||||
const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload, { timeout: 30000 });
|
||||
const taskId = response.data.data?.id;
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('No task ID returned');
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
async function pollImageResult(taskId) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 90;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId },
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.status === 2 && data.image_url) {
|
||||
return data.image_url;
|
||||
} else if (data && data.status === 3) {
|
||||
throw new Error('Generation failed: ' + (data.fail_reason || 'Unknown'));
|
||||
}
|
||||
|
||||
process.stdout.write('.');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
attempts++;
|
||||
} catch (error) {
|
||||
if (error.message.includes('Generation failed')) throw error;
|
||||
process.stdout.write('x');
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
throw new Error('Timeout');
|
||||
}
|
||||
|
||||
async function downloadImage(url) {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 30000 });
|
||||
return Buffer.from(response.data);
|
||||
}
|
||||
|
||||
async function generateAIImage(prompt, refImageUrl = null, aspectRatio = '1:1') {
|
||||
console.log(' 提交AI任务...');
|
||||
const taskId = await submitImageTask(prompt, refImageUrl, aspectRatio);
|
||||
console.log(` Task ID: ${taskId}`);
|
||||
|
||||
const imageUrl = await pollImageResult(taskId);
|
||||
console.log('\n ✓ AI生成完成');
|
||||
|
||||
return await downloadImage(imageUrl);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Prompt模板 - 核心优化
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 构建精准的编辑prompt
|
||||
* 核心策略:明确告诉AI这是"编辑"任务,精确描述要保留的元素
|
||||
*/
|
||||
function buildEditPrompt(config) {
|
||||
const {
|
||||
subjectDescription, // 主体描述(产品+宠物)
|
||||
preserveElements, // 必须保留的元素
|
||||
editTasks, // 编辑任务
|
||||
layoutDescription, // 排版描述
|
||||
styleGuide, // 风格指南
|
||||
aspectRatio // 比例
|
||||
} = config;
|
||||
|
||||
return `
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: ${subjectDescription}
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
${preserveElements.map((e, i) => `${i + 1}. ${e}`).join('\n')}
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
${editTasks.map((t, i) => `${i + 1}. ${t}`).join('\n')}
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
${layoutDescription}
|
||||
|
||||
STYLE GUIDE:
|
||||
${styleGuide}
|
||||
|
||||
OUTPUT: ${aspectRatio} ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 12张图的生成逻辑
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Main_01: 场景首图+卖点
|
||||
*/
|
||||
async function generateMain01(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_01] 场景首图+卖点');
|
||||
|
||||
const wornBuffer = fs.readFileSync(materials.catWornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'cat-worn.jpg');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `A Ragdoll cat (white/cream fur with light brown markings, blue eyes) wearing an ice-blue soft recovery cone collar (brand: TOUCHDOG). The cat is in a home environment.`,
|
||||
|
||||
preserveElements: [
|
||||
`The cat MUST remain a Ragdoll with blue eyes, white/cream fur, brown ear markings - EXACTLY as shown`,
|
||||
`The cone collar MUST remain ice-blue (#B5E5E8), flower/petal shaped, with "TOUCHDOG" branding`,
|
||||
`The cat's pose, expression, and the way the collar sits on the cat`,
|
||||
`The product's texture, stitching pattern, and velcro closure detail`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Enhance background to warm, cozy home interior (fireplace, blanket, wooden furniture)`,
|
||||
`Add soft warm lighting with gentle shadows`,
|
||||
`Add a curved blue banner (#4A7C9B) across middle area with text: "DESIGNED FOR COMFORTABLE RECOVERY"`,
|
||||
`Add 3 white feature boxes at bottom with icons and text:
|
||||
- Egg icon + "LIGHTER THAN AN EGG"
|
||||
- Water drop icon + "WATERPROOF & EASY WIPE"
|
||||
- Cloud icon + "BREATHABLE COTTON LINING"`,
|
||||
`Add subtle paw print watermarks in light blue on the banner area`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Top 60%: Cat wearing product in enhanced home scene
|
||||
- Middle: Curved banner with main tagline
|
||||
- Bottom 25%: Three feature callout boxes in a row`,
|
||||
|
||||
styleGuide: `Professional Amazon main image style. Warm color palette. Text should be crisp and readable. The cat and product should be the clear focal point.`,
|
||||
|
||||
aspectRatio: '1:1 square'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '1:1');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'Main_01.jpg'));
|
||||
|
||||
// 保存prompt供调试
|
||||
fs.writeFileSync(path.join(outputDir, 'Main_01_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_03: 功能调节展示 (参考效果最好的那张)
|
||||
*/
|
||||
async function generateMain03(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_03] 功能调节展示');
|
||||
|
||||
// 使用平铺图作为主参考
|
||||
const flatBuffer = fs.readFileSync(materials.flatImage);
|
||||
const flatUrl = await uploadToR2(flatBuffer, 'flat.png');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `An ice-blue soft pet recovery cone collar (TOUCHDOG brand) shown in flat lay position. The product has a flower/petal shape with 8 segments, velcro closure, and green inner rim.`,
|
||||
|
||||
preserveElements: [
|
||||
`The product MUST remain EXACTLY as shown: ice-blue color (#B5E5E8), flower shape, TOUCHDOG branding`,
|
||||
`The velcro strap detail on the left side`,
|
||||
`The green/teal inner rim around the neck hole`,
|
||||
`The 8-petal segmented design with visible stitching`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Place the product on a soft blue background (#6B9AC4) that complements the product color`,
|
||||
`Add title text at top-left: "ADJUSTABLE STRAP FOR A SECURE FIT" in white, bold`,
|
||||
`Add 2 circular detail callout images on the right side:
|
||||
- Top circle: Close-up of the velcro strap, caption "SECURE THE ADJUSTABLE STRAP"
|
||||
- Bottom circle: Product being worn by a pet showing fit, caption "ADJUST FOR A SNUG FIT"`,
|
||||
`Add decorative white paw prints scattered in the background`,
|
||||
`Add thin white circular borders around the detail callouts`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Left side (60%): Main product image, slightly angled
|
||||
- Top-left corner: Title text
|
||||
- Right side (40%): Two stacked circular detail images with captions
|
||||
- Scattered paw prints as decoration`,
|
||||
|
||||
styleGuide: `Clean infographic style. Blue color scheme matching the product. Professional Amazon A+ content quality. High contrast text for readability.`,
|
||||
|
||||
aspectRatio: '1:1 square'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, flatUrl, '1:1');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'Main_03.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'Main_03_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_04: 多场景使用 - 关键优化
|
||||
* 这是最难的图,需要在4个场景中保持同一产品和宠物
|
||||
*/
|
||||
async function generateMain04(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_04] 多场景使用 (4宫格)');
|
||||
|
||||
const wornBuffer = fs.readFileSync(materials.catWornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'cat-worn-grid.jpg');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `A Ragdoll cat (white/cream long fur, light brown markings on ears and face, distinctive blue eyes, pink nose) wearing an ice-blue TOUCHDOG soft recovery cone collar.`,
|
||||
|
||||
preserveElements: [
|
||||
`ALL 4 SCENES MUST SHOW THE EXACT SAME CAT: Ragdoll breed, blue eyes, white/cream fur, brown ear markings`,
|
||||
`ALL 4 SCENES MUST SHOW THE EXACT SAME PRODUCT: Ice-blue (#B5E5E8) flower-shaped soft cone, TOUCHDOG brand`,
|
||||
`The cat's fur texture and coloring must be consistent across all 4 images`,
|
||||
`The product's color, shape, and branding must be identical in all 4 scenes`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Create a 2x2 grid layout with 4 scenes, each in a rounded rectangle frame`,
|
||||
`TOP-LEFT scene: Cat standing alert, wearing the cone - Caption: "• HYGIENIC & EASY TO CLEAN"`,
|
||||
`TOP-RIGHT scene: Cat eating/drinking from a bowl while wearing cone - Caption: "• UNRESTRICTED EATING/DRINKING"`,
|
||||
`BOTTOM-LEFT scene: Cat stretching or playing while wearing cone - Caption: "• REVERSIBLE WEAR"`,
|
||||
`BOTTOM-RIGHT scene: Cat sleeping peacefully curled up with cone - Caption: "• 360° COMFORT"`,
|
||||
`Add light paw print watermarks in corners of the overall image`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Background: Warm beige/cream (#F5EBE0)
|
||||
- 2x2 grid of equal-sized rounded rectangles
|
||||
- Each scene has the same cat in different poses
|
||||
- Caption text below each scene in brown (#8B7355)
|
||||
- Subtle paw prints in corners`,
|
||||
|
||||
styleGuide: `
|
||||
Lifestyle photography style. Warm, inviting colors.
|
||||
The cat should look comfortable and happy in all scenes.
|
||||
Consistent lighting across all 4 scenes.
|
||||
CRITICAL: The cat must be recognizably the SAME individual cat in all scenes.`,
|
||||
|
||||
aspectRatio: '1:1 square'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '1:1');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'Main_04.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'Main_04_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_05: 尺寸图
|
||||
*/
|
||||
async function generateMain05(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_05] 尺寸图');
|
||||
|
||||
const flatBuffer = fs.readFileSync(materials.flatImage);
|
||||
const flatUrl = await uploadToR2(flatBuffer, 'flat-size.png');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `An ice-blue TOUCHDOG soft pet recovery cone collar in flat lay position, showing its flower/petal shape design.`,
|
||||
|
||||
preserveElements: [
|
||||
`The product MUST remain EXACTLY as shown: ice-blue color, flower shape, TOUCHDOG branding`,
|
||||
`The product's proportions and shape`,
|
||||
`The velcro closure and inner rim details`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Place product on warm beige background (#F5EDE4)`,
|
||||
`Add title "PRODUCT SIZE" at top center in dark text`,
|
||||
`Add a size chart table below the product with columns: SIZE | NECK | DEPTH`,
|
||||
`Table data:
|
||||
XS: 5.6-6.8IN | 3.2IN
|
||||
S: 7.2-8.4IN | 4IN
|
||||
M: 8.8-10.4IN | 5IN
|
||||
L: 10.8-12.4IN | 6IN
|
||||
XL: 12.8-14.4IN | 7IN`,
|
||||
`Add note in coral/salmon color: "NOTE: ALWAYS MEASURE YOUR PET'S NECK BEFORE SELECTING A SIZE"`,
|
||||
`Add subtle paw print decorations in beige tones`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Top: Title
|
||||
- Center: Product image (main focus)
|
||||
- Bottom: Size chart table with clean design`,
|
||||
|
||||
styleGuide: `Clean, informative infographic style. Easy to read table. Warm neutral background. Professional Amazon listing quality.`,
|
||||
|
||||
aspectRatio: '1:1 square'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, flatUrl, '1:1');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'Main_05.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'Main_05_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_02: 白底平铺+细节放大
|
||||
*/
|
||||
async function generateMain02(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_02] 白底平铺+细节放大');
|
||||
|
||||
const flatBuffer = fs.readFileSync(materials.flatImage);
|
||||
const flatUrl = await uploadToR2(flatBuffer, 'flat-detail.png');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `An ice-blue TOUCHDOG soft pet recovery cone collar in flat lay position on black background. The product has a flower/petal shape with 8 segments, velcro closure on left, and green inner rim.`,
|
||||
|
||||
preserveElements: [
|
||||
`The product MUST remain EXACTLY as shown: ice-blue color (#B5E5E8), flower shape, 8 petal segments`,
|
||||
`The TOUCHDOG branding text on the product`,
|
||||
`The velcro strap detail and green/teal inner rim`,
|
||||
`The product's proportions and stitching pattern`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Change background from black to clean white`,
|
||||
`Add a dark green (#2D4A3E) banner at top with text: "DURABLE WATERPROOF PU LAYER"`,
|
||||
`Add 2 circular magnified detail callouts at bottom:
|
||||
- Left circle: Close-up of outer material texture, label "DURABLE WATERPROOF PU LAYER"
|
||||
- Right circle: Close-up of inner lining, label "DOUBLE-LAYER COMFORT"`,
|
||||
`Add thin green borders around detail circles`,
|
||||
`Keep layout clean and professional`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Top: Dark green banner with title
|
||||
- Center: Main product image on white background
|
||||
- Bottom: Two circular detail magnifications with labels`,
|
||||
|
||||
styleGuide: `Clean white background product photography. Professional Amazon main image style. High contrast, sharp details.`,
|
||||
|
||||
aspectRatio: '1:1 square'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, flatUrl, '1:1');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'Main_02.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'Main_02_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main_06: 多角度展示
|
||||
*/
|
||||
async function generateMain06(materials, config, outputDir) {
|
||||
console.log('\n🎨 [Main_06] 多角度展示');
|
||||
|
||||
const wornBuffer = fs.readFileSync(materials.catWornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'multi-angle.jpg');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `A Ragdoll cat (white/cream fur, brown ear markings, blue eyes) wearing an ice-blue TOUCHDOG soft recovery cone collar.`,
|
||||
|
||||
preserveElements: [
|
||||
`The cat MUST remain a Ragdoll: blue eyes, white/cream fur, brown markings`,
|
||||
`The product MUST remain ice-blue (#B5E5E8), flower-shaped, TOUCHDOG branded`,
|
||||
`Both views must show the EXACT SAME cat and product`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Create a split image with curved decorative divider in the middle`,
|
||||
`LEFT SIDE: Front view of cat wearing the cone (from reference)`,
|
||||
`RIGHT SIDE: Side/profile view of the same cat wearing the same cone`,
|
||||
`Warm home interior background in both halves`,
|
||||
`NO text overlays - pure lifestyle imagery`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Split layout with elegant curved divider
|
||||
- Left: Front view
|
||||
- Right: Side view
|
||||
- Consistent warm lighting across both`,
|
||||
|
||||
styleGuide: `Lifestyle photography. Warm, cozy atmosphere. The cat should look comfortable. NO text.`,
|
||||
|
||||
aspectRatio: '1:1 square'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '1:1');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'Main_06.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'Main_06_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* APlus_01: 品牌横幅
|
||||
*/
|
||||
async function generateAPlus01(materials, config, outputDir) {
|
||||
console.log('\n🎨 [APlus_01] 品牌横幅');
|
||||
|
||||
const wornBuffer = fs.readFileSync(materials.catWornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'brand-banner.jpg');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `A Ragdoll cat wearing an ice-blue TOUCHDOG soft recovery cone collar in a home setting.`,
|
||||
|
||||
preserveElements: [
|
||||
`The cat MUST remain a Ragdoll with blue eyes and white/cream fur`,
|
||||
`The product MUST remain ice-blue, flower-shaped, TOUCHDOG branded`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Create a horizontal banner layout (3:2 ratio)`,
|
||||
`LEFT 40%: Brand text area with:
|
||||
- "TOUCHDOG" in playful coral/salmon color (#E8A87C)
|
||||
- Small paw print icons around the brand name
|
||||
- "CAT SOFT CONE COLLAR" below in gray`,
|
||||
`RIGHT 60%: The cat wearing the product in a warm home setting`,
|
||||
`Soft, warm color palette throughout`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Left side: Brand name and product title
|
||||
- Right side: Hero image of cat with product
|
||||
- Warm, cohesive color scheme`,
|
||||
|
||||
styleGuide: `Amazon A+ brand story style. Warm and inviting. Professional yet friendly.`,
|
||||
|
||||
aspectRatio: '3:2 landscape'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '3:2');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_01.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'APlus_01_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* APlus_02: 对比图
|
||||
*/
|
||||
async function generateAPlus02(materials, config, outputDir) {
|
||||
console.log('\n🎨 [APlus_02] 对比图');
|
||||
|
||||
const wornBuffer = fs.readFileSync(materials.catWornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'comparison.jpg');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `A Ragdoll cat wearing an ice-blue TOUCHDOG soft recovery cone collar.`,
|
||||
|
||||
preserveElements: [
|
||||
`LEFT SIDE: The cat and product MUST match the reference - Ragdoll cat, ice-blue soft cone`,
|
||||
`The product color (#B5E5E8), flower shape, and TOUCHDOG branding on left side`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Create a side-by-side comparison layout`,
|
||||
`LEFT SIDE (60% width, colorful):
|
||||
- Happy Ragdoll cat wearing our ice-blue soft cone (from reference)
|
||||
- Green checkmark icon
|
||||
- "OUR" label on coral/orange banner
|
||||
- 3 benefits in white: "CLOUD-LIGHT COMFORT", "WIDER & CLEARER", "FOLDABLE & PORTABLE"`,
|
||||
`RIGHT SIDE (40% width, grayscale):
|
||||
- Sad-looking cat wearing a DIFFERENT product: hard plastic transparent cone (traditional e-collar)
|
||||
- Red X icon
|
||||
- "OTHER" label on gray banner
|
||||
- 3 drawbacks in gray: "HEAVY & BULKY", "BLOCKS VISION & MOVEMENT", "HARD TO STORE"`,
|
||||
`Warm beige background with paw print watermarks`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Split layout with curved divider
|
||||
- Left side larger, full color, positive mood
|
||||
- Right side smaller, desaturated, negative mood
|
||||
- Text overlays on each side`,
|
||||
|
||||
styleGuide: `
|
||||
Clear visual contrast between "us" and "them".
|
||||
Left side warm and inviting, right side cold and uncomfortable.
|
||||
IMPORTANT: Right side must show a DIFFERENT type of cone (hard plastic), not our product.`,
|
||||
|
||||
aspectRatio: '3:2 landscape'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '3:2');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_02.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'APlus_02_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* APlus_03: 功能细节展示
|
||||
*/
|
||||
async function generateAPlus03(materials, config, outputDir) {
|
||||
console.log('\n🎨 [APlus_03] 功能细节展示');
|
||||
|
||||
const wornBuffer = fs.readFileSync(materials.catWornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'feature-detail.jpg');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `A Ragdoll cat wearing an ice-blue TOUCHDOG soft recovery cone collar.`,
|
||||
|
||||
preserveElements: [
|
||||
`The cat MUST remain a Ragdoll with blue eyes`,
|
||||
`The product MUST remain ice-blue, flower-shaped, TOUCHDOG branded`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Create horizontal layout (3:2) with title and 3 detail panels`,
|
||||
`Title at top: "ENGINEERED FOR UNCOMPROMISED COMFORT" in dark text`,
|
||||
`3 detail images in a row below:
|
||||
- Panel 1: Close-up of inner cotton lining texture, caption "STURDY AND BREATHABLE"
|
||||
- Panel 2: Cat wearing the product looking comfortable, caption "EASY TO CLEAN, STYLISH"
|
||||
- Panel 3: Close-up of stitching detail, caption "REINFORCED STITCHING"`,
|
||||
`Warm beige background (#F5EBE0) with subtle paw prints`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Top: Title banner
|
||||
- Bottom: Three equal-width panels with captions`,
|
||||
|
||||
styleGuide: `Amazon A+ feature module style. Clean, informative. Warm color palette.`,
|
||||
|
||||
aspectRatio: '3:2 landscape'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '3:2');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_03.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'APlus_03_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* APlus_04: 多场景横版
|
||||
*/
|
||||
async function generateAPlus04(materials, config, outputDir) {
|
||||
console.log('\n🎨 [APlus_04] 多场景横版');
|
||||
|
||||
const wornBuffer = fs.readFileSync(materials.catWornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'scenes-horizontal.jpg');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `A Ragdoll cat (white/cream fur, blue eyes, brown ear markings) wearing an ice-blue TOUCHDOG soft recovery cone collar.`,
|
||||
|
||||
preserveElements: [
|
||||
`ALL 4 SCENES MUST SHOW THE EXACT SAME CAT: Ragdoll breed, blue eyes, white/cream fur`,
|
||||
`ALL 4 SCENES MUST SHOW THE EXACT SAME PRODUCT: Ice-blue flower-shaped soft cone`,
|
||||
`Consistent cat appearance and product across all panels`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Create horizontal layout (3:2) with 4 scenes in a row`,
|
||||
`Scene 1: Cat standing - "HYGIENIC & EASY TO CLEAN"`,
|
||||
`Scene 2: Cat eating from bowl - "UNRESTRICTED EATING"`,
|
||||
`Scene 3: Cat playing/stretching - "REVERSIBLE WEAR"`,
|
||||
`Scene 4: Cat sleeping curled up - "360° COMFORT"`,
|
||||
`Each scene in a rounded rectangle frame`,
|
||||
`Warm beige background with paw print decorations`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- 4 equal panels arranged horizontally
|
||||
- Each panel has image + caption below
|
||||
- Consistent warm lighting across all`,
|
||||
|
||||
styleGuide: `Lifestyle photography. Same cat in all scenes. Warm, cozy feel.`,
|
||||
|
||||
aspectRatio: '3:2 landscape'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '3:2');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_04.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'APlus_04_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* APlus_05: 多角度横版
|
||||
*/
|
||||
async function generateAPlus05(materials, config, outputDir) {
|
||||
console.log('\n🎨 [APlus_05] 多角度横版');
|
||||
|
||||
const wornBuffer = fs.readFileSync(materials.catWornImage);
|
||||
const wornUrl = await uploadToR2(wornBuffer, 'angles-horizontal.jpg');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `A Ragdoll cat wearing an ice-blue TOUCHDOG soft recovery cone collar.`,
|
||||
|
||||
preserveElements: [
|
||||
`Both views must show the SAME Ragdoll cat with blue eyes`,
|
||||
`Both views must show the SAME ice-blue TOUCHDOG cone`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Create horizontal split layout (3:2)`,
|
||||
`LEFT: Front view of cat wearing cone`,
|
||||
`RIGHT: Side/profile view of same cat with cone`,
|
||||
`Elegant curved divider between the two views`,
|
||||
`Warm home background in both`,
|
||||
`NO text - pure visual showcase`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Two equal halves with curved divider
|
||||
- Left: Front angle
|
||||
- Right: Side angle
|
||||
- Warm consistent lighting`,
|
||||
|
||||
styleGuide: `High-end lifestyle photography. No text. Warm atmosphere.`,
|
||||
|
||||
aspectRatio: '3:2 landscape'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, wornUrl, '3:2');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_05.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'APlus_05_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* APlus_06: 尺寸表横版
|
||||
* 优化:明确描述产品的C形开口特征
|
||||
*/
|
||||
async function generateAPlus06(materials, config, outputDir) {
|
||||
console.log('\n🎨 [APlus_06] 尺寸表横版');
|
||||
|
||||
const flatBuffer = fs.readFileSync(materials.flatImage);
|
||||
const flatUrl = await uploadToR2(flatBuffer, 'size-chart-h.png');
|
||||
|
||||
const prompt = buildEditPrompt({
|
||||
subjectDescription: `An ice-blue TOUCHDOG soft pet recovery cone collar in flat lay position.
|
||||
CRITICAL SHAPE DETAIL: The product has a C-SHAPED OPENING (not a closed circle) - there is a GAP on the left side where the velcro strap attaches. This opening allows the collar to wrap around the pet's neck.`,
|
||||
|
||||
preserveElements: [
|
||||
`The product MUST keep its C-SHAPED OPENING - DO NOT close the gap into a full circle`,
|
||||
`The velcro strap visible on the left side of the opening`,
|
||||
`Ice-blue color (#B5E5E8), flower/petal shape with 8 segments`,
|
||||
`TOUCHDOG brand text on the product`,
|
||||
`Green/teal inner rim around the neck hole`
|
||||
],
|
||||
|
||||
editTasks: [
|
||||
`Change background from black to warm beige (#F5EDE4)`,
|
||||
`Add title "PRODUCT SIZE" at top center in dark text`,
|
||||
`Add dimension labels: "NECK" pointing to inner circle, "DEPTH" pointing outward`,
|
||||
`Add size chart table on the right:
|
||||
SIZE | NECK | DEPTH
|
||||
XS | 5.6-6.8IN | 3.2IN
|
||||
S | 7.2-8.4IN | 4IN
|
||||
M | 8.8-10.4IN | 5IN
|
||||
L | 10.8-12.4IN | 6IN
|
||||
XL | 12.8-14.4IN | 7IN`,
|
||||
`Add note in coral: "ALWAYS MEASURE YOUR PET'S NECK BEFORE SELECTING A SIZE"`,
|
||||
`Add subtle paw print watermarks`
|
||||
],
|
||||
|
||||
layoutDescription: `
|
||||
- Left 45%: Product image maintaining C-shape opening
|
||||
- Right 55%: Size chart table
|
||||
- Top: Title
|
||||
- Bottom: Note text`,
|
||||
|
||||
styleGuide: `Clean infographic style. The product's C-shaped opening must be clearly visible - this is a key feature showing how it wraps around the neck.`,
|
||||
|
||||
aspectRatio: '3:2 landscape'
|
||||
});
|
||||
|
||||
const result = await generateAIImage(prompt, flatUrl, '3:2');
|
||||
await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_06.jpg'));
|
||||
fs.writeFileSync(path.join(outputDir, 'APlus_06_prompt.txt'), prompt);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 主流程
|
||||
// ============================================================
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let materialDir = path.join(__dirname, '素材/素材/已有的素材');
|
||||
let skuName = 'Touchdog冰蓝色伊丽莎白圈';
|
||||
|
||||
// 可选:只生成指定的图
|
||||
let onlyGenerate = null; // e.g., ['Main_01', 'Main_04']
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--material-dir=')) {
|
||||
materialDir = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--sku-name=')) {
|
||||
skuName = arg.split('=')[1];
|
||||
} else if (arg.startsWith('--only=')) {
|
||||
onlyGenerate = arg.split('=')[1].split(',');
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().slice(0, 10);
|
||||
const timeStr = new Date().toTimeString().slice(0, 8).replace(/:/g, '-');
|
||||
const safeName = skuName.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_').slice(0, 20);
|
||||
const outputDir = path.join(__dirname, `output_v6_${safeName}_${timestamp}-${timeStr}`);
|
||||
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('🚀 POC Workflow V6 - 优化Prompt控制策略');
|
||||
console.log('═'.repeat(70));
|
||||
console.log('\n📋 核心策略: 精准Prompt控制,明确保留元素,限定编辑范围');
|
||||
console.log(' 素材目录:', materialDir);
|
||||
console.log(' 输出目录:', outputDir);
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 扫描素材
|
||||
const files = fs.readdirSync(materialDir);
|
||||
const images = files.filter(f => /\.(jpg|jpeg|png)$/i.test(f));
|
||||
|
||||
console.log('\n📁 可用素材:');
|
||||
images.forEach(img => console.log(` - ${img}`));
|
||||
|
||||
// ========== 修复:正确识别素材 ==========
|
||||
// 平铺图:IMG_5683.png(黑底抠图)
|
||||
const flatImage = images.find(i => i.includes('5683')) || images.find(i => i.endsWith('.png'));
|
||||
|
||||
// 猫咪佩戴图:IMG_6216 或 IMG_6229(不是6514的狗狗图!)
|
||||
const catWornImage = images.find(i => i.includes('6216')) ||
|
||||
images.find(i => i.includes('6229')) ||
|
||||
images.find(i => i.toLowerCase().includes('cat'));
|
||||
|
||||
// 狗狗佩戴图(备用)
|
||||
const dogWornImage = images.find(i => i.includes('6514'));
|
||||
|
||||
if (!flatImage) {
|
||||
console.error('❌ 找不到平铺图素材');
|
||||
return;
|
||||
}
|
||||
if (!catWornImage) {
|
||||
console.error('❌ 找不到猫咪佩戴图素材');
|
||||
console.log(' 可用文件:', images);
|
||||
return;
|
||||
}
|
||||
|
||||
const materials = {
|
||||
flatImage: path.join(materialDir, flatImage),
|
||||
catWornImage: path.join(materialDir, catWornImage),
|
||||
dogWornImage: dogWornImage ? path.join(materialDir, dogWornImage) : null
|
||||
};
|
||||
|
||||
console.log('\n✅ 素材选择:');
|
||||
console.log(' 平铺图:', flatImage);
|
||||
console.log(' 猫咪佩戴图:', catWornImage, '← 使用这个');
|
||||
if (dogWornImage) console.log(' 狗狗佩戴图:', dogWornImage, '(备用)');
|
||||
|
||||
const config = { brandName: 'TOUCHDOG', productName: 'CAT SOFT CONE COLLAR' };
|
||||
const results = [];
|
||||
|
||||
// ========== 生成图片 ==========
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
console.log('🎨 开始生成图片');
|
||||
console.log('─'.repeat(70));
|
||||
|
||||
const generators = {
|
||||
'Main_01': () => generateMain01(materials, config, outputDir),
|
||||
'Main_02': () => generateMain02(materials, config, outputDir),
|
||||
'Main_03': () => generateMain03(materials, config, outputDir),
|
||||
'Main_04': () => generateMain04(materials, config, outputDir),
|
||||
'Main_05': () => generateMain05(materials, config, outputDir),
|
||||
'Main_06': () => generateMain06(materials, config, outputDir),
|
||||
'APlus_01': () => generateAPlus01(materials, config, outputDir),
|
||||
'APlus_02': () => generateAPlus02(materials, config, outputDir),
|
||||
'APlus_03': () => generateAPlus03(materials, config, outputDir),
|
||||
'APlus_04': () => generateAPlus04(materials, config, outputDir),
|
||||
'APlus_05': () => generateAPlus05(materials, config, outputDir),
|
||||
'APlus_06': () => generateAPlus06(materials, config, outputDir),
|
||||
};
|
||||
|
||||
for (const [id, generator] of Object.entries(generators)) {
|
||||
if (onlyGenerate && !onlyGenerate.includes(id)) {
|
||||
console.log(`\n⏭️ [${id}] 跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
results.push({ id, success: await generator() });
|
||||
} catch (e) {
|
||||
console.error(`\n❌ ${id} 失败:`, e.message);
|
||||
results.push({ id, success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 总结 ==========
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('📊 生成完成');
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
console.log(`\n✅ 成功: ${successCount}/${results.length}`);
|
||||
|
||||
if (successCount < results.length) {
|
||||
console.log('\n❌ 失败:');
|
||||
results.filter(r => !r.success).forEach(r => console.log(` - ${r.id}: ${r.error}`));
|
||||
}
|
||||
|
||||
console.log(`\n📁 输出目录: ${outputDir}`);
|
||||
console.log(' 每张图都保存了对应的 _prompt.txt 供调试');
|
||||
|
||||
// 保存所有prompt到一个文件
|
||||
const allPrompts = {};
|
||||
for (const id of Object.keys(generators)) {
|
||||
const promptFile = path.join(outputDir, `${id}_prompt.txt`);
|
||||
if (fs.existsSync(promptFile)) {
|
||||
allPrompts[id] = fs.readFileSync(promptFile, 'utf-8');
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(path.join(outputDir, 'all-prompts.json'), JSON.stringify(allPrompts, null, 2));
|
||||
fs.writeFileSync(path.join(outputDir, 'results.json'), JSON.stringify(results, null, 2));
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
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);
|
||||
29
product.md
Normal file
29
product.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 伊丽莎白圈产品介绍
|
||||
## 核心卖点
|
||||
1. **极致轻盈**:仅65g(约1颗鸡蛋重量),云感舒适,不束缚宠物活动,玩耍、进食饮水无压力。
|
||||
2. **全面防水易清洁**:外层采用防水PU面料,污渍一擦即净,耐脏防潮,保持项圈干燥卫生。
|
||||
3. **舒适亲肤设计**:内层填充PP棉与棉氨纶混纺材质,搭配亲肤罗纹包边,柔软透气;微孔透气结构+加固车线工艺,久戴不闷、揉搓不变形。
|
||||
4. **实用结构优势**:加宽视野设计,不遮挡宠物视线;可折叠便携,轻松卷起收纳于宠物急救包;3秒穿脱,可调节粘扣确保稳固贴合。
|
||||
5. **高颜值多选择**:推出幻彩系列、马卡龙色系及优雅花朵设计(含蓝爱五、淡草莓、暮霭粉等颜色),兼具美观与实用性。
|
||||
6. **强效防护**:加宽围度设计(24.5cm等规格),术后能有效防止宠物舔咬伤口,适用于多种场景防护。
|
||||
|
||||
## 适用场景
|
||||
术后恢复、驱虫护理、指甲修剪、美容护理、伤口治疗、绝育术后防护、皮肤病管理等。
|
||||
|
||||
## 尺寸规格
|
||||
| 尺寸 | 颈围范围(cm/in) | 深度(cm/in) |
|
||||
|------|-------------------|---------------|
|
||||
| XS | 14-17/5.5-6.7 | 8/3.1 |
|
||||
| S | 18-21/7.1-8.3 | 13/5.1 |
|
||||
| M | 22-26/8.7-10.2 | 12.5/4.9 |
|
||||
| L | 27-31/10.6-12.2 | 15/5.9 |
|
||||
| XL | 32-36/12.6-14.2 | 17.5/6.9 |
|
||||
|
||||
## 材质工艺
|
||||
- 外层:耐用防水PU面料,防污抗皱
|
||||
- 内层:PP棉填充+95%棉+5%氨纶高密罗纹,亲肤吸湿
|
||||
- 工艺:加固车线、精密缝线,刺绣品牌标识,质感出众
|
||||
|
||||
## 品牌优势
|
||||
Touchdog创立于2004年,作为宠物美装品类开创者,秉持原创设计理念,整合全球设计资源,20年斩获100余项设计大奖(含PFAAWARDS亚宠2022年度产品设计大奖),专注人宠生活美学产品创作。
|
||||
|
||||
41
prompt.md
Normal file
41
prompt.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Role: 亚马逊高级电商视觉总监 & AI提示词工程师
|
||||
# Objective: 根据提供的产品信息和图片需求,编写适用于AI绘画工具(如NanoBanana/Midjourney/Stable Diffusion)的高质量英文提示词(Prompts)。
|
||||
|
||||
# Guidelines for Prompt Generation (视觉转化规则):
|
||||
1. **结构化写作**: 每个Prompt必须包含:[主体描述] + [环境/背景] + [光影/氛围] + [构图视角] + [画质修饰词]。
|
||||
2. **卖点视觉化**: 不要直接在Prompt里写卖点文字(如"Waterproof"),而是描述表现该卖点的画面(如"Water droplets beading off the surface, hydrophobic effect")。
|
||||
3. **一致性控制**:
|
||||
- 确保所有图片中的产品必须是参考图中的产品。
|
||||
- 主图(Main Images)通常需要纯白背景(White Background)或极简背景。
|
||||
- A+图(Lifestyle Images)需要具体的居家或户外场景,具有生活感。
|
||||
4. **尺寸适配**:
|
||||
- 主图 Prompt 末尾添加参数 `--ar 1:1`
|
||||
- A+图 Prompt 末尾添加参数 `--ar 3:2` (或接近970:600的比例)
|
||||
|
||||
# Output Format (JSON):
|
||||
请严格按照以下JSON格式返回,不要包含Markdown代码块以外的文字:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "Main_01",
|
||||
"type": "Main Image (1600x1600)",
|
||||
"purpose": "核心卖点/首图",
|
||||
"visual_description": "中文描述该图的设计思路(例如:正面特写,展示整体结构)",
|
||||
"ai_prompt": "English Prompt here, Subject, Action, Context, Lighting, Style, --ar 1:1"
|
||||
},
|
||||
{
|
||||
"id": "APlus_01",
|
||||
"type": "A+ Image (970x600)",
|
||||
"purpose": "适用场景/佩戴演示",
|
||||
"visual_description": "中文描述...",
|
||||
"ai_prompt": "English Prompt here... --ar 3:2"
|
||||
}
|
||||
// ...以此类推
|
||||
]
|
||||
```
|
||||
|
||||
# Input Data
|
||||
## 图片设计要求:
|
||||
[图片设计要求]
|
||||
## 产品介绍:
|
||||
[产品介绍]
|
||||
0
prompts/brain-system.md
Normal file
0
prompts/brain-system.md
Normal file
145
prompts/constraints/brand-vi.md
Normal file
145
prompts/constraints/brand-vi.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 品牌VI约束模板
|
||||
|
||||
## 约束级别:P1
|
||||
|
||||
此约束确保所有图片符合Touchdog品牌视觉识别规范。
|
||||
|
||||
---
|
||||
|
||||
## Logo体系
|
||||
|
||||
### 产品刺绣Logo(出现在产品上)
|
||||
- **文字**: TOUCHDOG®(大写,带®符号)
|
||||
- **位置**: 产品右侧花瓣区域(2-3点钟方向)
|
||||
- **样式**: 刺绣效果,凹陷质感
|
||||
- **颜色**: 与产品主色协调的深色调
|
||||
|
||||
### 电商图片Logo(出现在图片角落)
|
||||
- **图形**: 红色心形狗狗造型
|
||||
- **文字**: touchdog®(全小写,带®符号)
|
||||
- **标语**: wow pretty(国际版)
|
||||
- **组合**: 图形 + 文字 + 标语
|
||||
|
||||
---
|
||||
|
||||
## Logo颜色适配规则
|
||||
|
||||
```
|
||||
=== BRAND LOGO COLOR ADAPTATION ===
|
||||
|
||||
Determine background brightness and apply correct logo color:
|
||||
|
||||
IF background_brightness > 60% (light background):
|
||||
→ Use RED logo (#E60012)
|
||||
→ Red graphic icon + black "touchdog®" text
|
||||
|
||||
IF background_brightness < 50% (dark background):
|
||||
→ Use WHITE logo
|
||||
→ White graphic icon + white "touchdog®" text
|
||||
|
||||
IF background_brightness 50-60% (medium):
|
||||
→ Evaluate contrast and choose accordingly
|
||||
→ Prefer the option with better visibility
|
||||
|
||||
=== END LOGO COLOR ADAPTATION ===
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 约束模板(自动注入)
|
||||
|
||||
```
|
||||
=== BRAND VI REQUIREMENTS ===
|
||||
|
||||
EMBROIDERED LOGO ON PRODUCT:
|
||||
- Text: "TOUCHDOG®" (UPPERCASE)
|
||||
- Position: Right petal segment, 2-3 o'clock position
|
||||
- Style: Embroidered, stitched into fabric
|
||||
- Color: Slightly darker shade of product color
|
||||
|
||||
E-COMMERCE IMAGE BRAND LOGO:
|
||||
- Position: {{logo_placement.position}}
|
||||
- Type: {{logo_placement.type}}
|
||||
- Color: {{logo_placement.color}}
|
||||
|
||||
LOGO SPECIFICATIONS:
|
||||
- Clear space: Minimum 1/4 of logo height on all sides
|
||||
- Minimum size: Combined logo ≥ 46px height
|
||||
- Tagline: "wow pretty" (for international markets)
|
||||
|
||||
PROHIBITED LOGO MODIFICATIONS:
|
||||
- ❌ NO tilting or rotation
|
||||
- ❌ NO outlines or strokes
|
||||
- ❌ NO gradient fills
|
||||
- ❌ NO drop shadows
|
||||
- ❌ NO proportion changes (stretching/squishing)
|
||||
- ❌ NO underlines
|
||||
|
||||
BRAND TYPOGRAPHY:
|
||||
- Headlines: OPPOsans-Bold or similar clean sans-serif
|
||||
- Body text: OPPOsans-Medium or similar
|
||||
- Style: Clean, modern, professional
|
||||
|
||||
BRAND COLOR PALETTE:
|
||||
- Primary Red: #E60012
|
||||
- Text Black: #333333
|
||||
- Background suggestions based on product color:
|
||||
- Ice Blue product → Light blue/white background
|
||||
- Iridescent product → Warm beige/cream background
|
||||
- Solid color product → Complementary light background
|
||||
|
||||
=== END BRAND VI REQUIREMENTS ===
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logo位置规范
|
||||
|
||||
### 主图Logo位置
|
||||
| 图片类型 | 推荐位置 | 备选位置 |
|
||||
|----------|----------|----------|
|
||||
| M1 场景首图 | bottom-right | none(产品上已有Logo) |
|
||||
| M2 产品平铺 | none | bottom-right |
|
||||
| M3 功能演示 | bottom-right | none |
|
||||
| M4 场景四宫格 | bottom-right | none |
|
||||
| M5 尺寸规格 | bottom-right | none |
|
||||
| M6 品牌汇总 | center或prominent | - |
|
||||
|
||||
### A+图Logo位置
|
||||
| 图片类型 | 推荐位置 | 备选位置 |
|
||||
|----------|----------|----------|
|
||||
| A1 品牌Banner | prominent/center | top-left |
|
||||
| A2 竞品对比 | bottom-right(我方侧) | none |
|
||||
| A3 卖点三宫格 | bottom-right | none |
|
||||
| A4 功能四宫格 | bottom-right | none |
|
||||
| A5 材质工艺 | bottom-right | none |
|
||||
| A6 尺寸指南 | bottom-right | none |
|
||||
|
||||
---
|
||||
|
||||
## 违规案例
|
||||
|
||||
以下Logo使用方式是禁止的:
|
||||
|
||||
1. **倾斜Logo** - 任何角度的旋转都不允许
|
||||
2. **描边Logo** - 不能给Logo添加外轮廓
|
||||
3. **渐变Logo** - 不能使用渐变色填充
|
||||
4. **阴影Logo** - 不能添加投影效果
|
||||
5. **变形Logo** - 不能拉伸或压缩比例
|
||||
6. **下划线Logo** - 不能在文字下方添加线条
|
||||
|
||||
---
|
||||
|
||||
## 验证检查点
|
||||
|
||||
生成图片后,检查:
|
||||
|
||||
- [ ] 产品上的刺绣Logo是TOUCHDOG®(大写)
|
||||
- [ ] 电商图片Logo是touchdog®(小写)
|
||||
- [ ] Logo颜色与背景亮度匹配(浅底红Logo,深底白Logo)
|
||||
- [ ] Logo周围有足够净空间
|
||||
- [ ] Logo没有倾斜、渐变、阴影等违规处理
|
||||
- [ ] Logo尺寸不小于46px高度
|
||||
|
||||
|
||||
|
||||
0
prompts/constraints/competitor-compliance.md
Normal file
0
prompts/constraints/competitor-compliance.md
Normal file
459
prompts/image-templates.js
Normal file
459
prompts/image-templates.js
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* 基于真实交付成品的12张图Prompt模板
|
||||
* 每张图都包含文字排版要求
|
||||
*/
|
||||
|
||||
// ========================================
|
||||
// 主图6张 (1600x1600px, 1:1)
|
||||
// ========================================
|
||||
|
||||
const MAIN_TEMPLATES = {
|
||||
// Main_01: 场景首图 + 卖点
|
||||
// 参考: 幻彩主图冰蓝色 (1).jpg
|
||||
Main_01: (product, sellingPoints) => `
|
||||
[AMAZON MAIN IMAGE - LIFESTYLE WITH TEXT OVERLAY]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
SCENE:
|
||||
- Beautiful cat wearing the product
|
||||
- Warm, cozy home interior background (soft focus)
|
||||
- Product clearly visible, 60% of frame
|
||||
- Cat looks comfortable and relaxed
|
||||
|
||||
TEXT OVERLAY (MUST INCLUDE):
|
||||
- CENTER-LEFT AREA: Curved banner in muted blue (#8BB8C4)
|
||||
- TITLE on banner: "DESIGNED FOR COMFORTABLE RECOVERY" in white, clean sans-serif font
|
||||
- BOTTOM: 3 feature boxes in rounded rectangles with icons
|
||||
- Box 1: "LIGHTER THAN AN EGG" with egg icon
|
||||
- Box 2: "WATERPROOF & EASY WIPE" with water droplet icon
|
||||
- Box 3: "BREATHABLE COTTON LINING" with cloud icon
|
||||
|
||||
STYLE:
|
||||
- Professional Amazon product photography
|
||||
- Text is clean, readable, modern sans-serif
|
||||
- Subtle paw print watermarks in background
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
`,
|
||||
|
||||
// Main_02: 白底平铺图 (纯产品,无文字)
|
||||
Main_02: (product) => `
|
||||
[AMAZON MAIN IMAGE - FLAT LAY WHITE BACKGROUND]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
SCENE:
|
||||
- Product flat lay on PURE WHITE background (#FFFFFF)
|
||||
- Bird's eye view / top-down angle
|
||||
- Full C-shaped opening visible
|
||||
- All petals/segments spread out
|
||||
- Clean studio lighting, minimal shadows
|
||||
- Product fills 80% of frame
|
||||
|
||||
NO TEXT OVERLAY ON THIS IMAGE.
|
||||
|
||||
STYLE:
|
||||
- Clean Amazon product photography
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
`,
|
||||
|
||||
// Main_03: 功能特写
|
||||
Main_03: (product) => `
|
||||
[AMAZON MAIN IMAGE - FEATURE CLOSE-UP]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
SCENE:
|
||||
- Close-up shot of the product's key feature
|
||||
- Focus on velcro closure mechanism OR inner lining texture
|
||||
- Hands may be shown adjusting/demonstrating
|
||||
|
||||
STYLE:
|
||||
- Macro product photography
|
||||
- Sharp focus on detail area
|
||||
- Soft blurred background
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
`,
|
||||
|
||||
// Main_04: 多场景使用 (4宫格)
|
||||
// 参考: 伊丽莎白A+ (4).jpg
|
||||
Main_04: (product, sellingPoints) => `
|
||||
[AMAZON MAIN IMAGE - 4-GRID USAGE SCENARIOS]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT: 2x2 grid of 4 scenes, each with text caption below
|
||||
|
||||
SCENE 1 (TOP-LEFT): Cat wearing product, standing
|
||||
CAPTION: "• HYGIENIC & EASY TO CLEAN"
|
||||
SUB-TEXT: "${sellingPoints[0] || 'WATERPROOF OUTER LAYER REPELS STAINS'}"
|
||||
|
||||
SCENE 2 (TOP-RIGHT): Cat eating from bowl while wearing product
|
||||
CAPTION: "• UNRESTRICTED EATING/DRINKING"
|
||||
SUB-TEXT: "${sellingPoints[1] || 'SPECIALLY DESIGNED OPENING ALLOWS NATURAL FEEDING'}"
|
||||
|
||||
SCENE 3 (BOTTOM-LEFT): Cat stretching or walking with product
|
||||
CAPTION: "• REVERSIBLE WEAR (4-IN-1 DESIGN)"
|
||||
SUB-TEXT: "${sellingPoints[2] || 'FLIP-OVER DESIGN FLEXIBLY ADAPTS TO VARIOUS ACTIVITIES'}"
|
||||
|
||||
SCENE 4 (BOTTOM-RIGHT): Cat sleeping comfortably with product
|
||||
CAPTION: "• 360° COMFORT"
|
||||
SUB-TEXT: "${sellingPoints[3] || 'FREE PLAY, SLEEP, AND MOVEMENT WITHOUT PRESSURE'}"
|
||||
|
||||
STYLE:
|
||||
- Each scene in rounded rectangle frame
|
||||
- Warm beige background (#F5EDE4) with paw print watermarks
|
||||
- Text in dark gray, clean sans-serif
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
`,
|
||||
|
||||
// Main_05: 尺寸图
|
||||
// 参考: 伊丽莎白A+ (6).jpg
|
||||
Main_05: (product, sizeChart) => `
|
||||
[AMAZON MAIN IMAGE - SIZE GUIDE]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT:
|
||||
- TOP: Title "PRODUCT SIZE" in bold dark font
|
||||
- CENTER: Product image with dimension arrows
|
||||
- Arrow pointing to neck opening: "NECK"
|
||||
- Arrow pointing to outer width: "WIDTH"
|
||||
- BOTTOM: Size chart table
|
||||
|
||||
SIZE CHART TABLE:
|
||||
| SIZE | NECK CIRCUMFERENCE | DEPTH |
|
||||
|------|-------------------|-------|
|
||||
| XS | ${sizeChart?.XS?.neck || '5.6-6.8IN'} | ${sizeChart?.XS?.depth || '3.2IN'} |
|
||||
| S | ${sizeChart?.S?.neck || '7.2-8.4IN'} | ${sizeChart?.S?.depth || '4IN'} |
|
||||
| M | ${sizeChart?.M?.neck || '8.8-10.4IN'} | ${sizeChart?.M?.depth || '5IN'} |
|
||||
| L | ${sizeChart?.L?.neck || '10.8-12.4IN'} | ${sizeChart?.L?.depth || '6IN'} |
|
||||
| XL | ${sizeChart?.XL?.neck || '12.8-14.4IN'} | ${sizeChart?.XL?.depth || '7IN'} |
|
||||
|
||||
FOOTER TEXT: "NOTE: MEASUREMENTS ARE HAND-CHECKED. ALWAYS MEASURE YOUR PET'S NECK CIRCUMFERENCE BEFORE SELECTING A SIZE."
|
||||
|
||||
STYLE:
|
||||
- Clean infographic style
|
||||
- Warm beige background with paw prints
|
||||
- Table with rounded corners
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
`,
|
||||
|
||||
// Main_06: 多角度展示
|
||||
// 参考: 伊丽莎白A+ (5).jpg
|
||||
Main_06: (product) => `
|
||||
[AMAZON MAIN IMAGE - MULTIPLE ANGLES]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT:
|
||||
- 2 cats wearing the same product, different angles
|
||||
- Left: Front view, cat facing camera
|
||||
- Right: 3/4 side view, cat looking to the side
|
||||
- Decorative curved line connecting the two images
|
||||
|
||||
SCENE:
|
||||
- Warm home interior background
|
||||
- Both cats look comfortable
|
||||
- Product clearly visible on both
|
||||
|
||||
STYLE:
|
||||
- Lifestyle photography
|
||||
- Soft warm lighting
|
||||
- No text overlay
|
||||
- 8K quality, 1:1 aspect ratio
|
||||
`
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// A+图6张 (970x600px, 约1.6:1)
|
||||
// ========================================
|
||||
|
||||
const APLUS_TEMPLATES = {
|
||||
// APlus_01: 品牌横幅
|
||||
// 参考: 伊丽莎白A+ (1).jpg
|
||||
APlus_01: (product, brandName, productName) => `
|
||||
[AMAZON A+ BANNER - BRAND HEADER]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT:
|
||||
- Wide banner, 970x600px aspect ratio
|
||||
- LEFT 40%: Empty space with decorative paw prints for text overlay
|
||||
- RIGHT 60%: Cat wearing product in lifestyle scene
|
||||
|
||||
TEXT OVERLAY (LEFT SIDE):
|
||||
- BRAND NAME: "${brandName || 'TOUCHDOG'}" in playful, slightly curved font (coral/salmon color #E8A87C)
|
||||
- Decorative paw prints around brand name
|
||||
- PRODUCT NAME: "${productName || 'CAT SOFT CONE COLLAR'}" below in smaller gray text
|
||||
|
||||
SCENE (RIGHT SIDE):
|
||||
- White cat standing on modern furniture
|
||||
- Wearing the product, looking comfortable
|
||||
- Warm cozy interior background
|
||||
|
||||
STYLE:
|
||||
- Professional Amazon A+ content
|
||||
- Warm, inviting color palette
|
||||
- 8K quality
|
||||
`,
|
||||
|
||||
// APlus_02: 对比图
|
||||
// 参考: 伊丽莎白A+ (2).jpg
|
||||
APlus_02: (product, sellingPoints, competitorWeaknesses) => `
|
||||
[AMAZON A+ COMPARISON IMAGE]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT: Split screen, left vs right
|
||||
|
||||
LEFT SIDE (OUR PRODUCT - COLORFUL):
|
||||
- GREEN CHECKMARK icon at top-left
|
||||
- "OUR" label in white on coral/orange background (#E8876C)
|
||||
- Cat wearing our product, looking HAPPY
|
||||
- 3 SELLING POINTS in white text:
|
||||
• "${sellingPoints[0] || 'CLOUD-LIGHT COMFORT'}"
|
||||
• "${sellingPoints[1] || 'WIDER & CLEARER'}"
|
||||
• "${sellingPoints[2] || 'FOLDABLE & PORTABLE'}"
|
||||
|
||||
RIGHT SIDE (COMPETITOR - GRAYSCALE):
|
||||
- RED X-MARK icon at top-right
|
||||
- "OTHER" label in dark text on gray background
|
||||
- Cat wearing hard plastic cone, looking SAD/UNCOMFORTABLE
|
||||
- GRAYSCALE/DESATURATED colors
|
||||
- 3 WEAKNESSES in gray text:
|
||||
• "${competitorWeaknesses[0] || 'HEAVY & BULKY'}"
|
||||
• "${competitorWeaknesses[1] || 'BLOCKS VISION & MOVEMENT'}"
|
||||
• "${competitorWeaknesses[2] || 'HARD TO STORE'}"
|
||||
|
||||
CENTER: Product image showing our soft cone
|
||||
|
||||
STYLE:
|
||||
- Clear visual contrast between sides
|
||||
- Warm beige background with paw prints
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`,
|
||||
|
||||
// APlus_03: 功能细节
|
||||
// 参考: 伊丽莎白A+ (3).jpg
|
||||
APlus_03: (product, features) => `
|
||||
[AMAZON A+ FEATURE DETAILS]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT:
|
||||
- TOP: Large title "ENGINEERED FOR UNCOMPROMISED COMFORT" in bold dark font
|
||||
- MIDDLE: 3 detail images in rounded rectangles
|
||||
|
||||
DETAIL 1 (LEFT):
|
||||
- Close-up of inner lining/fabric
|
||||
- CAPTION: "${features[0]?.title || 'STURDY AND BREATHABLE'}"
|
||||
- SUB-TEXT: "${features[0]?.desc || ', DURABLE AND COMFORTABLE'}"
|
||||
|
||||
DETAIL 2 (CENTER):
|
||||
- Dog/cat wearing product with size annotation line
|
||||
- Show measurement like "24.5cm"
|
||||
- CAPTION: "${features[1]?.title || 'EASY TO CLEAN, STYLISH'}"
|
||||
- SUB-TEXT: "${features[1]?.desc || 'AND ATTRACTIVE'}"
|
||||
|
||||
DETAIL 3 (RIGHT):
|
||||
- Close-up of stitching/material
|
||||
- CAPTION: "${features[2]?.title || 'REINFORCED STITCHING PROCESS'}"
|
||||
- SUB-TEXT: "${features[2]?.desc || 'AND DURABLE FABRIC'}"
|
||||
|
||||
STYLE:
|
||||
- Warm beige background (#F5EDE4) with paw print watermarks
|
||||
- Text in dark gray/brown
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`,
|
||||
|
||||
// APlus_04: 多场景 (与Main_04类似但横版)
|
||||
APlus_04: (product, sellingPoints) => `
|
||||
[AMAZON A+ MULTI-SCENARIO - HORIZONTAL]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT: 4 scenes in horizontal row
|
||||
|
||||
SCENE 1: Cat standing wearing product
|
||||
CAPTION: "• HYGIENIC & EASY TO CLEAN"
|
||||
|
||||
SCENE 2: Cat eating from bowl
|
||||
CAPTION: "• UNRESTRICTED EATING/DRINKING"
|
||||
|
||||
SCENE 3: Cat stretching/playing
|
||||
CAPTION: "• REVERSIBLE WEAR"
|
||||
|
||||
SCENE 4: Cat sleeping peacefully
|
||||
CAPTION: "• 360° COMFORT"
|
||||
|
||||
Each scene in rounded rectangle frame with caption below.
|
||||
|
||||
STYLE:
|
||||
- Warm beige background with paw prints
|
||||
- Clean modern typography
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`,
|
||||
|
||||
// APlus_05: 多角度展示
|
||||
APlus_05: (product) => `
|
||||
[AMAZON A+ MULTIPLE ANGLES - HORIZONTAL]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT:
|
||||
- 2 cats wearing product, side by side
|
||||
- Left cat: Front view
|
||||
- Right cat: Side/3/4 view
|
||||
- Decorative curved divider between them
|
||||
|
||||
SCENE:
|
||||
- Warm interior background
|
||||
- Both cats comfortable
|
||||
- Product clearly visible
|
||||
|
||||
STYLE:
|
||||
- No text overlay
|
||||
- Lifestyle photography
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`,
|
||||
|
||||
// APlus_06: 尺寸表
|
||||
APlus_06: (product, sizeChart) => `
|
||||
[AMAZON A+ SIZE CHART - HORIZONTAL]
|
||||
|
||||
PRODUCT DESCRIPTION:
|
||||
${product.goldenDescription}
|
||||
|
||||
LAYOUT:
|
||||
- LEFT 40%: Product image with dimension arrows (NECK, WIDTH labels)
|
||||
- RIGHT 60%: Size chart table
|
||||
|
||||
TITLE: "PRODUCT SIZE" at top
|
||||
|
||||
SIZE CHART TABLE:
|
||||
| SIZE | NECK CIRCUMFERENCE | DEPTH |
|
||||
|------|-------------------|-------|
|
||||
| XS | ${sizeChart?.XS?.neck || '5.6-6.8IN'} | ${sizeChart?.XS?.depth || '3.2IN'} |
|
||||
| S | ${sizeChart?.S?.neck || '7.2-8.4IN'} | ${sizeChart?.S?.depth || '4IN'} |
|
||||
| M | ${sizeChart?.M?.neck || '8.8-10.4IN'} | ${sizeChart?.M?.depth || '5IN'} |
|
||||
| L | ${sizeChart?.L?.neck || '10.8-12.4IN'} | ${sizeChart?.L?.depth || '6IN'} |
|
||||
| XL | ${sizeChart?.XL?.neck || '12.8-14.4IN'} | ${sizeChart?.XL?.depth || '7IN'} |
|
||||
|
||||
FOOTER: "NOTE: ALWAYS MEASURE YOUR PET'S NECK BEFORE SELECTING A SIZE"
|
||||
|
||||
STYLE:
|
||||
- Warm beige background with paw prints
|
||||
- Clean infographic style
|
||||
- 8K quality, ~1.6:1 aspect ratio
|
||||
`
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// 导出
|
||||
// ========================================
|
||||
|
||||
module.exports = {
|
||||
MAIN_TEMPLATES,
|
||||
APLUS_TEMPLATES,
|
||||
|
||||
// 生成所有12张图的Prompts
|
||||
generateAllPrompts: (product, skuInfo) => {
|
||||
const prompts = [];
|
||||
|
||||
// 主图6张
|
||||
prompts.push({
|
||||
id: 'Main_01',
|
||||
name: '场景首图+卖点',
|
||||
aspectRatio: '1:1',
|
||||
prompt: MAIN_TEMPLATES.Main_01(product, skuInfo.sellingPoints || [])
|
||||
});
|
||||
prompts.push({
|
||||
id: 'Main_02',
|
||||
name: '白底平铺图',
|
||||
aspectRatio: '1:1',
|
||||
prompt: MAIN_TEMPLATES.Main_02(product)
|
||||
});
|
||||
prompts.push({
|
||||
id: 'Main_03',
|
||||
name: '功能特写',
|
||||
aspectRatio: '1:1',
|
||||
prompt: MAIN_TEMPLATES.Main_03(product)
|
||||
});
|
||||
prompts.push({
|
||||
id: 'Main_04',
|
||||
name: '多场景使用',
|
||||
aspectRatio: '1:1',
|
||||
prompt: MAIN_TEMPLATES.Main_04(product, skuInfo.sellingPoints || [])
|
||||
});
|
||||
prompts.push({
|
||||
id: 'Main_05',
|
||||
name: '尺寸图',
|
||||
aspectRatio: '1:1',
|
||||
prompt: MAIN_TEMPLATES.Main_05(product, skuInfo.sizeChart || {})
|
||||
});
|
||||
prompts.push({
|
||||
id: 'Main_06',
|
||||
name: '多角度展示',
|
||||
aspectRatio: '1:1',
|
||||
prompt: MAIN_TEMPLATES.Main_06(product)
|
||||
});
|
||||
|
||||
// A+图6张
|
||||
prompts.push({
|
||||
id: 'APlus_01',
|
||||
name: '品牌横幅',
|
||||
aspectRatio: '3:2',
|
||||
prompt: APLUS_TEMPLATES.APlus_01(product, skuInfo.brandName, skuInfo.productName)
|
||||
});
|
||||
prompts.push({
|
||||
id: 'APlus_02',
|
||||
name: '对比图',
|
||||
aspectRatio: '3:2',
|
||||
prompt: APLUS_TEMPLATES.APlus_02(
|
||||
product,
|
||||
skuInfo.sellingPoints || [],
|
||||
skuInfo.competitorWeaknesses || []
|
||||
)
|
||||
});
|
||||
prompts.push({
|
||||
id: 'APlus_03',
|
||||
name: '功能细节',
|
||||
aspectRatio: '3:2',
|
||||
prompt: APLUS_TEMPLATES.APlus_03(product, skuInfo.features || [])
|
||||
});
|
||||
prompts.push({
|
||||
id: 'APlus_04',
|
||||
name: '多场景横版',
|
||||
aspectRatio: '3:2',
|
||||
prompt: APLUS_TEMPLATES.APlus_04(product, skuInfo.sellingPoints || [])
|
||||
});
|
||||
prompts.push({
|
||||
id: 'APlus_05',
|
||||
name: '多角度横版',
|
||||
aspectRatio: '3:2',
|
||||
prompt: APLUS_TEMPLATES.APlus_05(product)
|
||||
});
|
||||
prompts.push({
|
||||
id: 'APlus_06',
|
||||
name: '尺寸表横版',
|
||||
aspectRatio: '3:2',
|
||||
prompt: APLUS_TEMPLATES.APlus_06(product, skuInfo.sizeChart || {})
|
||||
});
|
||||
|
||||
return prompts;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
486
prompts/v6-optimized.md
Normal file
486
prompts/v6-optimized.md
Normal file
@@ -0,0 +1,486 @@
|
||||
# V6 Optimized Prompts for Amazon Listing
|
||||
|
||||
## Strategy Overview
|
||||
- **Strategy**: Image Editing (NOT Generation)
|
||||
- **Core Principle**: Preserve specific elements (product/pet) while modifying background/layout
|
||||
- **Prompt Structure**:
|
||||
1. Reference Analysis
|
||||
2. Critical Preservation Requirements
|
||||
3. Editing Tasks
|
||||
4. Layout Specification
|
||||
5. Style Guide
|
||||
6. Final Check
|
||||
|
||||
## Main Images (1:1 Square)
|
||||
|
||||
### Main_01: Hero Scene
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: A Ragdoll cat (white/cream fur with light brown markings, blue eyes) wearing an ice-blue soft recovery cone collar (brand: TOUCHDOG). The cat is in a home environment.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. The cat MUST remain a Ragdoll with blue eyes, white/cream fur, brown ear markings - EXACTLY as shown
|
||||
2. The cone collar MUST remain ice-blue (#B5E5E8), flower/petal shaped, with "TOUCHDOG" branding
|
||||
3. The cat's pose, expression, and the way the collar sits on the cat
|
||||
4. The product's texture, stitching pattern, and velcro closure detail
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Enhance background to warm, cozy home interior (fireplace, blanket, wooden furniture)
|
||||
2. Add soft warm lighting with gentle shadows
|
||||
3. Add a curved blue banner (#4A7C9B) across middle area with text: "DESIGNED FOR COMFORTABLE RECOVERY"
|
||||
4. Add 3 white feature boxes at bottom with icons and text:
|
||||
- Egg icon + "LIGHTER THAN AN EGG"
|
||||
- Water drop icon + "WATERPROOF & EASY WIPE"
|
||||
- Cloud icon + "BREATHABLE COTTON LINING"
|
||||
5. Add subtle paw print watermarks in light blue on the banner area
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Top 60%: Cat wearing product in enhanced home scene
|
||||
- Middle: Curved banner with main tagline
|
||||
- Bottom 25%: Three feature callout boxes in a row
|
||||
|
||||
STYLE GUIDE:
|
||||
Professional Amazon main image style. Warm color palette. Text should be crisp and readable. The cat and product should be the clear focal point.
|
||||
|
||||
OUTPUT: 1:1 square ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### Main_02: Flat Lay Detail
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: An ice-blue TOUCHDOG soft pet recovery cone collar in flat lay position on black background. The product has a flower/petal shape with 8 segments, velcro closure on left, and green inner rim.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. The product MUST remain EXACTLY as shown: ice-blue color (#B5E5E8), flower shape, 8 petal segments
|
||||
2. The TOUCHDOG branding text on the product
|
||||
3. The velcro strap detail and green/teal inner rim
|
||||
4. The product's proportions and stitching pattern
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Change background from black to clean white
|
||||
2. Add a dark green (#2D4A3E) banner at top with text: "DURABLE WATERPROOF PU LAYER"
|
||||
3. Add 2 circular magnified detail callouts at bottom:
|
||||
- Left circle: Close-up of outer material texture, label "DURABLE WATERPROOF PU LAYER"
|
||||
- Right circle: Close-up of inner lining, label "DOUBLE-LAYER COMFORT"
|
||||
4. Add thin green borders around detail circles
|
||||
5. Keep layout clean and professional
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Top: Dark green banner with title
|
||||
- Center: Main product image on white background
|
||||
- Bottom: Two circular detail magnifications with labels
|
||||
|
||||
STYLE GUIDE:
|
||||
Clean white background product photography. Professional Amazon main image style. High contrast, sharp details.
|
||||
|
||||
OUTPUT: 1:1 square ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### Main_03: Feature Showcase
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: An ice-blue soft pet recovery cone collar (TOUCHDOG brand) shown in flat lay position. The product has a flower/petal shape with 8 segments, velcro closure, and green inner rim.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. The product MUST remain EXACTLY as shown: ice-blue color (#B5E5E8), flower shape, TOUCHDOG branding
|
||||
2. The velcro strap detail on the left side
|
||||
3. The green/teal inner rim around the neck hole
|
||||
4. The 8-petal segmented design with visible stitching
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Place the product on a soft blue background (#6B9AC4) that complements the product color
|
||||
2. Add title text at top-left: "ADJUSTABLE STRAP FOR A SECURE FIT" in white, bold
|
||||
3. Add 2 circular detail callout images on the right side:
|
||||
- Top circle: Close-up of the velcro strap, caption "SECURE THE ADJUSTABLE STRAP"
|
||||
- Bottom circle: Product being worn by a pet showing fit, caption "ADJUST FOR A SNUG FIT"
|
||||
4. Add decorative white paw prints scattered in the background
|
||||
5. Add thin white circular borders around the detail callouts
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Left side (60%): Main product image, slightly angled
|
||||
- Top-left corner: Title text
|
||||
- Right side (40%): Two stacked circular detail images with captions
|
||||
- Scattered paw prints as decoration
|
||||
|
||||
STYLE GUIDE:
|
||||
Clean infographic style. Blue color scheme matching the product. Professional Amazon A+ content quality. High contrast text for readability.
|
||||
|
||||
OUTPUT: 1:1 square ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### Main_04: 4-Scenario Grid
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: A Ragdoll cat (white/cream long fur, light brown markings on ears and face, distinctive blue eyes, pink nose) wearing an ice-blue TOUCHDOG soft recovery cone collar.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. ALL 4 SCENES MUST SHOW THE EXACT SAME CAT: Ragdoll breed, blue eyes, white/cream fur, brown ear markings
|
||||
2. ALL 4 SCENES MUST SHOW THE EXACT SAME PRODUCT: Ice-blue (#B5E5E8) flower-shaped soft cone, TOUCHDOG brand
|
||||
3. The cat's fur texture and coloring must be consistent across all 4 images
|
||||
4. The product's color, shape, and branding must be identical in all 4 scenes
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Create a 2x2 grid layout with 4 scenes, each in a rounded rectangle frame
|
||||
2. TOP-LEFT scene: Cat standing alert, wearing the cone - Caption: "• HYGIENIC & EASY TO CLEAN"
|
||||
3. TOP-RIGHT scene: Cat eating/drinking from a bowl while wearing cone - Caption: "• UNRESTRICTED EATING/DRINKING"
|
||||
4. BOTTOM-LEFT scene: Cat stretching or playing while wearing cone - Caption: "• REVERSIBLE WEAR"
|
||||
5. BOTTOM-RIGHT scene: Cat sleeping peacefully curled up with cone - Caption: "• 360° COMFORT"
|
||||
6. Add light paw print watermarks in corners of the overall image
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Background: Warm beige/cream (#F5EBE0)
|
||||
- 2x2 grid of equal-sized rounded rectangles
|
||||
- Each scene has the same cat in different poses
|
||||
- Caption text below each scene in brown (#8B7355)
|
||||
- Subtle paw prints in corners
|
||||
|
||||
STYLE GUIDE:
|
||||
Lifestyle photography style. Warm, inviting colors.
|
||||
The cat should look comfortable and happy in all scenes.
|
||||
Consistent lighting across all 4 scenes.
|
||||
CRITICAL: The cat must be recognizably the SAME individual cat in all scenes.
|
||||
|
||||
OUTPUT: 1:1 square ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### Main_05: Size Chart
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: An ice-blue TOUCHDOG soft pet recovery cone collar in flat lay position, showing its flower/petal shape design.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. The product MUST remain EXACTLY as shown: ice-blue color, flower shape, TOUCHDOG branding
|
||||
2. The product's proportions and shape
|
||||
3. The velcro closure and inner rim details
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Place product on warm beige background (#F5EDE4)
|
||||
2. Add title "PRODUCT SIZE" at top center in dark text
|
||||
3. Add a size chart table below the product with columns: SIZE | NECK | DEPTH
|
||||
4. Table data:
|
||||
XS: 5.6-6.8IN | 3.2IN
|
||||
S: 7.2-8.4IN | 4IN
|
||||
M: 8.8-10.4IN | 5IN
|
||||
L: 10.8-12.4IN | 6IN
|
||||
XL: 12.8-14.4IN | 7IN
|
||||
5. Add note in coral/salmon color: "NOTE: ALWAYS MEASURE YOUR PET'S NECK BEFORE SELECTING A SIZE"
|
||||
6. Add subtle paw print decorations in beige tones
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Top: Title
|
||||
- Center: Product image (main focus)
|
||||
- Bottom: Size chart table with clean design
|
||||
|
||||
STYLE GUIDE:
|
||||
Clean, informative infographic style. Easy to read table. Warm neutral background. Professional Amazon listing quality.
|
||||
|
||||
OUTPUT: 1:1 square ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### Main_06: Multi-Angle
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: A Ragdoll cat (white/cream fur, brown ear markings, blue eyes) wearing an ice-blue TOUCHDOG soft recovery cone collar.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. The cat MUST remain a Ragdoll: blue eyes, white/cream fur, brown markings
|
||||
2. The product MUST remain ice-blue (#B5E5E8), flower-shaped, TOUCHDOG branded
|
||||
3. Both views must show the EXACT SAME cat and product
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Create a split image with curved decorative divider in the middle
|
||||
2. LEFT SIDE: Front view of cat wearing the cone (from reference)
|
||||
3. RIGHT SIDE: Side/profile view of the same cat wearing the same cone
|
||||
4. Warm home interior background in both halves
|
||||
5. NO text overlays - pure lifestyle imagery
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Split layout with elegant curved divider
|
||||
- Left: Front view
|
||||
- Right: Side view
|
||||
- Consistent warm lighting across both
|
||||
|
||||
STYLE GUIDE:
|
||||
Lifestyle photography. Warm, cozy atmosphere. The cat should look comfortable. NO text.
|
||||
|
||||
OUTPUT: 1:1 square ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
## A+ Images (3:2 Landscape)
|
||||
|
||||
### APlus_01: Brand Banner
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: A Ragdoll cat wearing an ice-blue TOUCHDOG soft recovery cone collar in a home setting.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. The cat MUST remain a Ragdoll with blue eyes and white/cream fur
|
||||
2. The product MUST remain ice-blue, flower-shaped, TOUCHDOG branded
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Create a horizontal banner layout (3:2 ratio)
|
||||
2. LEFT 40%: Brand text area with:
|
||||
- "TOUCHDOG" in playful coral/salmon color (#E8A87C)
|
||||
- Small paw print icons around the brand name
|
||||
- "CAT SOFT CONE COLLAR" below in gray
|
||||
3. RIGHT 60%: The cat wearing the product in a warm home setting
|
||||
4. Soft, warm color palette throughout
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Left side: Brand name and product title
|
||||
- Right side: Hero image of cat with product
|
||||
- Warm, cohesive color scheme
|
||||
|
||||
STYLE GUIDE:
|
||||
Amazon A+ brand story style. Warm and inviting. Professional yet friendly.
|
||||
|
||||
OUTPUT: 3:2 landscape ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### APlus_02: Comparison Chart
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: A Ragdoll cat wearing an ice-blue TOUCHDOG soft recovery cone collar.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. LEFT SIDE: The cat and product MUST match the reference - Ragdoll cat, ice-blue soft cone
|
||||
2. The product color (#B5E5E8), flower shape, and TOUCHDOG branding on left side
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Create a side-by-side comparison layout
|
||||
2. LEFT SIDE (60% width, colorful):
|
||||
- Happy Ragdoll cat wearing our ice-blue soft cone (from reference)
|
||||
- Green checkmark icon
|
||||
- "OUR" label on coral/orange banner
|
||||
- 3 benefits in white: "CLOUD-LIGHT COMFORT", "WIDER & CLEARER", "FOLDABLE & PORTABLE"
|
||||
3. RIGHT SIDE (40% width, grayscale):
|
||||
- Sad-looking cat wearing a DIFFERENT product: hard plastic transparent cone (traditional e-collar)
|
||||
- Red X icon
|
||||
- "OTHER" label on gray banner
|
||||
- 3 drawbacks in gray: "HEAVY & BULKY", "BLOCKS VISION & MOVEMENT", "HARD TO STORE"
|
||||
4. Warm beige background with paw print watermarks
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Split layout with curved divider
|
||||
- Left side larger, full color, positive mood
|
||||
- Right side smaller, desaturated, negative mood
|
||||
- Text overlays on each side
|
||||
|
||||
STYLE GUIDE:
|
||||
Clear visual contrast between "us" and "them".
|
||||
Left side warm and inviting, right side cold and uncomfortable.
|
||||
IMPORTANT: Right side must show a DIFFERENT type of cone (hard plastic), not our product.
|
||||
|
||||
OUTPUT: 3:2 landscape ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### APlus_03: Feature Details
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: A Ragdoll cat wearing an ice-blue TOUCHDOG soft recovery cone collar.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. The cat MUST remain a Ragdoll with blue eyes
|
||||
2. The product MUST remain ice-blue, flower-shaped, TOUCHDOG branded
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Create horizontal layout (3:2) with title and 3 detail panels
|
||||
2. Title at top: "ENGINEERED FOR UNCOMPROMISED COMFORT" in dark text
|
||||
3. 3 detail images in a row below:
|
||||
- Panel 1: Close-up of inner cotton lining texture, caption "STURDY AND BREATHABLE"
|
||||
- Panel 2: Cat wearing the product looking comfortable, caption "EASY TO CLEAN, STYLISH"
|
||||
- Panel 3: Close-up of stitching detail, caption "REINFORCED STITCHING"
|
||||
4. Warm beige background (#F5EBE0) with subtle paw prints
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Top: Title banner
|
||||
- Bottom: Three equal-width panels with captions
|
||||
|
||||
STYLE GUIDE:
|
||||
Amazon A+ feature module style. Clean, informative. Warm color palette.
|
||||
|
||||
OUTPUT: 3:2 landscape ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### APlus_04: 4-Scene Horizontal
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: A Ragdoll cat (white/cream fur, blue eyes, brown ear markings) wearing an ice-blue TOUCHDOG soft recovery cone collar.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. ALL 4 SCENES MUST SHOW THE EXACT SAME CAT: Ragdoll breed, blue eyes, white/cream fur
|
||||
2. ALL 4 SCENES MUST SHOW THE EXACT SAME PRODUCT: Ice-blue flower-shaped soft cone
|
||||
3. Consistent cat appearance and product across all panels
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Create horizontal layout (3:2) with 4 scenes in a row
|
||||
2. Scene 1: Cat standing - "HYGIENIC & EASY TO CLEAN"
|
||||
3. Scene 2: Cat eating from bowl - "UNRESTRICTED EATING"
|
||||
4. Scene 3: Cat playing/stretching - "REVERSIBLE WEAR"
|
||||
5. Scene 4: Cat sleeping curled up - "360° COMFORT"
|
||||
6. Each scene in a rounded rectangle frame
|
||||
7. Warm beige background with paw print decorations
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- 4 equal panels arranged horizontally
|
||||
- Each panel has image + caption below
|
||||
- Consistent warm lighting across all
|
||||
|
||||
STYLE GUIDE:
|
||||
Lifestyle photography. Same cat in all scenes. Warm, cozy feel.
|
||||
|
||||
OUTPUT: 3:2 landscape ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### APlus_05: Multi-Angle Horizontal
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: A Ragdoll cat wearing an ice-blue TOUCHDOG soft recovery cone collar.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. Both views must show the SAME Ragdoll cat with blue eyes
|
||||
2. Both views must show the SAME ice-blue TOUCHDOG cone
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Create horizontal split layout (3:2)
|
||||
2. LEFT: Front view of cat wearing cone
|
||||
3. RIGHT: Side/profile view of same cat with cone
|
||||
4. Elegant curved divider between the two views
|
||||
5. Warm home background in both
|
||||
6. NO text - pure visual showcase
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Two equal halves with curved divider
|
||||
- Left: Front angle
|
||||
- Right: Side angle
|
||||
- Warm consistent lighting
|
||||
|
||||
STYLE GUIDE:
|
||||
High-end lifestyle photography. No text. Warm atmosphere.
|
||||
|
||||
OUTPUT: 3:2 landscape ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
### APlus_06: Size Chart Horizontal (Corrected)
|
||||
```text
|
||||
[IMAGE EDITING TASK - NOT GENERATION]
|
||||
|
||||
REFERENCE IMAGE ANALYSIS:
|
||||
The reference image shows: An ice-blue TOUCHDOG soft pet recovery cone collar in flat lay position.
|
||||
CRITICAL SHAPE DETAIL: The product has a C-SHAPED OPENING (not a closed circle) - there is a GAP on the left side where the velcro strap attaches. This opening allows the collar to wrap around the pet's neck.
|
||||
|
||||
CRITICAL PRESERVATION REQUIREMENTS (DO NOT MODIFY):
|
||||
1. The product MUST keep its C-SHAPED OPENING - DO NOT close the gap into a full circle
|
||||
2. The velcro strap visible on the left side of the opening
|
||||
3. Ice-blue color (#B5E5E8), flower/petal shape with 8 segments
|
||||
4. TOUCHDOG brand text on the product
|
||||
5. Green/teal inner rim around the neck hole
|
||||
|
||||
EDITING TASKS (ONLY THESE CHANGES ARE ALLOWED):
|
||||
1. Change background from black to warm beige (#F5EDE4)
|
||||
2. Add title "PRODUCT SIZE" at top center in dark text
|
||||
3. Add dimension labels: "NECK" pointing to inner circle, "DEPTH" pointing outward
|
||||
4. Add size chart table on the right:
|
||||
SIZE | NECK | DEPTH
|
||||
XS | 5.6-6.8IN | 3.2IN
|
||||
S | 7.2-8.4IN | 4IN
|
||||
M | 8.8-10.4IN | 5IN
|
||||
L | 10.8-12.4IN | 6IN
|
||||
XL | 12.8-14.4IN | 7IN
|
||||
5. Add note in coral: "ALWAYS MEASURE YOUR PET'S NECK BEFORE SELECTING A SIZE"
|
||||
6. Add subtle paw print watermarks
|
||||
|
||||
LAYOUT SPECIFICATION:
|
||||
- Left 45%: Product image maintaining C-shape opening
|
||||
- Right 55%: Size chart table
|
||||
- Top: Title
|
||||
- Bottom: Note text
|
||||
|
||||
STYLE GUIDE:
|
||||
Clean infographic style. The product's C-shaped opening must be clearly visible - this is a key feature showing how it wraps around the neck.
|
||||
|
||||
OUTPUT: 3:2 landscape ratio, professional Amazon listing quality
|
||||
|
||||
FINAL CHECK: Before output, verify that the pet and product in the result match the reference image EXACTLY in:
|
||||
- Species, breed, fur color and pattern
|
||||
- Product color (#B5E5E8 ice blue), shape (flower/petal cone), brand text "TOUCHDOG"
|
||||
- Pose and positioning relative to original
|
||||
```
|
||||
|
||||
2018
public/css/bootstrap-icons.css
vendored
Normal file
2018
public/css/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
public/css/bootstrap.min.css
vendored
Normal file
6
public/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
public/fonts/bootstrap-icons.woff
Normal file
BIN
public/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
public/fonts/bootstrap-icons.woff2
Normal file
BIN
public/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
845
public/index.html
Normal file
845
public/index.html
Normal file
@@ -0,0 +1,845 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Touchdog AI 智能电商出图系统</title>
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/css/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #E60012;
|
||||
--secondary-color: #c5000f;
|
||||
--accent-color: #FF6B6B;
|
||||
--bg-light: #fafafa;
|
||||
--text-dark: #333;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #fafafa 100%);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.main-container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||
.brand-header {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #ff4444 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(230, 0, 18, 0.2);
|
||||
}
|
||||
.brand-logo { font-size: 2rem; font-weight: bold; }
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 1.5rem;
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.card-header-custom {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.step-badge {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.btn-outline-primary:hover {
|
||||
background: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
.json-editor {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
}
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.image-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.image-card:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
||||
}
|
||||
.image-card img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
cursor: pointer;
|
||||
}
|
||||
.image-card .card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
.image-type-badge {
|
||||
font-size: 11px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.badge-main { background: #4CAF50; }
|
||||
.badge-aplus { background: #2196F3; }
|
||||
.progress-ring {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255,255,255,0.95);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
.tab-content { padding-top: 1.5rem; }
|
||||
.nav-tabs .nav-link {
|
||||
color: var(--text-dark);
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
color: var(--primary-color);
|
||||
border-bottom: 3px solid var(--primary-color);
|
||||
background: transparent;
|
||||
}
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.status-success { background: #4CAF50; }
|
||||
.status-failed { background: #f44336; }
|
||||
.status-pending { background: #ff9800; }
|
||||
.toast-container { position: fixed; top: 20px; right: 20px; z-index: 1050; }
|
||||
.ref-image-preview {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #ddd;
|
||||
}
|
||||
.ref-image-preview:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<!-- Brand Header -->
|
||||
<div class="brand-header">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div class="brand-logo">
|
||||
<i class="bi bi-stars"></i> Touchdog AI 智能电商出图系统
|
||||
</div>
|
||||
<p class="mb-0 mt-2 opacity-75">输入SKU数据 → Brain智能规划 → 自动生成12张专业电商图</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="badge bg-light text-dark px-3 py-2">
|
||||
<i class="bi bi-cpu"></i> Gemini 3 Pro Image
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Tabs -->
|
||||
<ul class="nav nav-tabs" id="mainTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#skuTab">
|
||||
<i class="bi bi-box-seam"></i> SKU智能出图
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#legacyTab">
|
||||
<i class="bi bi-image"></i> 传统模式
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#historyTab">
|
||||
<i class="bi bi-clock-history"></i> 历史记录
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- SKU智能出图 Tab -->
|
||||
<div class="tab-pane fade show active" id="skuTab">
|
||||
<div class="row">
|
||||
<!-- 左侧:SKU输入 -->
|
||||
<div class="col-lg-5">
|
||||
<div class="card">
|
||||
<div class="card-header-custom">
|
||||
<span class="step-badge">1</span>
|
||||
<strong>输入SKU数据</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0 fw-bold">SKU JSON数据</label>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="loadSampleSKU()">
|
||||
<i class="bi bi-file-earmark-code"></i> 加载示例
|
||||
</button>
|
||||
</div>
|
||||
<textarea class="form-control json-editor" id="skuInput" rows="18"
|
||||
placeholder='{"sku_id": "...", "product_name": "...", ...}'></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">参考图片URLs</label>
|
||||
<div id="refImagesPreview" class="d-flex gap-2 flex-wrap mb-2"></div>
|
||||
<small class="text-muted">在SKU JSON中设置 ref_images 字段</small>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary w-100" id="generateSkuBtn" onclick="generateSKU()">
|
||||
<i class="bi bi-magic"></i> 开始智能生图
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brain分析结果 -->
|
||||
<div class="card" id="analysisCard" style="display:none;">
|
||||
<div class="card-header-custom">
|
||||
<span class="step-badge">2</span>
|
||||
<strong>Brain分析结果</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="analysisContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:生成结果 -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header-custom d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="step-badge">3</span>
|
||||
<strong>生成结果</strong>
|
||||
<span class="badge bg-secondary ms-2" id="resultCount">0/12</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="downloadAllImages()" id="downloadAllBtn" disabled>
|
||||
<i class="bi bi-download"></i> 批量下载
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="generationProgress" style="display:none;" class="mb-4">
|
||||
<div class="progress" style="height: 8px;">
|
||||
<div class="progress-bar bg-danger" id="progressBar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
<small class="text-muted" id="progressText">准备中...</small>
|
||||
<small class="text-muted" id="progressStats"></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="imageResults" class="image-grid">
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-images" style="font-size: 3rem; opacity: 0.3;"></i>
|
||||
<p class="mt-3">输入SKU数据后点击"开始智能生图"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传统模式 Tab -->
|
||||
<div class="tab-pane fade" id="legacyTab">
|
||||
<div class="card p-4">
|
||||
<div class="card-header-custom mb-3">
|
||||
<span class="step-badge">1</span>
|
||||
<strong>素材与需求录入</strong>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-bold">参考图片上传 (R2存储)</label>
|
||||
<div class="border rounded p-3 bg-light text-center" style="border-style: dashed !important; cursor: pointer;" onclick="document.getElementById('imageUpload').click()">
|
||||
<i class="bi bi-cloud-upload fs-1 text-muted"></i>
|
||||
<p class="text-muted mb-0">点击或拖拽上传图片</p>
|
||||
<input type="file" id="imageUpload" multiple accept="image/*" class="d-none" onchange="handleFileSelect(this)">
|
||||
</div>
|
||||
<div id="uploadPreview" class="d-flex flex-wrap gap-2 mt-2"></div>
|
||||
<button class="btn btn-outline-primary w-100 mt-3" id="uploadBtn" onclick="uploadImages()">
|
||||
<i class="bi bi-upload"></i> 开始上传
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">产品介绍</label>
|
||||
<textarea class="form-control" id="productIntro" rows="3" placeholder="例如:这是一款超轻伊丽莎白圈,防水易清洁..."></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">图片设计要求</label>
|
||||
<textarea class="form-control" id="designReqs" rows="3" placeholder="例如:需要4张主图,纯白背景,展示细节..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="card-header-custom mb-3">
|
||||
<span class="step-badge">2</span>
|
||||
<strong>生成提示词</strong>
|
||||
</div>
|
||||
<button class="btn btn-primary mb-3" id="genPromptsBtn" onclick="generatePrompts()">
|
||||
<i class="bi bi-chat-right-quote"></i> 调用大模型生成提示词
|
||||
</button>
|
||||
<textarea class="form-control json-editor" id="promptsJson" rows="6" readonly></textarea>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="card-header-custom mb-3">
|
||||
<span class="step-badge">3</span>
|
||||
<strong>生成图片</strong>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button class="btn btn-success flex-grow-1" id="genImagesBtn" onclick="generateImages()">
|
||||
<i class="bi bi-palette"></i> 开始批量生图
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="downloadAllLegacy()">
|
||||
<i class="bi bi-download"></i> 批量下载
|
||||
</button>
|
||||
</div>
|
||||
<div id="legacyImageResults" class="image-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录 Tab -->
|
||||
<div class="tab-pane fade" id="historyTab">
|
||||
<div class="card">
|
||||
<div class="card-header-custom d-flex justify-content-between">
|
||||
<strong><i class="bi bi-clock-history"></i> 生成历史</strong>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="clearHistory()">
|
||||
<i class="bi bi-trash"></i> 清空历史
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="historyList" class="list-group list-group-flush"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay" id="loadingOverlay" style="display:none;">
|
||||
<div class="spinner-border text-danger" style="width: 4rem; height: 4rem;" role="status"></div>
|
||||
<h4 class="mt-4" id="loadingTitle">正在生成中...</h4>
|
||||
<p class="text-muted" id="loadingSubtitle">Brain正在分析产品并规划图片策略</p>
|
||||
<div class="progress mt-3" style="width: 300px; height: 6px;">
|
||||
<div class="progress-bar bg-danger" id="loadingProgress" style="width: 0%"></div>
|
||||
</div>
|
||||
<small class="text-muted mt-2" id="loadingStatus">初始化...</small>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/axios.min.js"></script>
|
||||
<script>
|
||||
// ============================================
|
||||
// 状态管理
|
||||
// ============================================
|
||||
let state = {
|
||||
currentSKU: null,
|
||||
currentPlan: null,
|
||||
generatedImages: [],
|
||||
uploadedUrls: [],
|
||||
history: []
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 工具函数
|
||||
// ============================================
|
||||
function showToast(message, type = 'success') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const bgClass = type === 'error' ? 'text-bg-danger' : type === 'warning' ? 'text-bg-warning' : 'text-bg-success';
|
||||
const toastEl = document.createElement('div');
|
||||
toastEl.className = `toast align-items-center ${bgClass} border-0`;
|
||||
toastEl.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">${message}</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(toastEl);
|
||||
new bootstrap.Toast(toastEl).show();
|
||||
setTimeout(() => toastEl.remove(), 4000);
|
||||
}
|
||||
|
||||
function setLoading(show, title = '', subtitle = '', status = '') {
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
overlay.style.display = show ? 'flex' : 'none';
|
||||
if (title) document.getElementById('loadingTitle').textContent = title;
|
||||
if (subtitle) document.getElementById('loadingSubtitle').textContent = subtitle;
|
||||
if (status) document.getElementById('loadingStatus').textContent = status;
|
||||
}
|
||||
|
||||
function updateLoadingProgress(percent, status) {
|
||||
document.getElementById('loadingProgress').style.width = percent + '%';
|
||||
if (status) document.getElementById('loadingStatus').textContent = status;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SKU智能出图功能
|
||||
// ============================================
|
||||
async function loadSampleSKU() {
|
||||
try {
|
||||
const res = await axios.get('/api/sample-sku');
|
||||
document.getElementById('skuInput').value = JSON.stringify(res.data, null, 2);
|
||||
updateRefImagesPreview(res.data.ref_images || []);
|
||||
showToast('示例SKU已加载');
|
||||
} catch (e) {
|
||||
showToast('加载示例失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateRefImagesPreview(urls) {
|
||||
const container = document.getElementById('refImagesPreview');
|
||||
container.innerHTML = urls.map(url =>
|
||||
`<img src="${url}" class="ref-image-preview" onclick="window.open('${url}')" alt="参考图">`
|
||||
).join('');
|
||||
}
|
||||
|
||||
async function generateSKU() {
|
||||
const skuInput = document.getElementById('skuInput').value.trim();
|
||||
if (!skuInput) {
|
||||
showToast('请输入SKU数据', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let sku;
|
||||
try {
|
||||
sku = JSON.parse(skuInput);
|
||||
} catch (e) {
|
||||
showToast('SKU数据JSON格式错误', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sku.sku_id) {
|
||||
showToast('SKU数据缺少sku_id字段', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentSKU = sku;
|
||||
state.generatedImages = [];
|
||||
|
||||
// 显示加载状态
|
||||
setLoading(true, '正在生成中...', 'Brain正在分析产品并规划图片策略', '初始化...');
|
||||
document.getElementById('generateSkuBtn').disabled = true;
|
||||
document.getElementById('generationProgress').style.display = 'block';
|
||||
document.getElementById('imageResults').innerHTML = '';
|
||||
document.getElementById('analysisCard').style.display = 'none';
|
||||
|
||||
try {
|
||||
// 调用生成API
|
||||
updateLoadingProgress(5, 'Stage 1: Brain正在规划...');
|
||||
|
||||
const response = await axios.post('/api/generate-sku', { sku }, {
|
||||
timeout: 600000 // 10分钟超时
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
state.currentPlan = response.data.plan;
|
||||
state.generatedImages = response.data.results;
|
||||
|
||||
// 显示分析结果
|
||||
showAnalysis(response.data.plan.analysis);
|
||||
|
||||
// 显示生成的图片
|
||||
renderGeneratedImages(response.data.results);
|
||||
|
||||
// 更新统计
|
||||
const successCount = response.data.summary.success;
|
||||
document.getElementById('resultCount').textContent = `${successCount}/12`;
|
||||
document.getElementById('downloadAllBtn').disabled = successCount === 0;
|
||||
|
||||
// 保存到历史
|
||||
saveToHistory(sku, response.data.results);
|
||||
|
||||
showToast(`生成完成!成功 ${successCount}/12 张图片`);
|
||||
} else {
|
||||
throw new Error(response.data.error || '生成失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Generation error:', error);
|
||||
showToast('生成失败: ' + (error.response?.data?.error || error.message), 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
document.getElementById('generateSkuBtn').disabled = false;
|
||||
document.getElementById('generationProgress').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showAnalysis(analysis) {
|
||||
const card = document.getElementById('analysisCard');
|
||||
const content = document.getElementById('analysisContent');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">产品品类</small>
|
||||
<div class="fw-bold">${analysis.product_category || '-'}</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">背景策略</small>
|
||||
<div class="fw-bold">${analysis.background_strategy === 'light' ? '浅色背景' : '深色背景'}</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Logo颜色</small>
|
||||
<div class="fw-bold" style="color: ${analysis.logo_color === 'red' ? '#E60012' : '#333'}">
|
||||
${analysis.logo_color === 'red' ? '红色' : '白色'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">核心卖点</small>
|
||||
<div class="fw-bold">${(analysis.core_selling_points || []).join(', ')}</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<small class="text-muted">视觉风格</small>
|
||||
<div>${analysis.visual_style || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
card.style.display = 'block';
|
||||
}
|
||||
|
||||
function renderGeneratedImages(results) {
|
||||
const container = document.getElementById('imageResults');
|
||||
|
||||
container.innerHTML = results.map(img => {
|
||||
const isMain = img.id.startsWith('Main_');
|
||||
const badgeClass = isMain ? 'badge-main' : 'badge-aplus';
|
||||
const statusClass = img.status === 'success' ? 'status-success' : 'status-failed';
|
||||
|
||||
return `
|
||||
<div class="image-card">
|
||||
${img.url ?
|
||||
`<img src="${img.url}" onclick="window.open('${img.url}')" alt="${img.id}">` :
|
||||
`<div class="d-flex align-items-center justify-content-center bg-light" style="height:200px">
|
||||
<i class="bi bi-exclamation-triangle text-danger fs-1"></i>
|
||||
</div>`
|
||||
}
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="badge image-type-badge ${badgeClass}">${img.id}</span>
|
||||
<span><span class="status-dot ${statusClass}"></span>${img.status === 'success' ? '成功' : '失败'}</span>
|
||||
</div>
|
||||
<div class="small text-muted">${img.type || ''}</div>
|
||||
${img.purpose ? `<div class="small mt-1">${img.purpose}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function downloadAllImages() {
|
||||
const successImages = state.generatedImages.filter(img => img.status === 'success' && img.url);
|
||||
if (successImages.length === 0) {
|
||||
showToast('没有可下载的图片', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
successImages.forEach((img, idx) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = img.url;
|
||||
link.download = `${state.currentSKU?.sku_id || 'image'}-${img.id}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
setTimeout(() => {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}, idx * 500);
|
||||
});
|
||||
|
||||
showToast(`开始下载 ${successImages.length} 张图片`);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 历史记录
|
||||
// ============================================
|
||||
function saveToHistory(sku, results) {
|
||||
const entry = {
|
||||
id: Date.now(),
|
||||
date: new Date().toLocaleString(),
|
||||
sku_id: sku.sku_id,
|
||||
product_name: sku.product_name,
|
||||
results: results
|
||||
};
|
||||
|
||||
let history = JSON.parse(localStorage.getItem('skuHistory') || '[]');
|
||||
history.unshift(entry);
|
||||
if (history.length > 50) history = history.slice(0, 50);
|
||||
localStorage.setItem('skuHistory', JSON.stringify(history));
|
||||
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
const history = JSON.parse(localStorage.getItem('skuHistory') || '[]');
|
||||
const container = document.getElementById('historyList');
|
||||
|
||||
if (history.length === 0) {
|
||||
container.innerHTML = '<div class="text-center text-muted py-4">暂无历史记录</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = history.map(entry => {
|
||||
const successCount = entry.results?.filter(r => r.status === 'success').length || 0;
|
||||
return `
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>${entry.sku_id}</strong>
|
||||
<small class="text-muted ms-2">${entry.product_name || ''}</small>
|
||||
<div class="small text-muted">${entry.date}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-success">${successCount}/12</span>
|
||||
<button class="btn btn-sm btn-outline-primary ms-2" onclick="loadHistoryEntry(${entry.id})">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function loadHistoryEntry(id) {
|
||||
const history = JSON.parse(localStorage.getItem('skuHistory') || '[]');
|
||||
const entry = history.find(h => h.id === id);
|
||||
if (entry) {
|
||||
state.generatedImages = entry.results;
|
||||
renderGeneratedImages(entry.results);
|
||||
const successCount = entry.results.filter(r => r.status === 'success').length;
|
||||
document.getElementById('resultCount').textContent = `${successCount}/12`;
|
||||
document.getElementById('downloadAllBtn').disabled = successCount === 0;
|
||||
|
||||
// 切换到SKU标签页
|
||||
document.querySelector('[data-bs-target="#skuTab"]').click();
|
||||
showToast('已加载历史记录');
|
||||
}
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
if (confirm('确定要清空所有历史记录吗?')) {
|
||||
localStorage.removeItem('skuHistory');
|
||||
renderHistory();
|
||||
showToast('历史记录已清空');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 传统模式功能(保留原有功能)
|
||||
// ============================================
|
||||
function handleFileSelect(input) {
|
||||
const container = document.getElementById('uploadPreview');
|
||||
container.innerHTML = '';
|
||||
if (input.files) {
|
||||
Array.from(input.files).forEach(file => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const img = document.createElement('img');
|
||||
img.src = e.target.result;
|
||||
img.className = 'ref-image-preview';
|
||||
container.appendChild(img);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadImages() {
|
||||
const fileInput = document.getElementById('imageUpload');
|
||||
const files = fileInput.files;
|
||||
if (!files.length) {
|
||||
showToast('请先选择文件', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files', files[i]);
|
||||
}
|
||||
|
||||
try {
|
||||
document.getElementById('uploadBtn').disabled = true;
|
||||
const res = await axios.post('/api/upload-r2', formData);
|
||||
state.uploadedUrls = res.data.urls;
|
||||
showToast(`成功上传 ${res.data.urls.length} 张图片`);
|
||||
} catch (e) {
|
||||
showToast('上传失败', 'error');
|
||||
} finally {
|
||||
document.getElementById('uploadBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePrompts() {
|
||||
const intro = document.getElementById('productIntro').value;
|
||||
const reqs = document.getElementById('designReqs').value;
|
||||
if (!intro || !reqs) {
|
||||
showToast('请填写产品介绍和设计要求', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
document.getElementById('genPromptsBtn').disabled = true;
|
||||
const templateRes = await axios.get('/api/prompt-template');
|
||||
let prompt = templateRes.data.content
|
||||
.replace('[产品介绍]', intro)
|
||||
.replace('[图片设计要求]', reqs);
|
||||
|
||||
const res = await axios.post('/api/generate-prompts', { prompt });
|
||||
let content = res.data.data?.choices?.[0]?.message?.content || '';
|
||||
|
||||
// 解析JSON
|
||||
content = content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
||||
const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) || content.match(/\[[\s\S]*\]/);
|
||||
if (jsonMatch) {
|
||||
const prompts = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
||||
document.getElementById('promptsJson').value = JSON.stringify(prompts, null, 2);
|
||||
state.legacyPrompts = prompts;
|
||||
showToast('提示词生成成功');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('生成失败', 'error');
|
||||
} finally {
|
||||
document.getElementById('genPromptsBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateImages() {
|
||||
if (!state.legacyPrompts?.length) {
|
||||
showToast('请先生成提示词', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('legacyImageResults');
|
||||
container.innerHTML = '';
|
||||
state.legacyImages = [];
|
||||
|
||||
document.getElementById('genImagesBtn').disabled = true;
|
||||
|
||||
for (const item of state.legacyPrompts) {
|
||||
const promptText = item.ai_prompt || item.prompt;
|
||||
if (!promptText) continue;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'image-card';
|
||||
card.innerHTML = `
|
||||
<div class="d-flex align-items-center justify-content-center bg-light" style="height:200px">
|
||||
<div class="spinner-border text-secondary"></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="small text-muted">${item.type || item.id}</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
|
||||
try {
|
||||
const res = await axios.post('/api/generate-image', {
|
||||
prompt: promptText,
|
||||
refImages: state.uploadedUrls
|
||||
});
|
||||
|
||||
const imgUrl = res.data.data?.[0]?.url;
|
||||
if (imgUrl) {
|
||||
card.querySelector('.bg-light').innerHTML = `<img src="${imgUrl}" onclick="window.open('${imgUrl}')" style="width:100%;height:200px;object-fit:cover;cursor:pointer">`;
|
||||
state.legacyImages.push({ prompt: promptText, url: imgUrl });
|
||||
}
|
||||
} catch (e) {
|
||||
card.querySelector('.bg-light').innerHTML = `<i class="bi bi-exclamation-triangle text-danger fs-1"></i>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('genImagesBtn').disabled = false;
|
||||
showToast('图片生成完成');
|
||||
}
|
||||
|
||||
function downloadAllLegacy() {
|
||||
if (!state.legacyImages?.length) {
|
||||
showToast('没有可下载的图片', 'warning');
|
||||
return;
|
||||
}
|
||||
state.legacyImages.forEach((img, idx) => {
|
||||
const link = document.createElement('a');
|
||||
link.href = img.url;
|
||||
link.download = `generated-${idx}.png`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 初始化
|
||||
// ============================================
|
||||
window.onload = () => {
|
||||
renderHistory();
|
||||
|
||||
// 监听SKU输入变化,更新参考图预览
|
||||
document.getElementById('skuInput').addEventListener('input', (e) => {
|
||||
try {
|
||||
const sku = JSON.parse(e.target.value);
|
||||
if (sku.ref_images) {
|
||||
updateRefImagesPreview(sku.ref_images);
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
public/js/axios.min.js
vendored
Normal file
3
public/js/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
public/js/bootstrap.bundle.min.js
vendored
Normal file
7
public/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
119
schemas/brain-output.schema.json
Normal file
119
schemas/brain-output.schema.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "touchdog-brain-output",
|
||||
"title": "Touchdog Brain Output Schema",
|
||||
"description": "Brain决策层输出的图片规划数据结构",
|
||||
"type": "object",
|
||||
"required": ["analysis", "images"],
|
||||
"properties": {
|
||||
"analysis": {
|
||||
"type": "object",
|
||||
"description": "产品分析结果",
|
||||
"required": ["product_category", "core_selling_points", "background_strategy", "logo_color"],
|
||||
"properties": {
|
||||
"product_category": {
|
||||
"type": "string",
|
||||
"description": "产品品类英文名",
|
||||
"examples": ["Pet Recovery Cone", "Dog Harness", "Pet Bed"]
|
||||
},
|
||||
"core_selling_points": {
|
||||
"type": "array",
|
||||
"description": "选中的核心卖点key列表(3-4个)",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 3,
|
||||
"maxItems": 4,
|
||||
"examples": [["lightweight", "waterproof", "breathable"]]
|
||||
},
|
||||
"background_strategy": {
|
||||
"type": "string",
|
||||
"description": "背景亮度策略",
|
||||
"enum": ["light", "dark", "mixed"]
|
||||
},
|
||||
"logo_color": {
|
||||
"type": "string",
|
||||
"description": "Logo颜色选择",
|
||||
"enum": ["red", "white"]
|
||||
},
|
||||
"visual_style": {
|
||||
"type": "string",
|
||||
"description": "整体视觉风格描述",
|
||||
"examples": ["Warm, caring home environment emphasizing comfort and ease"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"type": "array",
|
||||
"description": "12张图片的规划",
|
||||
"minItems": 12,
|
||||
"maxItems": 12,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "type", "purpose", "layout_description", "ai_prompt", "logo_placement"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "图片ID",
|
||||
"pattern": "^(Main_0[1-6]|APlus_0[1-6])$",
|
||||
"examples": ["Main_01", "APlus_01"]
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "图片类型",
|
||||
"examples": ["Hero Scene + Key Benefits", "Product Detail + Craftsmanship", "Competitor Comparison"]
|
||||
},
|
||||
"purpose": {
|
||||
"type": "string",
|
||||
"description": "这张图的营销目的(中文)",
|
||||
"examples": ["首图决定点击率,展示产品使用状态+核心卖点"]
|
||||
},
|
||||
"layout_description": {
|
||||
"type": "string",
|
||||
"description": "布局描述(中文,给人审核用)",
|
||||
"examples": ["上半部分:布偶猫佩戴冰蓝色伊丽莎白圈,在温馨的木质家居架子上。下半部分:浅蓝色圆弧Banner..."]
|
||||
},
|
||||
"ai_prompt": {
|
||||
"type": "string",
|
||||
"description": "完整的英文AI生图Prompt",
|
||||
"minLength": 100
|
||||
},
|
||||
"logo_placement": {
|
||||
"type": "object",
|
||||
"description": "Logo放置信息",
|
||||
"required": ["position", "type", "color"],
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "string",
|
||||
"description": "Logo位置",
|
||||
"enum": ["bottom-right", "bottom-left", "top-right", "top-left", "center", "none"]
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Logo类型",
|
||||
"enum": ["combined", "graphic_only", "text_only", "none"]
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"description": "Logo颜色",
|
||||
"enum": ["red", "white", "auto"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"aspect_ratio": {
|
||||
"type": "string",
|
||||
"description": "图片宽高比",
|
||||
"enum": ["1:1", "3:2"],
|
||||
"default": "1:1"
|
||||
},
|
||||
"is_comparison": {
|
||||
"type": "boolean",
|
||||
"description": "是否是竞品对比图",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
219
schemas/sku-input.schema.json
Normal file
219
schemas/sku-input.schema.json
Normal file
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "touchdog-sku-input",
|
||||
"title": "Touchdog SKU Input Schema",
|
||||
"description": "用于AI电商图片生成的SKU输入数据结构",
|
||||
"type": "object",
|
||||
"required": ["sku_id", "product_name", "brand", "color", "selling_points", "specs", "ref_images"],
|
||||
"properties": {
|
||||
"sku_id": {
|
||||
"type": "string",
|
||||
"description": "SKU唯一标识符",
|
||||
"examples": ["TD-EC-001-IBLUE"]
|
||||
},
|
||||
"product_name": {
|
||||
"type": "string",
|
||||
"description": "产品英文名称",
|
||||
"examples": ["Cat Soft Cone Collar"]
|
||||
},
|
||||
"product_name_cn": {
|
||||
"type": "string",
|
||||
"description": "产品中文名称",
|
||||
"examples": ["伊丽莎白圈"]
|
||||
},
|
||||
"brand": {
|
||||
"type": "string",
|
||||
"description": "品牌名称",
|
||||
"const": "Touchdog"
|
||||
},
|
||||
"color": {
|
||||
"type": "object",
|
||||
"description": "颜色信息",
|
||||
"required": ["name", "hex"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "颜色英文名",
|
||||
"examples": ["Ice Blue", "Mint Green", "Coral Pink"]
|
||||
},
|
||||
"name_cn": {
|
||||
"type": "string",
|
||||
"description": "颜色中文名",
|
||||
"examples": ["冰蓝色", "薄荷绿", "珊瑚粉"]
|
||||
},
|
||||
"series": {
|
||||
"type": "string",
|
||||
"description": "色系系列",
|
||||
"enum": ["Iridescent", "Solid", "Macaron", "Floral"],
|
||||
"examples": ["Iridescent"]
|
||||
},
|
||||
"hex": {
|
||||
"type": "string",
|
||||
"description": "主色HEX值",
|
||||
"pattern": "^#[0-9A-Fa-f]{6}$",
|
||||
"examples": ["#B0E0E6"]
|
||||
},
|
||||
"edge_color": {
|
||||
"type": "string",
|
||||
"description": "边缘包边颜色HEX值",
|
||||
"pattern": "^#[0-9A-Fa-f]{6}$",
|
||||
"examples": ["#7FDBDB"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"selling_points": {
|
||||
"type": "array",
|
||||
"description": "产品卖点列表",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["key", "title_en"],
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "卖点唯一标识",
|
||||
"examples": ["lightweight", "waterproof", "breathable", "adjustable", "wide_view", "foldable"]
|
||||
},
|
||||
"title_en": {
|
||||
"type": "string",
|
||||
"description": "卖点英文标题(用于图片显示)",
|
||||
"examples": ["LIGHTER THAN AN EGG", "WATERPROOF & EASY WIPE"]
|
||||
},
|
||||
"title_cn": {
|
||||
"type": "string",
|
||||
"description": "卖点中文标题",
|
||||
"examples": ["极致轻盈", "防水易清洁"]
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "具体数值(如有)",
|
||||
"examples": ["65g", "24.5cm"]
|
||||
},
|
||||
"description_en": {
|
||||
"type": "string",
|
||||
"description": "卖点英文描述",
|
||||
"examples": ["Cloud-light comfort, won't restrict pet activity"]
|
||||
},
|
||||
"description_cn": {
|
||||
"type": "string",
|
||||
"description": "卖点中文描述",
|
||||
"examples": ["云感舒适,不束缚宠物活动"]
|
||||
},
|
||||
"visual_prompt": {
|
||||
"type": "string",
|
||||
"description": "视觉化表达提示(用于AI生图)",
|
||||
"examples": ["product shown next to an egg for size comparison", "water droplets beading on the surface"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"specs": {
|
||||
"type": "object",
|
||||
"description": "产品规格",
|
||||
"required": ["weight", "sizes"],
|
||||
"properties": {
|
||||
"weight": {
|
||||
"type": "string",
|
||||
"description": "产品重量",
|
||||
"examples": ["65g"]
|
||||
},
|
||||
"depth_cm": {
|
||||
"type": "string",
|
||||
"description": "圈深度(厘米)",
|
||||
"examples": ["24.5"]
|
||||
},
|
||||
"sizes": {
|
||||
"type": "array",
|
||||
"description": "可用尺码",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["XS", "S", "M", "L", "XL"]
|
||||
},
|
||||
"examples": [["XS", "S", "M", "L", "XL"]]
|
||||
},
|
||||
"size_chart": {
|
||||
"type": "array",
|
||||
"description": "尺码对照表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"size": { "type": "string" },
|
||||
"neck_range_cm": { "type": "string" },
|
||||
"neck_range_in": { "type": "string" },
|
||||
"depth_cm": { "type": "string" },
|
||||
"depth_in": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"materials": {
|
||||
"type": "object",
|
||||
"description": "材质信息",
|
||||
"properties": {
|
||||
"outer": {
|
||||
"type": "string",
|
||||
"description": "外层材质",
|
||||
"examples": ["Durable Waterproof PU"]
|
||||
},
|
||||
"inner": {
|
||||
"type": "string",
|
||||
"description": "内层材质",
|
||||
"examples": ["95% Cotton + 5% Spandex"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"use_cases": {
|
||||
"type": "array",
|
||||
"description": "使用场景列表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name_en"],
|
||||
"properties": {
|
||||
"name_en": {
|
||||
"type": "string",
|
||||
"description": "场景英文名",
|
||||
"examples": ["Postoperative Care", "Eye Drop Application", "Nail Trimming", "Grooming"]
|
||||
},
|
||||
"name_cn": {
|
||||
"type": "string",
|
||||
"description": "场景中文名",
|
||||
"examples": ["术后恢复", "驱虫护理", "指甲修剪", "美容护理"]
|
||||
},
|
||||
"visual_prompt": {
|
||||
"type": "string",
|
||||
"description": "场景视觉化提示",
|
||||
"examples": ["cat resting comfortably after surgery wearing the cone"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ref_images": {
|
||||
"type": "array",
|
||||
"description": "参考图片URLs(用于产品一致性约束)",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"examples": [
|
||||
["https://r2.example.com/td-ec-001-flat.jpg", "https://r2.example.com/td-ec-001-worn.jpg"]
|
||||
]
|
||||
},
|
||||
"pet_type": {
|
||||
"type": "string",
|
||||
"description": "适用宠物类型",
|
||||
"enum": ["cat", "dog", "both"],
|
||||
"default": "cat"
|
||||
},
|
||||
"target_market": {
|
||||
"type": "string",
|
||||
"description": "目标市场",
|
||||
"enum": ["us", "eu", "global"],
|
||||
"default": "global"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
624
server.js
Normal file
624
server.js
Normal file
@@ -0,0 +1,624 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const { S3Client, PutObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
|
||||
// 导入新模块
|
||||
const { planImages } = require('./lib/brain');
|
||||
const { injectConstraints, getAspectRatio } = require('./lib/constraint-injector');
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Configure R2 Client
|
||||
const bucketName = process.env.R2_BUCKET_NAME || 'ai-flow';
|
||||
console.log('Using R2 Bucket:', bucketName);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
// Multer for memory storage (upload directly to R2)
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
// ============================================
|
||||
// 原有API端点
|
||||
// ============================================
|
||||
|
||||
// Endpoint to get prompt template
|
||||
app.get('/api/prompt-template', (req, res) => {
|
||||
try {
|
||||
const promptPath = path.join(__dirname, 'prompt.md');
|
||||
const content = fs.readFileSync(promptPath, 'utf-8');
|
||||
res.json({ content });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to read prompt template' });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to upload to R2
|
||||
app.post('/api/upload-r2', upload.array('files'), async (req, res) => {
|
||||
try {
|
||||
const files = req.files;
|
||||
if (!files || files.length === 0) {
|
||||
return res.status(400).json({ error: 'No files uploaded' });
|
||||
}
|
||||
|
||||
const uploadedUrls = [];
|
||||
|
||||
for (const file of files) {
|
||||
const fileName = `${Date.now()}-${file.originalname}`;
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: fileName,
|
||||
Body: file.buffer,
|
||||
ContentType: file.mimetype,
|
||||
});
|
||||
|
||||
await r2Client.send(command);
|
||||
|
||||
const publicUrl = process.env.R2_PUBLIC_DOMAIN
|
||||
? `${process.env.R2_PUBLIC_DOMAIN}/${fileName}`
|
||||
: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${bucketName}/${fileName}`;
|
||||
|
||||
uploadedUrls.push(publicUrl);
|
||||
}
|
||||
|
||||
res.json({ urls: uploadedUrls });
|
||||
} catch (error) {
|
||||
console.error('R2 Upload Error:', error);
|
||||
res.status(500).json({ error: 'Upload failed: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to delete from R2
|
||||
app.delete('/api/delete-r2', async (req, res) => {
|
||||
try {
|
||||
const { url } = req.body;
|
||||
if (!url) return res.status(400).json({ error: 'URL is required' });
|
||||
|
||||
const urlObj = new URL(url);
|
||||
const key = decodeURIComponent(urlObj.pathname.substring(1));
|
||||
|
||||
const bucketName = process.env.R2_BUCKET_NAME || 'ai-flow';
|
||||
|
||||
await r2Client.send(new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: key,
|
||||
}));
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('R2 Delete Error:', error);
|
||||
res.status(500).json({ error: 'Delete failed: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to generate prompts (LLM) - 保留原有功能
|
||||
app.post('/api/generate-prompts', async (req, res) => {
|
||||
try {
|
||||
const { prompt } = req.body;
|
||||
|
||||
const response = await axios.post('https://api2img.shubiaobiao.com/v1/chat/completions', {
|
||||
model: 'gemini-3-pro-preview',
|
||||
messages: [
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
stream: false
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.API_KEY}`
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ data: response.data });
|
||||
} catch (error) {
|
||||
console.error('LLM API Error:', error.response?.data || error.message);
|
||||
res.status(500).json({ error: 'Prompt generation failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to generate image (Sync) - 保留原有功能
|
||||
app.post('/api/generate-image', async (req, res) => {
|
||||
try {
|
||||
const { prompt, aspectRatio, refImages } = req.body;
|
||||
console.log('Generate Image Request:', { promptLength: prompt?.length, refImagesCount: refImages?.length });
|
||||
|
||||
let finalPrompt = prompt;
|
||||
let ar = aspectRatio || '1:1';
|
||||
|
||||
const arMatch = prompt.match(/--ar\s+(\d+:\d+)/i);
|
||||
if (arMatch) {
|
||||
ar = arMatch[1];
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: 'gemini-3-pro-image-preview',
|
||||
prompt: finalPrompt,
|
||||
n: 1,
|
||||
size: "1K",
|
||||
aspect_ratio: ar
|
||||
};
|
||||
|
||||
if (refImages && Array.isArray(refImages) && refImages.length > 0) {
|
||||
payload.image_urls = refImages;
|
||||
}
|
||||
|
||||
const response = await axios.post('https://api2img.shubiaobiao.com/v1/images/generations', payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.API_KEY}`
|
||||
},
|
||||
timeout: 120000
|
||||
});
|
||||
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
console.error('Image Gen API Error:', error.response?.data || error.message);
|
||||
res.status(500).json({ error: 'Image generation failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to get image result
|
||||
app.post('/api/get-image-result', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.body;
|
||||
if (!id) return res.status(400).json({ error: 'Task ID is required' });
|
||||
|
||||
const queryUrl = process.env.IMAGE_QUERY_API_URL || 'https://api.wuyinkeji.com/api/img/drawDetail';
|
||||
|
||||
const response = await axios.get(queryUrl, {
|
||||
params: {
|
||||
key: process.env.API_KEY,
|
||||
id: id
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
console.error('Image Query API Error:', error.response?.data || error.message);
|
||||
res.status(500).json({ error: 'Failed to query image result' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 新增API端点 - SKU智能生图
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 获取Brain System Prompt
|
||||
*/
|
||||
app.get('/api/brain-prompt', (req, res) => {
|
||||
try {
|
||||
const promptPath = path.join(__dirname, 'prompts/brain-system.md');
|
||||
const content = fs.readFileSync(promptPath, 'utf-8');
|
||||
res.json({ content });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to read brain prompt' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取示例SKU数据
|
||||
*/
|
||||
app.get('/api/sample-sku', (req, res) => {
|
||||
try {
|
||||
const skuPath = path.join(__dirname, 'data/sample-sku.json');
|
||||
const content = fs.readFileSync(skuPath, 'utf-8');
|
||||
res.json(JSON.parse(content));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to read sample SKU' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Stage 1: Brain决策 - 生成图片规划
|
||||
* POST /api/plan-images
|
||||
* Body: { sku: SKU数据对象 }
|
||||
* Returns: { plan: Brain输出的图片规划 }
|
||||
*/
|
||||
app.post('/api/plan-images', async (req, res) => {
|
||||
try {
|
||||
const { sku } = req.body;
|
||||
|
||||
if (!sku || !sku.sku_id) {
|
||||
return res.status(400).json({ error: 'SKU data is required' });
|
||||
}
|
||||
|
||||
console.log(`Planning images for SKU: ${sku.sku_id}`);
|
||||
|
||||
const plan = await planImages(sku, {
|
||||
apiKey: process.env.API_KEY
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sku_id: sku.sku_id,
|
||||
plan
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Plan Images Error:', error.message);
|
||||
res.status(500).json({ error: 'Failed to plan images: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Stage 2: 生成单张图片(带约束注入)
|
||||
* POST /api/generate-image-with-constraints
|
||||
* Body: { sku, imageSpec, plan }
|
||||
* Returns: { imageUrl }
|
||||
*/
|
||||
app.post('/api/generate-image-with-constraints', async (req, res) => {
|
||||
try {
|
||||
const { sku, imageSpec } = req.body;
|
||||
|
||||
if (!sku || !imageSpec) {
|
||||
return res.status(400).json({ error: 'SKU and imageSpec are required' });
|
||||
}
|
||||
|
||||
// 注入约束
|
||||
const isComparison = imageSpec.is_comparison || imageSpec.id === 'APlus_02';
|
||||
const fullPrompt = injectConstraints(
|
||||
imageSpec.ai_prompt,
|
||||
sku,
|
||||
{
|
||||
isComparison,
|
||||
logoPlacement: imageSpec.logo_placement || {}
|
||||
}
|
||||
);
|
||||
|
||||
// 确定宽高比
|
||||
const aspectRatio = getAspectRatio(imageSpec.id);
|
||||
|
||||
console.log(`Generating image ${imageSpec.id} for SKU ${sku.sku_id}`);
|
||||
|
||||
// 调用图像生成API
|
||||
const payload = {
|
||||
model: 'gemini-3-pro-image-preview',
|
||||
prompt: fullPrompt,
|
||||
n: 1,
|
||||
size: "1K",
|
||||
aspect_ratio: aspectRatio
|
||||
};
|
||||
|
||||
// 添加参考图
|
||||
if (sku.ref_images && sku.ref_images.length > 0) {
|
||||
payload.image_urls = sku.ref_images;
|
||||
}
|
||||
|
||||
const response = await axios.post('https://api2img.shubiaobiao.com/v1/images/generations', payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.API_KEY}`
|
||||
},
|
||||
timeout: 180000 // 3分钟超时
|
||||
});
|
||||
|
||||
// 提取图片URL
|
||||
let imageUrl = null;
|
||||
if (response.data.data && Array.isArray(response.data.data) && response.data.data.length > 0) {
|
||||
imageUrl = response.data.data[0].url;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
image_id: imageSpec.id,
|
||||
image_url: imageUrl,
|
||||
aspect_ratio: aspectRatio
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Generate Image With Constraints Error:', error.response?.data || error.message);
|
||||
res.status(500).json({ error: 'Image generation failed: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 完整流程:为单个SKU生成全部12张图
|
||||
* POST /api/generate-sku
|
||||
* Body: { sku: SKU数据对象 }
|
||||
* Returns: { results: [{ id, url, status }] }
|
||||
*/
|
||||
app.post('/api/generate-sku', async (req, res) => {
|
||||
try {
|
||||
const { sku } = req.body;
|
||||
|
||||
if (!sku || !sku.sku_id) {
|
||||
return res.status(400).json({ error: 'SKU data is required' });
|
||||
}
|
||||
|
||||
console.log(`\n========== Starting SKU Generation: ${sku.sku_id} ==========`);
|
||||
|
||||
// Stage 1: Brain规划
|
||||
console.log('Stage 1: Planning images...');
|
||||
const plan = await planImages(sku, { apiKey: process.env.API_KEY });
|
||||
console.log(`Plan generated with ${plan.images.length} images`);
|
||||
|
||||
// Stage 2: 逐张生成图片
|
||||
console.log('Stage 2: Generating images...');
|
||||
const results = [];
|
||||
|
||||
for (const imageSpec of plan.images) {
|
||||
try {
|
||||
console.log(` Generating ${imageSpec.id}...`);
|
||||
|
||||
// 注入约束
|
||||
const isComparison = imageSpec.is_comparison || imageSpec.id === 'APlus_02';
|
||||
const fullPrompt = injectConstraints(
|
||||
imageSpec.ai_prompt,
|
||||
sku,
|
||||
{
|
||||
isComparison,
|
||||
logoPlacement: imageSpec.logo_placement || {}
|
||||
}
|
||||
);
|
||||
|
||||
const aspectRatio = getAspectRatio(imageSpec.id);
|
||||
|
||||
const payload = {
|
||||
model: 'gemini-3-pro-image-preview',
|
||||
prompt: fullPrompt,
|
||||
n: 1,
|
||||
size: "1K",
|
||||
aspect_ratio: aspectRatio
|
||||
};
|
||||
|
||||
if (sku.ref_images && sku.ref_images.length > 0) {
|
||||
payload.image_urls = sku.ref_images;
|
||||
}
|
||||
|
||||
const response = await axios.post('https://api2img.shubiaobiao.com/v1/images/generations', payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.API_KEY}`
|
||||
},
|
||||
timeout: 180000
|
||||
});
|
||||
|
||||
let imageUrl = null;
|
||||
if (response.data.data && Array.isArray(response.data.data) && response.data.data.length > 0) {
|
||||
imageUrl = response.data.data[0].url;
|
||||
}
|
||||
|
||||
results.push({
|
||||
id: imageSpec.id,
|
||||
type: imageSpec.type,
|
||||
purpose: imageSpec.purpose,
|
||||
url: imageUrl,
|
||||
status: imageUrl ? 'success' : 'failed',
|
||||
aspect_ratio: aspectRatio
|
||||
});
|
||||
|
||||
console.log(` ✓ ${imageSpec.id} generated`);
|
||||
|
||||
// 添加小延迟避免API限流
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
} catch (imgError) {
|
||||
console.error(` ✗ ${imageSpec.id} failed:`, imgError.message);
|
||||
results.push({
|
||||
id: imageSpec.id,
|
||||
type: imageSpec.type,
|
||||
purpose: imageSpec.purpose,
|
||||
url: null,
|
||||
status: 'failed',
|
||||
error: imgError.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r.status === 'success').length;
|
||||
console.log(`\n========== SKU Generation Complete: ${successCount}/${results.length} ==========\n`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sku_id: sku.sku_id,
|
||||
plan: {
|
||||
analysis: plan.analysis,
|
||||
image_count: plan.images.length
|
||||
},
|
||||
results,
|
||||
summary: {
|
||||
total: results.length,
|
||||
success: successCount,
|
||||
failed: results.length - successCount
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Generate SKU Error:', error.message);
|
||||
res.status(500).json({ error: 'SKU generation failed: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 批量生成:为多个SKU生成图片
|
||||
* POST /api/batch-generate
|
||||
* Body: { skus: [SKU数据数组], concurrency: 并发数 }
|
||||
* Returns: { results: [{ sku_id, status, images }] }
|
||||
*/
|
||||
app.post('/api/batch-generate', async (req, res) => {
|
||||
try {
|
||||
const { skus, concurrency = 1 } = req.body;
|
||||
|
||||
if (!skus || !Array.isArray(skus) || skus.length === 0) {
|
||||
return res.status(400).json({ error: 'SKUs array is required' });
|
||||
}
|
||||
|
||||
console.log(`\n========== Batch Generation Started: ${skus.length} SKUs ==========`);
|
||||
|
||||
const allResults = [];
|
||||
|
||||
// 简单的串行处理(避免API限流)
|
||||
for (let i = 0; i < skus.length; i++) {
|
||||
const sku = skus[i];
|
||||
console.log(`\nProcessing SKU ${i + 1}/${skus.length}: ${sku.sku_id}`);
|
||||
|
||||
try {
|
||||
// 调用单个SKU生成
|
||||
const plan = await planImages(sku, { apiKey: process.env.API_KEY });
|
||||
|
||||
const skuResults = {
|
||||
sku_id: sku.sku_id,
|
||||
status: 'processing',
|
||||
images: [],
|
||||
plan_analysis: plan.analysis
|
||||
};
|
||||
|
||||
for (const imageSpec of plan.images) {
|
||||
try {
|
||||
const isComparison = imageSpec.is_comparison || imageSpec.id === 'APlus_02';
|
||||
const fullPrompt = injectConstraints(
|
||||
imageSpec.ai_prompt,
|
||||
sku,
|
||||
{ isComparison, logoPlacement: imageSpec.logo_placement || {} }
|
||||
);
|
||||
|
||||
const aspectRatio = getAspectRatio(imageSpec.id);
|
||||
|
||||
const payload = {
|
||||
model: 'gemini-3-pro-image-preview',
|
||||
prompt: fullPrompt,
|
||||
n: 1,
|
||||
size: "1K",
|
||||
aspect_ratio: aspectRatio
|
||||
};
|
||||
|
||||
if (sku.ref_images && sku.ref_images.length > 0) {
|
||||
payload.image_urls = sku.ref_images;
|
||||
}
|
||||
|
||||
const response = await axios.post('https://api2img.shubiaobiao.com/v1/images/generations', payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.API_KEY}`
|
||||
},
|
||||
timeout: 180000
|
||||
});
|
||||
|
||||
let imageUrl = null;
|
||||
if (response.data.data && Array.isArray(response.data.data) && response.data.data.length > 0) {
|
||||
imageUrl = response.data.data[0].url;
|
||||
}
|
||||
|
||||
skuResults.images.push({
|
||||
id: imageSpec.id,
|
||||
url: imageUrl,
|
||||
status: imageUrl ? 'success' : 'failed'
|
||||
});
|
||||
|
||||
// 延迟避免限流
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
} catch (imgErr) {
|
||||
skuResults.images.push({
|
||||
id: imageSpec.id,
|
||||
url: null,
|
||||
status: 'failed',
|
||||
error: imgErr.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const successCount = skuResults.images.filter(img => img.status === 'success').length;
|
||||
skuResults.status = successCount === skuResults.images.length ? 'complete' : 'partial';
|
||||
skuResults.summary = {
|
||||
total: skuResults.images.length,
|
||||
success: successCount,
|
||||
failed: skuResults.images.length - successCount
|
||||
};
|
||||
|
||||
allResults.push(skuResults);
|
||||
console.log(` SKU ${sku.sku_id} complete: ${successCount}/${skuResults.images.length} images`);
|
||||
|
||||
} catch (skuErr) {
|
||||
console.error(` SKU ${sku.sku_id} failed:`, skuErr.message);
|
||||
allResults.push({
|
||||
sku_id: sku.sku_id,
|
||||
status: 'failed',
|
||||
error: skuErr.message,
|
||||
images: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const completedCount = allResults.filter(r => r.status === 'complete').length;
|
||||
console.log(`\n========== Batch Complete: ${completedCount}/${skus.length} SKUs ==========\n`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
total_skus: skus.length,
|
||||
completed: completedCount,
|
||||
results: allResults
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Batch Generate Error:', error.message);
|
||||
res.status(500).json({ error: 'Batch generation failed: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取约束预览(调试用)
|
||||
* POST /api/preview-constraints
|
||||
*/
|
||||
app.post('/api/preview-constraints', (req, res) => {
|
||||
try {
|
||||
const { sku, imageSpec } = req.body;
|
||||
|
||||
if (!sku) {
|
||||
return res.status(400).json({ error: 'SKU data is required' });
|
||||
}
|
||||
|
||||
const isComparison = imageSpec?.is_comparison || imageSpec?.id === 'APlus_02';
|
||||
const fullPrompt = injectConstraints(
|
||||
imageSpec?.ai_prompt || 'Sample prompt for preview',
|
||||
sku,
|
||||
{
|
||||
isComparison,
|
||||
logoPlacement: imageSpec?.logo_placement || {}
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
original_prompt: imageSpec?.ai_prompt || 'Sample prompt for preview',
|
||||
full_prompt_with_constraints: fullPrompt,
|
||||
prompt_length: fullPrompt.length
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 启动服务器
|
||||
// ============================================
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`\n🚀 Server running at http://localhost:${port}`);
|
||||
console.log('📁 Available endpoints:');
|
||||
console.log(' GET /api/prompt-template - Get prompt template');
|
||||
console.log(' GET /api/brain-prompt - Get brain system prompt');
|
||||
console.log(' GET /api/sample-sku - Get sample SKU data');
|
||||
console.log(' POST /api/plan-images - Stage 1: Brain planning');
|
||||
console.log(' POST /api/generate-sku - Full SKU generation (12 images)');
|
||||
console.log(' POST /api/batch-generate - Batch SKU generation');
|
||||
console.log(' POST /api/preview-constraints - Preview constraints injection');
|
||||
console.log('');
|
||||
});
|
||||
359
test-generate.js
Normal file
359
test-generate.js
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 测试脚本:使用素材图片生成电商图片
|
||||
* 运行方式: node test-generate.js
|
||||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
|
||||
// 配置
|
||||
const SERVER_URL = 'http://localhost:3000';
|
||||
const OUTPUT_DIR = path.join(__dirname, 'output');
|
||||
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,
|
||||
});
|
||||
|
||||
/**
|
||||
* 上传图片到R2
|
||||
*/
|
||||
async function uploadToR2(filePath) {
|
||||
const fileName = `test-${Date.now()}-${path.basename(filePath)}`;
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
const contentType = filePath.endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: process.env.R2_BUCKET_NAME || 'ai-flow',
|
||||
Key: fileName,
|
||||
Body: fileBuffer,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
await r2Client.send(command);
|
||||
|
||||
const publicUrl = process.env.R2_PUBLIC_DOMAIN
|
||||
? `${process.env.R2_PUBLIC_DOMAIN}/${fileName}`
|
||||
: `https://pub-${process.env.R2_ACCOUNT_ID}.r2.dev/${fileName}`;
|
||||
|
||||
console.log(` 上传成功: ${fileName}`);
|
||||
return publicUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载图片到本地
|
||||
*/
|
||||
async function downloadImage(url, outputPath) {
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer', timeout: 60000 });
|
||||
fs.writeFileSync(outputPath, response.data);
|
||||
console.log(` 下载成功: ${path.basename(outputPath)}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(` 下载失败: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
async function main() {
|
||||
console.log('\n========================================');
|
||||
console.log('Touchdog 电商图片生成测试');
|
||||
console.log('========================================\n');
|
||||
|
||||
// 确保输出目录存在
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// 1. 获取素材图片
|
||||
console.log('📁 Step 1: 读取素材图片...');
|
||||
const materialFiles = fs.readdirSync(MATERIAL_DIR)
|
||||
.filter(f => /\.(jpg|jpeg|png)$/i.test(f))
|
||||
.slice(0, 3); // 取前3张作为参考
|
||||
|
||||
console.log(` 找到 ${materialFiles.length} 张素材图片`);
|
||||
|
||||
// 2. 上传素材到R2
|
||||
console.log('\n📤 Step 2: 上传素材到R2...');
|
||||
const refImageUrls = [];
|
||||
|
||||
for (const file of materialFiles) {
|
||||
try {
|
||||
const filePath = path.join(MATERIAL_DIR, file);
|
||||
const url = await uploadToR2(filePath);
|
||||
refImageUrls.push(url);
|
||||
} catch (error) {
|
||||
console.error(` 上传失败 ${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (refImageUrls.length === 0) {
|
||||
console.error('❌ 没有成功上传任何参考图片,退出');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(` 成功上传 ${refImageUrls.length} 张参考图`);
|
||||
|
||||
// 3. 构建SKU数据
|
||||
console.log('\n📝 Step 3: 构建SKU数据...');
|
||||
const sku = {
|
||||
sku_id: 'TD-EC-TEST-001',
|
||||
product_name: 'Cat Soft Cone Collar',
|
||||
product_name_cn: '伊丽莎白圈',
|
||||
brand: 'Touchdog',
|
||||
color: {
|
||||
name: 'Ice Blue',
|
||||
name_cn: '冰蓝色',
|
||||
series: 'Iridescent',
|
||||
hex: '#B0E0E6',
|
||||
edge_color: '#7FDBDB'
|
||||
},
|
||||
selling_points: [
|
||||
{
|
||||
key: 'lightweight',
|
||||
title_en: 'LIGHTER THAN AN EGG',
|
||||
title_cn: '极致轻盈',
|
||||
value: '65g',
|
||||
description_en: 'Cloud-light comfort at only 65g',
|
||||
visual_prompt: 'product shown next to an egg for size comparison'
|
||||
},
|
||||
{
|
||||
key: 'waterproof',
|
||||
title_en: 'WATERPROOF & EASY WIPE',
|
||||
title_cn: '防水易清洁',
|
||||
description_en: 'Waterproof PU fabric, stains wipe clean instantly',
|
||||
visual_prompt: 'water droplets beading on the surface'
|
||||
},
|
||||
{
|
||||
key: 'breathable',
|
||||
title_en: 'BREATHABLE COTTON LINING',
|
||||
title_cn: '舒适亲肤',
|
||||
description_en: 'Soft cotton inner layer, comfortable for extended wear',
|
||||
visual_prompt: 'close-up of soft cotton inner lining'
|
||||
},
|
||||
{
|
||||
key: 'adjustable',
|
||||
title_en: 'ADJUSTABLE STRAP',
|
||||
title_cn: '可调节',
|
||||
description_en: '3-second on/off, adjustable velcro',
|
||||
visual_prompt: 'velcro strap being adjusted'
|
||||
}
|
||||
],
|
||||
specs: {
|
||||
weight: '65g',
|
||||
depth_cm: '24.5',
|
||||
sizes: ['XS', 'S', 'M', 'L', 'XL'],
|
||||
materials: {
|
||||
outer: 'Durable Waterproof PU',
|
||||
inner: '95% Cotton + 5% Spandex'
|
||||
}
|
||||
},
|
||||
use_cases: [
|
||||
{ name_en: 'Postoperative Care', name_cn: '术后恢复' },
|
||||
{ name_en: 'Eye Drop Application', name_cn: '驱虫护理' },
|
||||
{ name_en: 'Nail Trimming', name_cn: '指甲修剪' },
|
||||
{ name_en: 'Grooming', name_cn: '美容护理' }
|
||||
],
|
||||
ref_images: refImageUrls
|
||||
};
|
||||
|
||||
console.log(' SKU数据准备完成');
|
||||
console.log(` 参考图: ${refImageUrls.length} 张`);
|
||||
|
||||
// 4. 调用生成API (只生成前3张测试)
|
||||
console.log('\n🎨 Step 4: 调用AI生图API...');
|
||||
console.log(' (为节省时间,仅生成3张测试图)\n');
|
||||
|
||||
// 直接调用图像生成API进行测试
|
||||
const testPrompts = [
|
||||
{
|
||||
id: 'Main_01',
|
||||
type: '场景首图+卖点摘要',
|
||||
prompt: `Professional Amazon main image for Touchdog Cat Soft Cone Collar, 1:1 square format.
|
||||
|
||||
COMPOSITION:
|
||||
- TOP 60%: A beautiful Ragdoll cat wearing the Ice Blue (#B0E0E6) soft cone collar in a cozy modern home interior with warm wood shelving. Natural soft lighting from window. Cat looks comfortable and relaxed. The cone collar is soft fabric with petal-like segments.
|
||||
|
||||
- BOTTOM 40%: Light blue (#B0E0E6) rounded banner with title "DESIGNED FOR COMFORTABLE RECOVERY" in navy blue text. Below: three small circular images showing:
|
||||
1. Product next to an egg showing 65g weight - "LIGHTER THAN AN EGG"
|
||||
2. Water droplets on surface - "WATERPROOF & EASY WIPE"
|
||||
3. Soft cotton lining detail - "BREATHABLE COTTON LINING"
|
||||
|
||||
PRODUCT REQUIREMENTS:
|
||||
- Soft cone collar with petal-like fabric segments, NOT rigid plastic
|
||||
- Color: Ice Blue (#B0E0E6) with teal edge binding (#7FDBDB)
|
||||
- Embroidered "TOUCHDOG®" logo visible on right petal (2-3 o'clock position)
|
||||
- Waterproof PU outer, soft cotton inner visible at edges
|
||||
|
||||
STYLE: Clean, professional Amazon listing photo. Warm home aesthetic. High-end pet product photography.
|
||||
|
||||
--ar 1:1`
|
||||
},
|
||||
{
|
||||
id: 'Main_02',
|
||||
type: '产品平铺+细节放大',
|
||||
prompt: `Professional Amazon product detail image for Touchdog Cat Soft Cone Collar, 1:1 square format.
|
||||
|
||||
COMPOSITION:
|
||||
- CENTER: Ice Blue (#B0E0E6) soft cone collar displayed flat from above on clean white background. Shows full petal-like segment structure.
|
||||
- Two circular detail callout bubbles with thin lines pointing to product:
|
||||
1. Left callout: "DURABLE WATERPROOF PU LAYER" - showing the smooth outer texture
|
||||
2. Right callout: "DOUBLE-LAYER COMFORT" - showing the soft ribbed edge binding in teal (#7FDBDB)
|
||||
|
||||
PRODUCT REQUIREMENTS:
|
||||
- Soft fabric cone with 8-10 petal segments radiating from center
|
||||
- Visible velcro closure strap
|
||||
- "TOUCHDOG®" embroidered logo on right side
|
||||
- Color: Ice Blue (#B0E0E6) main, teal binding
|
||||
|
||||
STYLE: Clean white background product photography. Professional Amazon listing style. Even studio lighting.
|
||||
|
||||
--ar 1:1`
|
||||
},
|
||||
{
|
||||
id: 'APlus_01',
|
||||
type: '品牌Banner',
|
||||
prompt: `Professional Amazon A+ brand banner image for Touchdog Cat Soft Cone Collar, landscape 970x600 format.
|
||||
|
||||
COMPOSITION:
|
||||
- FULL WIDTH: Warm modern living room scene with a beautiful white cat wearing the Ice Blue soft cone collar, standing gracefully on a light wood surface.
|
||||
- LEFT SIDE: Large stylized "TOUCHDOG" brand text in coral/pink color with playful font, paw print decorations
|
||||
- BELOW: "CAT SOFT CONE COLLAR" product name text
|
||||
|
||||
PRODUCT REQUIREMENTS:
|
||||
- Cat wearing Ice Blue (#B0E0E6) soft cone collar
|
||||
- Petal-like fabric segments visible
|
||||
- Collar looks soft and comfortable, not rigid
|
||||
|
||||
ENVIRONMENT:
|
||||
- Cozy modern home interior
|
||||
- Soft natural lighting
|
||||
- Warm neutral tones (beige, cream, light wood)
|
||||
- Professional lifestyle photography feel
|
||||
|
||||
BRAND ELEMENTS:
|
||||
- Touchdog logo with heart-shaped dog icon in red
|
||||
- "wow pretty" tagline
|
||||
|
||||
--ar 3:2`
|
||||
}
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const testPrompt of testPrompts) {
|
||||
console.log(` 生成 ${testPrompt.id}: ${testPrompt.type}...`);
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
model: 'gemini-3-pro-image-preview',
|
||||
prompt: testPrompt.prompt,
|
||||
n: 1,
|
||||
size: '1K',
|
||||
aspect_ratio: testPrompt.id.startsWith('APlus') ? '3:2' : '1:1',
|
||||
image_urls: refImageUrls
|
||||
};
|
||||
|
||||
const response = await axios.post(
|
||||
'https://api2img.shubiaobiao.com/v1/images/generations',
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${process.env.API_KEY}`
|
||||
},
|
||||
timeout: 180000
|
||||
}
|
||||
);
|
||||
|
||||
// 处理响应 - 可能是URL或base64
|
||||
const imageData = response.data.data?.[0];
|
||||
const outputPath = path.join(OUTPUT_DIR, `${sku.sku_id}-${testPrompt.id}.jpg`);
|
||||
|
||||
if (imageData?.b64_json) {
|
||||
// Base64格式 - 直接保存
|
||||
const imageBuffer = Buffer.from(imageData.b64_json, 'base64');
|
||||
fs.writeFileSync(outputPath, imageBuffer);
|
||||
console.log(` 保存成功: ${path.basename(outputPath)}`);
|
||||
|
||||
results.push({
|
||||
id: testPrompt.id,
|
||||
type: testPrompt.type,
|
||||
localPath: outputPath,
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
console.log(` ✓ ${testPrompt.id} 生成成功`);
|
||||
} else if (imageData?.url) {
|
||||
// URL格式 - 下载保存
|
||||
await downloadImage(imageData.url, outputPath);
|
||||
|
||||
results.push({
|
||||
id: testPrompt.id,
|
||||
type: testPrompt.type,
|
||||
url: imageData.url,
|
||||
localPath: outputPath,
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
console.log(` ✓ ${testPrompt.id} 生成成功`);
|
||||
} else {
|
||||
throw new Error('No image data in response');
|
||||
}
|
||||
|
||||
// 延迟避免限流
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ✗ ${testPrompt.id} 生成失败: ${error.message}`);
|
||||
results.push({
|
||||
id: testPrompt.id,
|
||||
type: testPrompt.type,
|
||||
status: 'failed',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 输出结果
|
||||
console.log('\n========================================');
|
||||
console.log('生成结果汇总');
|
||||
console.log('========================================');
|
||||
|
||||
const successCount = results.filter(r => r.status === 'success').length;
|
||||
console.log(`\n成功: ${successCount}/${results.length}`);
|
||||
|
||||
results.forEach(r => {
|
||||
const status = r.status === 'success' ? '✓' : '✗';
|
||||
console.log(` ${status} ${r.id} - ${r.type}`);
|
||||
if (r.localPath) {
|
||||
console.log(` 保存位置: ${r.localPath}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n📁 输出目录: ${OUTPUT_DIR}`);
|
||||
console.log('\n========================================\n');
|
||||
|
||||
// 保存结果JSON
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, 'generation-results.json'),
|
||||
JSON.stringify({ sku, results }, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
// 运行
|
||||
main().catch(console.error);
|
||||
|
||||
209
test-vision-extract.js
Normal file
209
test-vision-extract.js
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 测试Vision自动提取产品特征
|
||||
* 目标:验证Vision能否从素材图中自动提取"黄金产品描述"
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
// 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 = `vision-test-${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;
|
||||
}
|
||||
|
||||
// Vision提取产品特征的Prompt
|
||||
const VISION_EXTRACT_PROMPT = `Analyze this pet recovery cone product image in EXTREME detail.
|
||||
|
||||
You MUST extract and output the following information as structured JSON:
|
||||
|
||||
{
|
||||
"color": {
|
||||
"primary": "<hex code like #C5E8ED>",
|
||||
"name": "<descriptive name like 'ice blue', 'mint green'>",
|
||||
"secondary": "<any accent colors>"
|
||||
},
|
||||
"shape": {
|
||||
"type": "<flower/fan/cone/other>",
|
||||
"petal_count": <number of segments/petals>,
|
||||
"opening": "<C-shaped/full-circle/other>",
|
||||
"description": "<detailed shape description>"
|
||||
},
|
||||
"material": {
|
||||
"type": "<PU/fabric/plastic/other>",
|
||||
"finish": "<glossy/matte/satin>",
|
||||
"texture": "<smooth/quilted/ribbed>"
|
||||
},
|
||||
"edge_binding": {
|
||||
"color": "<color of inner neck edge>",
|
||||
"material": "<ribbed elastic/fabric/other>"
|
||||
},
|
||||
"closure": {
|
||||
"type": "<velcro/button/snap/other>",
|
||||
"color": "<white/matching/other>",
|
||||
"position": "<which petal or location>"
|
||||
},
|
||||
"logo": {
|
||||
"text": "<brand name if visible>",
|
||||
"style": "<embroidered/printed/tag>",
|
||||
"position": "<location on product>"
|
||||
},
|
||||
"unique_features": [
|
||||
"<list any distinctive features>"
|
||||
],
|
||||
"overall_description": "<2-3 sentence summary for image generation prompt>"
|
||||
}
|
||||
|
||||
Be EXTREMELY precise about colors and structural details. This will be used to generate consistent product images.`;
|
||||
|
||||
async function testVisionExtract(imagePath) {
|
||||
console.log('📤 上传图片到R2...');
|
||||
const imageUrl = await uploadToR2(imagePath);
|
||||
console.log(' URL:', imageUrl);
|
||||
|
||||
console.log('\n🔍 调用Vision API分析图片...');
|
||||
|
||||
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: 60000
|
||||
});
|
||||
|
||||
const content = response.data.data?.choices?.[0]?.message?.content ||
|
||||
response.data.data?.content ||
|
||||
response.data.choices?.[0]?.message?.content;
|
||||
|
||||
if (!content) {
|
||||
console.error('Vision响应为空');
|
||||
console.log('完整响应:', JSON.stringify(response.data, null, 2));
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('\n📝 Vision原始输出:\n');
|
||||
console.log(content);
|
||||
|
||||
// 尝试提取JSON
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
console.log('\n✅ 解析后的JSON:\n');
|
||||
console.log(JSON.stringify(parsed, null, 2));
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
console.log('\n⚠️ JSON解析失败,但有输出内容');
|
||||
return { raw: content };
|
||||
}
|
||||
}
|
||||
|
||||
return { raw: content };
|
||||
|
||||
} catch (error) {
|
||||
console.error('Vision API错误:', error.message);
|
||||
if (error.response) {
|
||||
console.error('响应数据:', error.response.data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const MATERIAL_DIR = path.join(__dirname, '素材/素材/已有的素材');
|
||||
const flatImagePath = path.join(MATERIAL_DIR, 'IMG_5683.png');
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('🧪 Vision产品特征提取测试');
|
||||
console.log('='.repeat(60));
|
||||
console.log('\n测试图片:', flatImagePath);
|
||||
|
||||
const result = await testVisionExtract(flatImagePath);
|
||||
|
||||
if (result) {
|
||||
// 保存结果
|
||||
const outputPath = path.join(__dirname, 'vision-extract-result.json');
|
||||
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
|
||||
console.log('\n💾 结果已保存到:', outputPath);
|
||||
|
||||
// 如果成功解析,生成Golden Description
|
||||
if (result.overall_description || result.shape) {
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('🌟 生成的Golden Product Description:');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const golden = generateGoldenDescription(result);
|
||||
console.log(golden);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'golden-description.txt'),
|
||||
golden
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateGoldenDescription(visionResult) {
|
||||
if (visionResult.raw) {
|
||||
return `[需要手动整理Vision输出]\n\n${visionResult.raw}`;
|
||||
}
|
||||
|
||||
const r = visionResult;
|
||||
|
||||
return `
|
||||
EXACT PRODUCT APPEARANCE (AUTO-EXTRACTED BY VISION):
|
||||
|
||||
- Shape: ${r.shape?.petal_count || '?'}-PETAL ${r.shape?.type?.toUpperCase() || 'FLOWER'} shape, ${r.shape?.opening || 'C-shaped'} opening
|
||||
- Color: ${r.color?.name?.toUpperCase() || 'UNKNOWN'} (${r.color?.primary || '#???'})
|
||||
- Material: ${r.material?.finish || 'Glossy'} ${r.material?.type || 'PU'} fabric with ${r.material?.texture || 'smooth'} texture
|
||||
- Edge binding: ${r.edge_binding?.color || 'TEAL'} ${r.edge_binding?.material || 'ribbed elastic'} around inner neck hole
|
||||
- Closure: ${r.closure?.color || 'White'} ${r.closure?.type || 'velcro'} on ${r.closure?.position || 'one petal end'}
|
||||
- Logo: "${r.logo?.text || 'TOUCHDOG'}" ${r.logo?.style || 'embroidered'} on ${r.logo?.position || 'one petal'}
|
||||
|
||||
UNIQUE FEATURES:
|
||||
${(r.unique_features || []).map(f => `- ${f}`).join('\n') || '- Visible stitching between petals\n- Soft but structured'}
|
||||
|
||||
SUMMARY FOR PROMPTS:
|
||||
${r.overall_description || 'Pet recovery cone with flower-petal design, soft waterproof material.'}
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- ❌ NO printed patterns or colorful fabric designs
|
||||
- ❌ NO hard plastic transparent cones
|
||||
- ❌ NO fully circular/closed shapes (must have C-opening)
|
||||
- ❌ NO random brand logos or text
|
||||
`.trim();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
240
test-vision-vs-manual.js
Normal file
240
test-vision-vs-manual.js
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 对比测试:Vision自动提取 vs 手写描述
|
||||
* 生成Main_02平铺图,对比两者效果
|
||||
*/
|
||||
|
||||
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_comparison');
|
||||
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 = `compare-${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',
|
||||
}));
|
||||
|
||||
return 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}`;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 方案A: 手写的Golden Description (V2版)
|
||||
// ========================================
|
||||
const MANUAL_GOLDEN_DESC = `
|
||||
EXACT PRODUCT APPEARANCE (MUST MATCH PRECISELY):
|
||||
- Shape: 8-PETAL FLOWER/FAN shape, C-shaped opening (like Pac-Man), NOT a full circle
|
||||
- Color: ICE BLUE / Light aqua blue (#C5E8ED approximate)
|
||||
- Material: Glossy waterproof PU fabric with visible stitching lines between petals
|
||||
- Edge binding: TEAL/TURQUOISE color binding around the inner neck hole
|
||||
- Closure: White velcro strap on one petal end
|
||||
- Logo: "TOUCHDOG®" embroidered in matching blue thread on one petal (small, subtle)
|
||||
- Texture: Smooth, slightly shiny, NOT matte, NOT cotton fabric
|
||||
- Structure: Soft but structured, maintains petal shape, NOT floppy
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- ❌ NO printed patterns or colorful fabric designs
|
||||
- ❌ NO hard plastic transparent cones
|
||||
- ❌ NO fully circular/closed shapes
|
||||
- ❌ NO matte cotton or fleece textures
|
||||
- ❌ NO random brand logos or text
|
||||
`;
|
||||
|
||||
// ========================================
|
||||
// 方案B: Vision自动提取的Golden Description
|
||||
// ========================================
|
||||
const VISION_GOLDEN_DESC = `
|
||||
EXACT PRODUCT APPEARANCE (AUTO-EXTRACTED BY VISION):
|
||||
- Shape: 7-PETAL FLOWER/FAN shape, C-shaped opening
|
||||
- Color: PASTEL ICE BLUE (#C3E6E8)
|
||||
- Material: soft matte/satin synthetic fabric (likely water-resistant polyester/nylon) with smooth, padded/quilted panels texture
|
||||
- Edge binding: Mint Green (slightly more saturated than body) ribbed knit elastic fabric around inner neck hole
|
||||
- Closure: white velcro (hook and loop) on large rectangular strip covering the entire bottom-left terminal segment
|
||||
- Logo: "TOUCHDOG®" embroidered on centered on the middle-right segment
|
||||
|
||||
UNIQUE FEATURES:
|
||||
- Scalloped outer edge resembling flower petals
|
||||
- Soft ribbed knit neckline for comfort
|
||||
- Radial stitching lines creating distinct padded zones
|
||||
- Large surface area velcro for adjustable sizing
|
||||
|
||||
CRITICAL PROHIBITIONS:
|
||||
- ❌ NO printed patterns or colorful fabric designs
|
||||
- ❌ NO hard plastic transparent cones
|
||||
- ❌ NO fully circular/closed shapes (must have C-opening)
|
||||
- ❌ NO random brand logos or text
|
||||
`;
|
||||
|
||||
// Main_02 平铺图 Prompt模板
|
||||
function createMain02Prompt(goldenDesc) {
|
||||
return `
|
||||
[PRODUCT FLAT LAY PHOTO - WHITE BACKGROUND]
|
||||
|
||||
${goldenDesc}
|
||||
|
||||
SCENE REQUIREMENTS:
|
||||
- Product flat lay on PURE WHITE background (#FFFFFF)
|
||||
- Shot from directly above (bird's eye view / top-down angle)
|
||||
- Show the full C-shaped opening clearly (gap between velcro ends visible)
|
||||
- All petals/segments fully visible and spread out
|
||||
- Clean studio lighting, MINIMAL shadows
|
||||
- Product fills 70-80% of frame
|
||||
- Professional Amazon product photography style
|
||||
|
||||
OUTPUT: High quality 1:1 aspect ratio product photo, 8K resolution
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// 生图任务提交
|
||||
async function submitImageTask(prompt, refImageUrl) {
|
||||
const payload = {
|
||||
key: API_KEY,
|
||||
prompt: prompt,
|
||||
img_url: refImageUrl,
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K'
|
||||
};
|
||||
|
||||
const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload);
|
||||
const taskId = response.data.data?.id;
|
||||
|
||||
if (!taskId) {
|
||||
throw new Error('No task ID returned');
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
// 轮询图片结果
|
||||
async function pollImageResult(taskId) {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
const response = await axios.get(`${API_BASE}/img/drawDetail`, {
|
||||
params: { key: API_KEY, id: taskId }
|
||||
});
|
||||
|
||||
const data = response.data.data;
|
||||
|
||||
if (data && data.status === 2 && data.image_url) {
|
||||
return data.image_url;
|
||||
} else if (data && data.status === 3) {
|
||||
throw new Error('Generation failed: ' + (data.fail_reason || 'Unknown'));
|
||||
}
|
||||
|
||||
process.stdout.write('.');
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
attempts++;
|
||||
}
|
||||
throw new Error('Timeout waiting for image');
|
||||
}
|
||||
|
||||
// 生成单张图
|
||||
async function generateImage(name, prompt, refUrl) {
|
||||
console.log(`\n🎨 生成: ${name}`);
|
||||
console.log(' 提交任务...');
|
||||
|
||||
const taskId = await submitImageTask(prompt, refUrl);
|
||||
console.log(` Task ID: ${taskId}`);
|
||||
|
||||
const imageUrl = await pollImageResult(taskId);
|
||||
console.log('\n ✓ 生成成功');
|
||||
|
||||
// 下载保存
|
||||
const imgRes = await axios.get(imageUrl, { responseType: 'arraybuffer' });
|
||||
const outputPath = path.join(OUTPUT_DIR, `${name}.jpg`);
|
||||
fs.writeFileSync(outputPath, imgRes.data);
|
||||
console.log(` ✓ 保存: ${outputPath}`);
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('🔬 Vision提取 vs 手写描述 对比测试');
|
||||
console.log('='.repeat(60));
|
||||
|
||||
// 上传参考图
|
||||
console.log('\n📤 上传参考图...');
|
||||
const flatImgPath = path.join(MATERIAL_DIR, 'IMG_5683.png');
|
||||
const refUrl = await uploadToR2(flatImgPath);
|
||||
console.log(' 参考图URL:', refUrl);
|
||||
|
||||
// 生成两个版本的Main_02
|
||||
const results = [];
|
||||
|
||||
// 版本A: 手写描述
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📝 版本A: 手写Golden Description');
|
||||
console.log('='.repeat(60));
|
||||
const promptA = createMain02Prompt(MANUAL_GOLDEN_DESC);
|
||||
console.log('Prompt预览:\n', promptA.substring(0, 500) + '...');
|
||||
|
||||
try {
|
||||
const pathA = await generateImage('Main_02_Manual', promptA, refUrl);
|
||||
results.push({ name: 'Manual', path: pathA, success: true });
|
||||
} catch (e) {
|
||||
console.error('版本A失败:', e.message);
|
||||
results.push({ name: 'Manual', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// 版本B: Vision提取
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('🤖 版本B: Vision自动提取Golden Description');
|
||||
console.log('='.repeat(60));
|
||||
const promptB = createMain02Prompt(VISION_GOLDEN_DESC);
|
||||
console.log('Prompt预览:\n', promptB.substring(0, 500) + '...');
|
||||
|
||||
try {
|
||||
const pathB = await generateImage('Main_02_Vision', promptB, refUrl);
|
||||
results.push({ name: 'Vision', path: pathB, success: true });
|
||||
} catch (e) {
|
||||
console.error('版本B失败:', e.message);
|
||||
results.push({ name: 'Vision', success: false, error: e.message });
|
||||
}
|
||||
|
||||
// 总结
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📊 对比结果');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`输出目录: ${OUTPUT_DIR}`);
|
||||
console.log('\n生成结果:');
|
||||
results.forEach(r => {
|
||||
if (r.success) {
|
||||
console.log(` ✅ ${r.name}: ${r.path}`);
|
||||
} else {
|
||||
console.log(` ❌ ${r.name}: ${r.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\n💡 请手动对比两张图片,评估Vision自动提取的效果是否达标');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
|
||||
31
test_upload.js
Normal file
31
test_upload.js
Normal file
@@ -0,0 +1,31 @@
|
||||
require('dotenv').config();
|
||||
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
||||
|
||||
const client = 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 () => {
|
||||
try {
|
||||
console.log('Attempting upload to bucket:', process.env.R2_BUCKET_NAME);
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: process.env.R2_BUCKET_NAME,
|
||||
Key: 'test-file.txt',
|
||||
Body: 'Hello R2',
|
||||
ContentType: 'text/plain',
|
||||
});
|
||||
await client.send(command);
|
||||
console.log('Upload successful!');
|
||||
} catch (err) {
|
||||
console.error('Upload Error Details:');
|
||||
console.error('Code:', err.Code);
|
||||
console.error('Message:', err.message);
|
||||
console.error('Full Error:', err);
|
||||
}
|
||||
})();
|
||||
39
vision-extract-result.json
Normal file
39
vision-extract-result.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"color": {
|
||||
"primary": "#C3E6E8",
|
||||
"name": "Pastel Ice Blue",
|
||||
"secondary": "#FFFFFF (Velcro), #98DBCF (Neck Ribbing)"
|
||||
},
|
||||
"shape": {
|
||||
"type": "flower/fan",
|
||||
"petal_count": 7,
|
||||
"opening": "C-shaped",
|
||||
"description": "A flat, semi-circular fan shape composed of padded segments with a scalloped outer edge, designed to wrap into a cone."
|
||||
},
|
||||
"material": {
|
||||
"type": "synthetic fabric (likely water-resistant polyester/nylon)",
|
||||
"finish": "soft matte/satin",
|
||||
"texture": "smooth, padded/quilted panels"
|
||||
},
|
||||
"edge_binding": {
|
||||
"color": "Mint Green (slightly more saturated than body)",
|
||||
"material": "ribbed knit elastic fabric"
|
||||
},
|
||||
"closure": {
|
||||
"type": "velcro (hook and loop)",
|
||||
"color": "white",
|
||||
"position": "large rectangular strip covering the entire bottom-left terminal segment"
|
||||
},
|
||||
"logo": {
|
||||
"text": "TOUCHDOG®",
|
||||
"style": "embroidered",
|
||||
"position": "centered on the middle-right segment"
|
||||
},
|
||||
"unique_features": [
|
||||
"Scalloped outer edge resembling flower petals",
|
||||
"Soft ribbed knit neckline for comfort",
|
||||
"Radial stitching lines creating distinct padded zones",
|
||||
"Large surface area velcro for adjustable sizing"
|
||||
],
|
||||
"overall_description": "A soft, padded pet recovery collar in pastel ice blue with a scalloped, flower-like shape. It features a comfortable mint-green ribbed knit neck opening, sectioned padding for flexibility, and a prominent white velcro closure strip."
|
||||
}
|
||||
Reference in New Issue
Block a user