/** * POC Workflow Carrier: 登机包 P图工作流 (V2) * 基于 V6 优化逻辑,针对 "Touchdog Pioneer Light Luxury Pet Carrier" 定制 * 更新:使用用户提供的精确产品信息 (US English) */ 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'; 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 = `carrier-v2-${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: '4K' }; if (refImageUrl) payload.img_url = refImageUrl; const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload, { timeout: 60000 }); // 增加超时时间到60s,因为4K生成慢 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 构建器 // ============================================================ 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 product matches the reference image EXACTLY in: - Color (Chanel Brown / Chocolate Brown) - Texture (Premium Faux Leather with sheen) - Shape (Rectangular Carrier) - Brand details (Silver hardware, TOUCHDOG logo) `.trim(); } // ============================================================ // 7张 Main Images 生成逻辑 // ============================================================ async function generateMain01(materials, outputDir) { console.log('\n🎨 [Main_01] 场景首图+卖点'); const buffer = fs.readFileSync(materials.mainImage); const url = await uploadToR2(buffer, 'main-01.jpg'); const prompt = buildEditPrompt({ subjectDescription: `A luxury Chanel Brown pet carrier bag (TOUCHDOG Pioneer) with premium faux leather texture.`, preserveElements: [ `The bag MUST remain Chanel Brown with its specific glossy texture`, `Silver metal clasps and TOUCHDOG logo plate`, `Shape and structure must be preserved exactly` ], editTasks: [ `Place bag in a premium airport VIP lounge setting (softly blurred)`, `Add "AIRLINE APPROVED" gold/silver stamp icon at top left`, `Add 3 feature bubbles at bottom: "FITS UNDER SEAT", "PREMIUM FAUX LEATHER", "VENTILATED DESIGN"`, `Ensure lighting highlights the "light-luxury sheen" of the material` ], layoutDescription: `Center: Product; Background: High-end travel context; Bottom: Feature icons`, styleGuide: `Light Luxury aesthetic. Sophisticated and elegant.`, aspectRatio: '1:1 square' }); const result = await generateAIImage(prompt, url, '1:1'); await imageProcessor.saveImage(result, path.join(outputDir, 'Main_01.jpg')); fs.writeFileSync(path.join(outputDir, 'Main_01_prompt.txt'), prompt); return true; } async function generateMain02(materials, outputDir) { console.log('\n🎨 [Main_02] 侧面透气窗展示'); const buffer = fs.readFileSync(materials.sideImage); const url = await uploadToR2(buffer, 'main-02.jpg'); const prompt = buildEditPrompt({ subjectDescription: `Side view of the carrier showing mesh ventilation and dual 2.5cm vent holes.`, preserveElements: [ `The mesh window and vent hole details`, `Brown leather texture and silver hardware` ], editTasks: [ `Add airflow arrows (light blue/white) flowing into the mesh and vent holes`, `Add text "360° BREATHABLE COMFORT"`, `Add callout for "PRIVACY CURTAINS" pointing to the roll-up flap`, `Background: Clean, bright studio setting`, `Show a small dog peeking through the mesh` ], layoutDescription: `Side view focus. Graphic arrows indicating airflow.`, styleGuide: `Technical yet premium. Emphasize "First-class cabin" breathability.`, aspectRatio: '1:1 square' }); const result = await generateAIImage(prompt, url, '1:1'); await imageProcessor.saveImage(result, path.join(outputDir, 'Main_02.jpg')); fs.writeFileSync(path.join(outputDir, 'Main_02_prompt.txt'), prompt); return true; } async function generateMain03(materials, outputDir) { console.log('\n🎨 [Main_03] 顶部开口与收纳'); const buffer = fs.readFileSync(materials.topImage || materials.mainImage); const url = await uploadToR2(buffer, 'main-03.jpg'); const prompt = buildEditPrompt({ subjectDescription: `Top/Front view showing the flap pockets and magnetic closures.`, preserveElements: [ `Pocket structure and magnetic buckle details`, `Leather texture` ], editTasks: [ `Show the front flap pocket slightly open with pet treats/toys visible inside`, `Add text label "THOUGHTFUL STORAGE"`, `Add callout "MAGNETIC CLOSURE" pointing to the buckle`, `Background: Soft neutral surface`, `Add "SECURE & STABLE" text` ], layoutDescription: `Focus on storage features and security.`, styleGuide: `Functional elegance.`, aspectRatio: '1:1 square' }); const result = await generateAIImage(prompt, url, '1:1'); await imageProcessor.saveImage(result, path.join(outputDir, 'Main_03.jpg')); fs.writeFileSync(path.join(outputDir, 'Main_03_prompt.txt'), prompt); return true; } async function generateMain04(materials, outputDir) { console.log('\n🎨 [Main_04] 材质细节与做工'); const buffer = fs.readFileSync(materials.detailImage); const url = await uploadToR2(buffer, 'main-04.jpg'); const prompt = buildEditPrompt({ subjectDescription: `Close-up of the premium faux leather and silver metal clasps.`, preserveElements: [ `Silver metal clasps MUST remain shiny silver`, `Smooth water-shedding texture`, `Stitching quality` ], editTasks: [ `Enhance lighting to show the "water-shedding" capability (maybe a water droplet effect)`, `Add magnifying glass effect on material`, `Label 1: "WATER-SHEDDING FAUX LEATHER"`, `Label 2: "METICULOUS CRAFTSMANSHIP"`, `Background: Blurred luxury interior` ], layoutDescription: `Extreme close-up macro shot style.`, styleGuide: `High-end fashion photography style. Focus on texture and sheen.`, aspectRatio: '1:1 square' }); const result = await generateAIImage(prompt, url, '1:1'); await imageProcessor.saveImage(result, path.join(outputDir, 'Main_04.jpg')); fs.writeFileSync(path.join(outputDir, 'Main_04_prompt.txt'), prompt); return true; } async function generateMain05(materials, outputDir) { console.log('\n🎨 [Main_05] 尺寸图'); const buffer = fs.readFileSync(materials.mainImage); const url = await uploadToR2(buffer, 'main-05.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The carrier bag isolated on plain background.`, preserveElements: [`Bag shape and proportions`], editTasks: [ `Place bag on pure white background`, `Add precise dimension lines: Length 15.35", Width 6.1", Height 8.27"`, `Add text "TSA COMPLIANT" and "FITS UNDER SEAT"`, `Add weight capacity: "0.56 KG Lightweight Design"` ], layoutDescription: `Clean product shot with technical dimension overlays.`, styleGuide: `Technical, informative, clean.`, aspectRatio: '1:1 square' }); const result = await generateAIImage(prompt, url, '1:1'); await imageProcessor.saveImage(result, path.join(outputDir, 'Main_05.jpg')); fs.writeFileSync(path.join(outputDir, 'Main_05_prompt.txt'), prompt); return true; } async function generateMain06(materials, outputDir) { console.log('\n🎨 [Main_06] 旅行场景 (拉杆箱)'); const buffer = fs.readFileSync(materials.mainImage); const url = await uploadToR2(buffer, 'main-06.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The carrier bag sitting on top of a suitcase.`, preserveElements: [`Bag appearance`], editTasks: [ `Composite the bag sitting securely on top of a rolling suitcase (simulating luggage sleeve use)`, `Background: Busy airport terminal`, `Add text "HASSLE-FREE TRAVEL"`, `Add callout "LUGGAGE SLEEVE COMPATIBLE"` ], layoutDescription: `Lifestyle action shot.`, styleGuide: `Travel lifestyle. Smooth boarding experience.`, aspectRatio: '1:1 square' }); const result = await generateAIImage(prompt, url, '1:1'); await imageProcessor.saveImage(result, path.join(outputDir, 'Main_06.jpg')); fs.writeFileSync(path.join(outputDir, 'Main_06_prompt.txt'), prompt); return true; } async function generateMain07(materials, outputDir) { console.log('\n🎨 [Main_07] 模特/宠物佩戴图'); const buffer = fs.readFileSync(materials.mainImage); const url = await uploadToR2(buffer, 'main-07.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The carrier bag being carried by a woman.`, preserveElements: [`Bag appearance`], editTasks: [ `Show a stylish woman carrying the bag on her shoulder`, `A small dog head peeking out happily`, `Setting: Outdoor city street or park`, `Text overlay: "TRAVEL IN ELEGANCE"`, `Vibe: "Light-luxury statement"` ], layoutDescription: `Fashion lifestyle shot.`, styleGuide: `Fashion-forward, stylish. Chanel Brown color palette.`, aspectRatio: '1:1 square' }); const result = await generateAIImage(prompt, url, '1:1'); await imageProcessor.saveImage(result, path.join(outputDir, 'Main_07.jpg')); fs.writeFileSync(path.join(outputDir, 'Main_07_prompt.txt'), prompt); return true; } // ============================================================ // 7张 A+ Images 生成逻辑 (3:2 Landscape) // ============================================================ async function generateAPlus01(materials, outputDir) { console.log('\n🎨 [APlus_01] 品牌横幅'); const buffer = fs.readFileSync(materials.mainImage); const url = await uploadToR2(buffer, 'aplus-01.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The carrier bag in a premium setting.`, preserveElements: [`Bag appearance`], editTasks: [ `Create wide banner (3:2)`, `Left: Elegant brand text "TOUCHDOG" and "PIONEER LIGHT LUXURY SERIES"`, `Right: The bag placed on a velvet armchair or luxury car seat`, `Mood: Sophisticated, comfortable, high-end` ], layoutDescription: `Wide brand header.`, styleGuide: `Luxury brand aesthetic. Chanel Brown tones.`, aspectRatio: '3:2 landscape' }); const result = await generateAIImage(prompt, url, '3:2'); await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_01.jpg')); fs.writeFileSync(path.join(outputDir, 'APlus_01_prompt.txt'), prompt); return true; } async function generateAPlus02(materials, outputDir) { console.log('\n🎨 [APlus_02] 核心卖点: 航空认证'); const buffer = fs.readFileSync(materials.mainImage); const url = await uploadToR2(buffer, 'aplus-02.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The carrier bag under an airplane seat.`, preserveElements: [`Bag structure`], editTasks: [ `Show the bag fitting perfectly under an airplane seat`, `Text overlay: "AIRLINE APPROVED FOR HASSLE-FREE TRAVEL"`, `Bullet points: "Height < 25cm", "Fits Most Airlines", "Smooth Boarding"`, `Background: Airplane cabin interior` ], layoutDescription: `Contextual usage shot.`, styleGuide: `Professional travel.`, aspectRatio: '3:2 landscape' }); const result = await generateAIImage(prompt, url, '3:2'); await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_02.jpg')); fs.writeFileSync(path.join(outputDir, 'APlus_02_prompt.txt'), prompt); return true; } async function generateAPlus03(materials, outputDir) { console.log('\n🎨 [APlus_03] 材质细节: 奢华体验'); const buffer = fs.readFileSync(materials.detailImage); const url = await uploadToR2(buffer, 'aplus-03.jpg'); const prompt = buildEditPrompt({ subjectDescription: `Close-up of the smooth faux leather texture.`, preserveElements: [`Leather texture and silver hardware`], editTasks: [ `Split screen or collage layout`, `Image 1: Close up of water-shedding surface`, `Image 2: Close up of silver buckle details`, `Text: "LUXURIOUS FEEL & STYLISH DESIGN"`, `Caption: "Premium textured finish with light-luxury sheen"` ], layoutDescription: `Detail oriented layout.`, styleGuide: `High-end fashion detail.`, aspectRatio: '3:2 landscape' }); const result = await generateAIImage(prompt, url, '3:2'); await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_03.jpg')); fs.writeFileSync(path.join(outputDir, 'APlus_03_prompt.txt'), prompt); return true; } async function generateAPlus04(materials, outputDir) { console.log('\n🎨 [APlus_04] 透气与舒适 (头等舱体验)'); const buffer = fs.readFileSync(materials.sideImage); const url = await uploadToR2(buffer, 'aplus-04.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The carrier showing ventilation features.`, preserveElements: [`Mesh window and vent holes`], editTasks: [ `Show interior view or cutaway showing "First-class cabin" space`, `Highlight "Dual 2.5cm Vent Holes" and "Light-blocking Curtains"`, `Text: "360° BREATHABLE COMFORT"`, `Show happy pet inside enjoying the airflow` ], layoutDescription: `Comfort focused.`, styleGuide: `Airy, comfortable, safe.`, aspectRatio: '3:2 landscape' }); const result = await generateAIImage(prompt, url, '3:2'); await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_04.jpg')); fs.writeFileSync(path.join(outputDir, 'APlus_04_prompt.txt'), prompt); return true; } async function generateAPlus05(materials, outputDir) { console.log('\n🎨 [APlus_05] 安全与稳固'); const buffer = fs.readFileSync(materials.mainImage); const url = await uploadToR2(buffer, 'aplus-05.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The carrier bag structure.`, preserveElements: [`Bag shape`], editTasks: [ `Visual diagram showing "Internal Sturdy Base Board" (prevents sagging)`, `Callout for "Anti-escape Safety Buckle"`, `Text: "SECURE, STABLE & METICULOUSLY CRAFTED"`, `Show diagram of composite fiberboard support` ], layoutDescription: `Technical breakdown/X-ray view style.`, styleGuide: `Safe, reliable, sturdy.`, aspectRatio: '3:2 landscape' }); const result = await generateAIImage(prompt, url, '3:2'); await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_05.jpg')); fs.writeFileSync(path.join(outputDir, 'APlus_05_prompt.txt'), prompt); return true; } async function generateAPlus06(materials, outputDir) { console.log('\n🎨 [APlus_06] 收纳与便捷'); const buffer = fs.readFileSync(materials.topImage || materials.mainImage); const url = await uploadToR2(buffer, 'aplus-06.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The carrier bag pockets.`, preserveElements: [`Pocket details`], editTasks: [ `Show items being organized into pockets (documents, treats, toys)`, `Text: "THOUGHTFUL STORAGE & PRACTICAL CONVENIENCE"`, `Highlight "Front Flap Pocket" and "Rear Slip Pocket"`, `Lifestyle context: preparing for travel` ], layoutDescription: `Organization demonstration.`, styleGuide: `Organized, clean, helpful.`, aspectRatio: '3:2 landscape' }); const result = await generateAIImage(prompt, url, '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 generateAPlus07(materials, outputDir) { console.log('\n🎨 [APlus_07] 对比图'); const buffer = fs.readFileSync(materials.mainImage); const url = await uploadToR2(buffer, 'aplus-07.jpg'); const prompt = buildEditPrompt({ subjectDescription: `The Touchdog carrier.`, preserveElements: [`Bag appearance`], editTasks: [ `Left (Touchdog): Stylish, sturdy, airline approved (Checkmark)`, `Right (Generic): Saggy, ugly, uncomfortable (X mark)`, `Text: "WHY CHOOSE TOUCHDOG PIONEER?"`, `Comparison points: "Anti-Sag Base vs Flimsy", "Breathable vs Stuffy", "Luxury PU vs Cheap Cloth"` ], layoutDescription: `Side-by-side comparison.`, styleGuide: `Clear contrast. Superior quality emphasis.`, aspectRatio: '3:2 landscape' }); const result = await generateAIImage(prompt, url, '3:2'); await imageProcessor.saveImage(result, path.join(outputDir, 'APlus_07.jpg')); fs.writeFileSync(path.join(outputDir, 'APlus_07_prompt.txt'), prompt); return true; } async function main() { const materialDir = path.join(__dirname, '素材/登机包'); // 识别素材 const materials = { // 换成干净的图作为主素材 mainImage: path.join(materialDir, '1023_0201.JPG'), // 侧面图保持不变 sideImage: path.join(materialDir, 'P1191451.JPG'), topImage: path.join(materialDir, '1023_0201.JPG'), detailImage: path.join(materialDir, '1023_0151.JPG') }; // 检查文件 for (const [key, val] of Object.entries(materials)) { if (!fs.existsSync(val)) { console.log(`⚠️ 警告: ${key} 文件不存在 (${val}),尝试使用主图替代`); materials[key] = materials.mainImage; } } const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); const outputDir = path.join(__dirname, `output_carrier_v3_4K_${timestamp}`); if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); console.log('🚀 开始生成登机包图片 (V2 - US English)...'); console.log(' 输出目录:', outputDir); const tasks = [ // Main Images // { id: 'Main_01', fn: generateMain01 }, // { id: 'Main_02', fn: generateMain02 }, // { id: 'Main_03', fn: generateMain03 }, // { id: 'Main_04', fn: generateMain04 }, // { id: 'Main_05', fn: generateMain05 }, { id: 'Main_06', fn: generateMain06 }, { id: 'Main_07', fn: generateMain07 }, // A+ Images // { id: 'APlus_01', fn: generateAPlus01 }, // { id: 'APlus_02', fn: generateAPlus02 }, // { id: 'APlus_03', fn: generateAPlus03 }, // { id: 'APlus_04', fn: generateAPlus04 }, // { id: 'APlus_05', fn: generateAPlus05 }, // { id: 'APlus_06', fn: generateAPlus06 }, // { id: 'APlus_07', fn: generateAPlus07 }, ]; for (const task of tasks) { try { await task.fn(materials, outputDir); console.log(`✅ ${task.id} 完成`); } catch (e) { console.error(`❌ ${task.id} 失败: ${e.message}`); } } } main().catch(console.error);