Carrier workflow + 4K upscale scripts

This commit is contained in:
tony
2026-01-07 10:12:38 +08:00
parent f5cb1042ae
commit 578a360894
6 changed files with 1185 additions and 0 deletions

572
poc-workflow-carrier.js Normal file
View File

@@ -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);