diff --git a/fix-aplus03.js b/fix-aplus03.js new file mode 100644 index 0000000..6a290e2 --- /dev/null +++ b/fix-aplus03.js @@ -0,0 +1,94 @@ +/** + * 修复 APlus_03 的 4K 还原脚本 + */ +require('dotenv').config(); +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +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 = `fix-aplus03-${Date.now()}-${filename}`; + await r2Client.send(new PutObjectCommand({ + Bucket: process.env.R2_BUCKET_NAME || 'ai-flow', + Key: fileName, + Body: buffer, + ContentType: 'image/jpeg', + })); + return `${process.env.R2_PUBLIC_DOMAIN}/${fileName}`; +} + +async function main() { + const v2Path = path.join(__dirname, 'output_carrier_v2_2025-12-27/APlus_03.jpg'); + const outputDir = path.join(__dirname, 'output_carrier_v3_4K_Upscale_2025-12-29T02-32-52'); // 覆盖到当前的 4K 目录 + + console.log('🛠️ 正在修复 APlus_03.jpg...'); + + const prompt = ` +[FIDELITY ENHANCEMENT - 4K] +REFERENCE: An Amazon A+ detail module. +DESCRIPTION: +- Two panels showing Chanel Brown quilted leather. +- Left panel has water splashes and "TouchDog" silver logo in small script font. +- Right panel shows a silver buckle. +- Bottom has text "LUXURIOUS FEEL & STYLISH DESIGN". + +STRICT REQUIREMENTS: +1. 1:1 PIXEL FIDELITY: Keep the layout, logo position (bottom-left of left panel), and fonts EXACTLY as shown in the reference image. +2. NO REDESIGN: Do not change the logo to capital letters or move it to the center. +3. QUALITY ONLY: Just enhance the clarity, leather texture sharpness, and metal reflections. +`.trim(); + + const imgBuffer = fs.readFileSync(v2Path); + const imgUrl = await uploadToR2(imgBuffer, 'APlus_03.jpg'); + + const payload = { + key: API_KEY, + prompt: prompt, + img_url: imgUrl, + aspectRatio: '3:2', + imageSize: '4K' + }; + + const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload); + const taskId = response.data.data?.id || response.data.id; + console.log(` Task ID: ${taskId}`); + + let resultUrl; + let attempts = 0; + while (attempts < 60) { + const res = await axios.get(`${API_BASE}/img/drawDetail`, { params: { key: API_KEY, id: taskId } }); + if (res.data.data?.status === 2) { + resultUrl = res.data.data.image_url; + break; + } + process.stdout.write('.'); + await new Promise(r => setTimeout(r, 3000)); + attempts++; + } + + if (resultUrl) { + const res = await axios.get(resultUrl, { responseType: 'arraybuffer' }); + fs.writeFileSync(path.join(outputDir, 'APlus_03.jpg'), Buffer.from(res.data)); + console.log('\n✅ APlus_03.jpg 修复完成并存入 v3 目录'); + } +} + +main().catch(console.error); + + + diff --git a/poc-workflow-carrier-v3.js b/poc-workflow-carrier-v3.js new file mode 100644 index 0000000..e2abb2c --- /dev/null +++ b/poc-workflow-carrier-v3.js @@ -0,0 +1,214 @@ +/** + * POC Workflow Carrier V3: 高保真、零推理工作流 + * + * 核心策略: + * 1. 强力约束:锁定五金细节、文字大小写、精确尺寸数据。 + * 2. 视觉统一:强制 AI 使用统一背景颜色、字体风格和标注样式。 + * 3. 素材绑定:每张图绑定最准确的源文件,减少 AI “脑补”。 + */ + +require('dotenv').config(); +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +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, +}); + +// ============================================================ +// 统一视觉标准 (Global UI Standards) +// ============================================================ +const UI_STANDARDS = { + font: "Modern geometric sans-serif font (OPPOSans style), clean and highly readable", + background: "Clean unified soft beige gradient background (#F5EDE4 to #FFFFFF)", + calloutStyle: "Minimalist thin solid lines with small dots, professional and tidy", + brandText: "touchdog (ALWAYS LOWERCASE, no exceptions)", + dimensions: "L: 15.35\" x W: 6.1\" x H: 8.27\" (Height: 28 cm)" +}; + +// ============================================================ +// 工具函数 +// ============================================================ + +async function uploadToR2(buffer, filename) { + const fileName = `carrier-v3-${Date.now()}-${filename}`; + await r2Client.send(new PutObjectCommand({ + Bucket: process.env.R2_BUCKET_NAME || 'ai-flow', + Key: fileName, + Body: buffer, + ContentType: 'image/jpeg', + })); + return `${process.env.R2_PUBLIC_DOMAIN}/${fileName}`; +} + +async function submitTask(prompt, imgUrl, aspectRatio) { + const payload = { + key: API_KEY, + prompt: prompt, + img_url: imgUrl, + aspectRatio: aspectRatio, + imageSize: '4K' // 维持 4K 高清 + }; + const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload, { timeout: 60000 }); + return response.data.data?.id || response.data.id; +} + +async function pollResult(taskId) { + let attempts = 0; + while (attempts < 100) { + const res = await axios.get(`${API_BASE}/img/drawDetail`, { params: { key: API_KEY, id: taskId } }); + if (res.data.data?.status === 2) return res.data.data.image_url; + if (res.data.data?.status === 3) throw new Error('Failed: ' + res.data.data.fail_reason); + process.stdout.write('.'); + await new Promise(r => setTimeout(r, 2000)); + attempts++; + } + throw new Error('Timeout'); +} + +// ============================================================ +// V3 高保真 Prompt 构建器 +// ============================================================ + +function buildFidelityPrompt(config) { + return ` +[IMAGE EDITING TASK - STRICT FIDELITY MODE] + +1. REFERENCE ANALYSIS: +Source product is a luxury pet carrier. +KEY DETAIL: The front pockets use SILVER TUCK-LOCK CLASPS (round/oval metal locks), NOT prong buckles. +KEY DETAIL: Brand logo is "touchdog" in LOWERCASE. + +2. CRITICAL PRESERVATION (DO NOT MODIFY PRODUCT): +- KEEP the exact hardware style: Silver metal tuck-lock clasps. +- KEEP the "touchdog" logo in LOWERCASE. +- KEEP the diamond quilted texture and mocha brown color (#59483D). +- DO NOT add side pockets if they are not in the reference. + +3. EDITING TASKS: +${config.tasks.join('\n')} + +4. UI & STYLE STANDARDS: +- Background: ${UI_STANDARDS.background} +- Font: ${UI_STANDARDS.font} +- All text must be in English. +- Use ${UI_STANDARDS.calloutStyle} for any annotations. + +5. TEXT CONTENT: +${config.textContent.join('\n')} + +OUTPUT: 4K High Definition, Professional Amazon US Listing Style. +`.trim(); +} + +// ============================================================ +// 任务执行逻辑 +// ============================================================ + +async function runV3() { + const materialDir = path.join(__dirname, '素材/登机包'); + const outputDir = path.join(__dirname, `output_carrier_v3_Final_${new Date().toISOString().slice(0, 10)}`); + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); + + console.log('🚀 启动 V3 高保真生成工作流 (对齐客户反馈)...'); + + const tasks = [ + { + id: 'APlus_02', + source: 'P1191464.JPG', // 侧面结构图 + config: { + tasks: [ + "Place the bag under an airplane seat in a high-end cabin setting.", + "Add airline approval icons." + ], + textContent: [ + "Title: AIRLINE APPROVED", + "Bullet 1: Height: 28 cm (Fits Most Airlines)", + "Bullet 2: TSA Compliant Design" + ] + } + }, + { + id: 'APlus_03', + source: '1023_0151.JPG', // 特写图(有正确扣件) + config: { + tasks: [ + "Maintain the split-screen detail layout.", + "Enhance the water-shedding droplets on the left side.", + "Sharpen the silver tuck-lock clasp on the right side." + ], + textContent: [ + "Logo: touchdog (LOWERCASE)", + "Main Text: LUXURIOUS FEEL & STYLISH DESIGN", + "Subtext: Premium textured finish with light-luxury sheen" + ] + } + }, + { + id: 'Main_02', + source: 'P1191451.JPG', // 侧面透气图 + config: { + tasks: [ + "Highlight the privacy curtains and mesh windows.", + "Add clean blue airflow arrows.", + "Replace background with unified soft beige gradient.", + "CRITICAL: REMOVE the short carrying handle on top of the bag. The top should be clean and flat without any short briefcase-style handle." + ], + textContent: [ + "Text: 360° BREATHABLE COMFORT", + "Label: Adjustable Privacy Curtains", + "Label: Dual 2.5cm Vent Holes" + ] + } + }, + { + id: 'Main_05', + source: 'P1191464.JPG', // 正面图用于尺寸 + config: { + tasks: [ + "Place bag on unified white/beige background.", + "Add dimension lines with precision." + ], + textContent: [ + `Dimensions: ${UI_STANDARDS.dimensions}`, + "Text: FITS UNDER SEAT", + "Weight: 0.56 KG Lightweight" + ] + } + } + ]; + + for (const task of tasks) { + console.log(`\n🎨 正在处理 [${task.id}] 使用素材: ${task.source}`); + try { + const prompt = buildFidelityPrompt(task.config); + const imgBuffer = fs.readFileSync(path.join(materialDir, task.source)); + const imgUrl = await uploadToR2(imgBuffer, task.source); + + const taskId = await submitTask(prompt, imgUrl, task.id.startsWith('APlus') ? '3:2' : '1:1'); + const resultUrl = await pollResult(taskId); + + const resultBuffer = await axios.get(resultUrl, { responseType: 'arraybuffer' }); + fs.writeFileSync(path.join(outputDir, `${task.id}.jpg`), Buffer.from(resultBuffer.data)); + fs.writeFileSync(path.join(outputDir, `${task.id}_prompt.txt`), prompt); + console.log(`✅ ${task.id} 成功`); + } catch (e) { + console.error(`❌ ${task.id} 失败: ${e.message}`); + } + } +} + +runV3().catch(console.error); + diff --git a/poc-workflow-carrier.js b/poc-workflow-carrier.js new file mode 100644 index 0000000..f157823 --- /dev/null +++ b/poc-workflow-carrier.js @@ -0,0 +1,572 @@ +/** + * 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); diff --git a/test-vision-carrier.js b/test-vision-carrier.js new file mode 100644 index 0000000..ef756c8 --- /dev/null +++ b/test-vision-carrier.js @@ -0,0 +1,81 @@ +/** + * Test Vision Extractor on Carrier Images + */ +require('dotenv').config(); +const fs = require('fs'); +const path = require('path'); +const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); +const { extractProductFeatures, buildGoldenDescription } = require('./lib/vision-extractor'); + +// 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 buffer = fs.readFileSync(filePath); + const fileName = `vision-test-${path.basename(filePath)}`; + + console.log(` 上传 ${path.basename(filePath)}...`); + + await r2Client.send(new PutObjectCommand({ + Bucket: process.env.R2_BUCKET_NAME || 'ai-flow', + Key: fileName, + Body: buffer, + ContentType: '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 main() { + const materialDir = path.join(__dirname, '素材/登机包'); + + // 选择几张代表性图片 + // 假设 P1191464.JPG 是主图,1023_0151.JPG 是细节图 + const targetFiles = ['P1191464.JPG', '1023_0151.JPG']; + + for (const file of targetFiles) { + const filePath = path.join(materialDir, file); + if (!fs.existsSync(filePath)) { + console.log(`❌ 文件不存在: ${file}`); + continue; + } + + console.log(`\n🔍 分析图片: ${file}`); + + try { + // 1. 上传图片获取URL + const imageUrl = await uploadToR2(filePath); + console.log(` URL: ${imageUrl}`); + + // 2. 调用Vision提取特征 + const result = await extractProductFeatures(imageUrl); + + if (result) { + console.log('\n📋 提取结果:'); + console.log(JSON.stringify(result, null, 2)); + + console.log('\n✨ Golden Description:'); + console.log(buildGoldenDescription(result, 'pet travel carrier')); + } + + } catch (e) { + console.error(`❌ 分析失败: ${e.message}`); + } + } +} + +main().catch(console.error); + + + + diff --git a/upscale-images.js b/upscale-images.js new file mode 100644 index 0000000..c38d352 --- /dev/null +++ b/upscale-images.js @@ -0,0 +1,63 @@ +/** + * 批量图片优化脚本 + * 功能:将指定目录下的图片放大到 2048px 并优化 JPEG 质量 + */ +const fs = require('fs'); +const path = require('path'); +const sharp = require('sharp'); + +async function upscaleImages(sourceDir) { + const outputDir = sourceDir + '_upscaled'; + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); + + const files = fs.readdirSync(sourceDir).filter(f => f.endsWith('.jpg') || f.endsWith('.png')); + + console.log(`🚀 开始优化 ${files.length} 张图片...`); + console.log(`📂 源目录: ${sourceDir}`); + console.log(`📂 输出目录: ${outputDir}`); + + for (const file of files) { + const inputPath = path.join(sourceDir, file); + const outputPath = path.join(outputDir, file); + + try { + const image = sharp(inputPath); + const metadata = await image.metadata(); + + console.log(`Processing ${file} (${metadata.width}x${metadata.height})...`); + + // 目标尺寸:长边 2500px (满足亚马逊 Zoom 要求) + const targetSize = 2500; + + await image + .resize({ + width: metadata.width >= metadata.height ? targetSize : null, + height: metadata.height > metadata.width ? targetSize : null, + kernel: sharp.kernel.lanczos3 // 高质量插值算法 + }) + // 适度锐化,提升清晰感 + .sharpen({ + sigma: 1.5, + m1: 1.0, + m2: 0.5 + }) + .jpeg({ + quality: 95, // 高质量 JPEG + chromaSubsampling: '4:4:4' // 减少色彩压缩 + }) + .toFile(outputPath); + + console.log(`✅ 已优化: ${file}`); + } catch (e) { + console.error(`❌ 失败 ${file}: ${e.message}`); + } + } + console.log('✨ 全部完成!'); +} + +// 目标目录 +const targetDir = path.join(__dirname, 'output_carrier_v2_2025-12-27'); +upscaleImages(targetDir).catch(console.error); + + + diff --git a/upscale-to-4k-ai.js b/upscale-to-4k-ai.js new file mode 100644 index 0000000..83973b0 --- /dev/null +++ b/upscale-to-4k-ai.js @@ -0,0 +1,161 @@ +/** + * 4K AI 升级脚本 (1:1 还原版) + * 逻辑: + * 1. 读取 v2 版本已生成的图片(客户已对齐的版本) + * 2. 将其作为 img_url (参考图) 传给 AI + * 3. 使用原 prompt,并开启 imageSize: '4K' + * 4. 这样 AI 会在 v2 的基础上进行高清化重绘,保持构图 1:1 还原 + */ + +require('dotenv').config(); +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +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 = `upscale-4k-${Date.now()}-${filename}`; + await r2Client.send(new PutObjectCommand({ + Bucket: process.env.R2_BUCKET_NAME || 'ai-flow', + Key: fileName, + Body: buffer, + ContentType: '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 submitUpscaleTask(prompt, refImageUrl, aspectRatio) { + const payload = { + key: API_KEY, + prompt: prompt, // 使用原 prompt 保持语义一致 + img_url: refImageUrl, // 使用已生成的图作为底图 + aspectRatio: aspectRatio, + imageSize: '4K' // 升级到 4K + }; + + const response = await axios.post(`${API_BASE}/img/nanoBanana-pro`, payload, { timeout: 60000 }); + return response.data.data?.id || response.data.id; +} + +async function pollImageResult(taskId) { + let attempts = 0; + const maxAttempts = 120; // 4K 稍微给多点时间 + 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; + if (data && data.status === 3) throw new Error('Upscale failed: ' + (data.fail_reason || 'Unknown')); + + process.stdout.write('.'); + await new Promise(r => setTimeout(r, 2000)); + attempts++; + } catch (error) { + if (error.message.includes('Upscale 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 main() { + const v2Dir = path.join(__dirname, 'output_carrier_v2_2025-12-27'); + const outputDir = path.join(__dirname, `output_carrier_v3_4K_Upscale_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}`); + + if (!fs.existsSync(v2Dir)) { + console.error('❌ 找不到 v2 目录:', v2Dir); + return; + } + + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); + + console.log('🚀 开始 4K AI 升级 (基于 v2 已对齐版本)...'); + console.log('📂 输入目录:', v2Dir); + console.log('📂 输出目录:', outputDir); + + const files = fs.readdirSync(v2Dir).filter(f => f.endsWith('.jpg')); + + for (const file of files) { + const id = file.replace('.jpg', ''); + const promptFile = path.join(v2Dir, `${id}_prompt.txt`); + + if (!fs.existsSync(promptFile)) { + console.log(`⏭️ 跳过 ${file} (找不到对应的 prompt 文件)`); + continue; + } + + console.log(`\n🎨 升级 [${id}]...`); + + try { + const prompt = fs.readFileSync(promptFile, 'utf-8'); + const imgBuffer = fs.readFileSync(path.join(v2Dir, file)); + const aspectRatio = id.startsWith('APlus') ? '3:2' : '1:1'; + + // 1. 上传 v2 图片到 R2 + const imgUrl = await uploadToR2(imgBuffer, file); + + // 2. 提交 4K 任务 + const taskId = await submitUpscaleTask(prompt, imgUrl, aspectRatio); + console.log(` Task ID: ${taskId}`); + + // 3. 轮询结果 + const resultUrl = await pollImageResult(taskId); + console.log('\n ✓ 4K 生成完成'); + + // 4. 下载并保存 + const resultBuffer = await downloadImage(resultUrl); + await imageProcessor.saveImage(resultBuffer, path.join(outputDir, file), 'jpeg', 95); + + // 拷贝 prompt 文件 + fs.copyFileSync(promptFile, path.join(outputDir, `${id}_prompt.txt`)); + + console.log(`✅ ${file} 已升级到 4K`); + } catch (e) { + console.error(`\n❌ ${id} 失败:`, e.message); + } + } + + console.log('\n✨ 全部升级任务完成!'); +} + +main().catch(console.error); + + +