xiaohongshufabu/server/lib/generate-image.js
2026-07-21 21:17:20 +08:00

130 lines
5.4 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.

const { imageToPHash, isSimilar } = require('./phash');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
const path = require('path');
async function generateImage(db, noteId, productTitle, options = {}) {
const imgConfig = {};
const keys = ['image_provider', 'image_api_key', 'image_base_url', 'image_prompt_template', 'image_style_options', 'image_count', 'image_size', 'image_save_path'];
for (const k of keys) {
const row = db.prepare('SELECT value FROM configs WHERE key = ?').get(k);
imgConfig[k] = row?.value || '';
}
if (!imgConfig.image_api_key) return { imagePaths: [], imagePrompt: '' };
const note = db.prepare('SELECT * FROM notes WHERE id = ?').get(noteId);
if (!note) return { imagePaths: [], imagePrompt: '' };
const imageCount = parseInt(options.image_count || imgConfig.image_count || '3', 10);
const imageSize = options.image_size || imgConfig.image_size || '1K';
const styles = JSON.parse(imgConfig.image_style_options || '["默认"]');
const style = styles[Math.floor(Math.random() * styles.length)];
const shop = db.prepare('SELECT shop_name FROM shops WHERE id = ?').get(note.shop_id);
const prompt = (options.image_prompt_template || imgConfig.image_prompt_template || '{{title}}')
.replace(/\{\{title\}\}/g, productTitle)
.replace(/\{商品标题\}/g, productTitle)
.replace(/\{\{shopName\}\}/g, shop?.shop_name || '')
.replace(/\{店铺名\}\}/g, shop?.shop_name || '')
.replace(/\{\{style\}\}/g, style);
const finalPrompt = prompt.includes(productTitle) ? prompt : `${productTitle}${prompt}`;
const imagesRoot = imgConfig.image_save_path || path.join(__dirname, '..', 'images');
const imagesDir = path.join(imagesRoot, note.product_id);
if (!fs.existsSync(imagesDir)) fs.mkdirSync(imagesDir, { recursive: true });
// Load product reference image if available
let referenceImage = null;
const product = db.prepare('SELECT reference_image FROM products WHERE id = ?').get(note.product_id);
if (product?.reference_image) {
const refPath = path.join(imagesRoot, note.product_id, 'ref.png');
if (fs.existsSync(refPath)) {
const refBuffer = fs.readFileSync(refPath);
referenceImage = `data:image/png;base64,${refBuffer.toString('base64')}`;
console.log(`[generate] Using reference image for product ${note.product_id}`);
}
}
const imagePaths = [];
for (let i = 0; i < imageCount; i++) {
try {
let baseUrl = (imgConfig.image_base_url || '').replace(/\/+$/, '');
if (!baseUrl) { console.error('[generate] image_base_url is empty, skipping'); continue; }
const apiUrl = baseUrl.endsWith('/v1') ? baseUrl + '/images/generations' : baseUrl + '/v1/images/generations';
console.log(`[generate] Image API request: ${apiUrl} model=${imgConfig.image_provider || 'agnes-image-2.1-flash'} size=${imageSize}`);
const requestBody = {
model: imgConfig.image_provider || 'agnes-image-2.1-flash',
prompt: finalPrompt,
size: imageSize,
};
if (referenceImage) {
requestBody.image = referenceImage;
}
// Retry logic for transient errors (502, 503, 429)
let data = null;
for (let attempt = 0; attempt < 3; attempt++) {
if (attempt > 0) {
const wait = attempt * 5000;
console.log(`[generate] Retry ${attempt}/2 after ${wait / 1000}s...`);
await new Promise(r => setTimeout(r, wait));
}
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${imgConfig.image_api_key}`,
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errBody = await response.text().catch(() => '');
console.error(`[generate] Image API error: ${response.status} ${errBody}`);
if ([502, 503, 429].includes(response.status) && attempt < 2) continue;
break;
}
data = await response.json();
break;
}
const imageUrl = data?.data?.[0]?.url || data?.data?.[0]?.b64_json;
if (!imageUrl) continue;
const imageFilename = `${noteId}_${i + 1}.png`;
const imagePath = path.join(imagesDir, imageFilename);
if (imageUrl.startsWith('http')) {
const imgResp = await fetch(imageUrl);
const buffer = Buffer.from(await imgResp.arrayBuffer());
fs.writeFileSync(imagePath, buffer);
} else {
fs.writeFileSync(imagePath, Buffer.from(imageUrl, 'base64'));
}
// pHash dedup
const fileBuffer = fs.readFileSync(imagePath);
const newHash = await imageToPHash(fileBuffer);
const existingHashes = db.prepare('SELECT phash FROM image_hashes ORDER BY created_at DESC LIMIT 50').all();
const isDuplicate = existingHashes.some(h => isSimilar(h.phash, newHash));
if (isDuplicate) {
console.log(`[pHash] Similar image detected, skipping: ${imageFilename}`);
fs.unlinkSync(imagePath);
continue;
}
const publicPath = `/api/images/${note.product_id}/${imageFilename}`;
db.prepare('INSERT INTO image_hashes (id, note_id, image_path, phash) VALUES (?, ?, ?, ?)')
.run(uuidv4(), noteId, publicPath, newHash);
imagePaths.push(publicPath);
} catch (e) {
console.error(`[generate] Image generation failed:`, e.message);
}
}
return { imagePaths, imagePrompt: finalPrompt };
}
module.exports = { generateImage };