/** * 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 = ` "DURABLE WATERPROOF PU LAYER" `; 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(` `); 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(` `); const detail1WithBorder = await sharp(detail1Circle) .composite([{ input: borderSvg }]) .toBuffer(); const detail2WithBorder = await sharp(detail2Circle) .composite([{ input: borderSvg }]) .toBuffer(); // 创建标签文字 const label1Svg = Buffer.from(` DURABLE WATERPROOF PU LAYER `); const label2Svg = Buffer.from(` DOUBLE-LAYER COMFORT `); // 合成所有图层 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(` PRODUCT SIZE `); // 尺寸表 const tableSvg = Buffer.from(` 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 NOTE: ALWAYS MEASURE YOUR PET'S NECK BEFORE SELECTING A SIZE `); 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);