Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
578a360894 |
94
fix-aplus03.js
Normal file
94
fix-aplus03.js
Normal file
@@ -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);
|
||||
|
||||
|
||||
|
||||
214
poc-workflow-carrier-v3.js
Normal file
214
poc-workflow-carrier-v3.js
Normal file
@@ -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);
|
||||
|
||||
572
poc-workflow-carrier.js
Normal file
572
poc-workflow-carrier.js
Normal 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);
|
||||
81
test-vision-carrier.js
Normal file
81
test-vision-carrier.js
Normal file
@@ -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);
|
||||
|
||||
|
||||
|
||||
|
||||
63
upscale-images.js
Normal file
63
upscale-images.js
Normal file
@@ -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);
|
||||
|
||||
|
||||
|
||||
161
upscale-to-4k-ai.js
Normal file
161
upscale-to-4k-ai.js
Normal file
@@ -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);
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user