Files
amz-pic-flow/poc-workflow-carrier.js
2026-01-07 10:12:38 +08:00

573 lines
22 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 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);