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