Files
amz-pic-flow/poc-workflow-v5.js

688 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);