- 新增 gen-covers 系列脚本(kling/gpt/t2i/batch/direct/final等方案) - 执黑先行二号添加9种风格提示词目录(梦核/剪纸/水墨/毛毡/硬核线条等) - 执黑先行添加封面提示词、执黑先行二号更新图片提示词 - product_viral_factory 账号配置扩充并添加 cover_template 参考图 - capcut_assemble/kling-video-generator/oss-upload/poll-utils 细节修复 - CLAUDE.md 更新流程文档 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
7.8 KiB
JavaScript
113 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/** GPT Image 文生图封面 - generations endpoint, 纯文本 prompt */
|
|
const fs = require('fs'), path = require('path'), https = require('https');
|
|
const sharp = require('sharp');
|
|
const OSS = require('ali-oss');
|
|
|
|
const cfg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'config.json'), 'utf-8'));
|
|
const oss = new OSS({ region: cfg.ossRegion, accessKeyId: cfg.ossAccessKeyId, accessKeySecret: cfg.ossAccessKeySecret, bucket: cfg.ossBucket, secure: true });
|
|
const AK = cfg.gptImageApiKey;
|
|
const OUT = '/Users/lc/video-create/output/执黑先行_补封面/t2i_final';
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
|
|
const LIST = [
|
|
{ s:'01', t:'闭嘴做事·才是狠人', p:'A cinematic vertical movie poster. A lone powerful figure in a sleek black suit stands in a dark modern cityscape at night, arms crossed, looking down with cold confidence. Semi-realistic Marvel-tone style. Dramatic warm orange light from one side, cold blue light from the other. Dark atmospheric background. High contrast. Poster composition. NO text, NO watermark, NO words.' },
|
|
{ s:'02', t:'你只是没钱罢了', p:'A cinematic vertical movie poster. A person standing alone in an empty luxurious penthouse overlooking a glittering city at night, hand in empty pockets, expression of raw realization. Semi-realistic Marvel-tone. Warm golden light clashing with cold blue moonlight. Dark moody atmosphere. High contrast poster composition. NO text, NO watermark, NO words.' },
|
|
{ s:'03', t:'别跪了·你配的上', p:'A cinematic vertical movie poster. A person rising from their knees in a grand marble hall, back straight, chin up, light breaking through tall windows behind them. Dignity and power. Semi-realistic Marvel-tone style. Warm amber light flooding in, cold blue shadows retreating. Dramatic composition. NO text, NO watermark, NO words.' },
|
|
{ s:'04', t:'没人说的·阶层真相', p:'A cinematic vertical movie poster. A figure standing at the edge of two worlds - below a crowded grey street, above a golden elite realm behind glass. Looking through an invisible wall. Semi-realistic Marvel-tone. Cold blue street below, warm gold above. Social commentary mood. NO text, NO watermark, NO words.' },
|
|
{ s:'05', t:'出轨真相·不是道德', p:'A cinematic vertical movie poster. Two silhouettes facing away from each other in a rain-soaked street at night, neon reflections on wet pavement, one looking back with a complex expression. Semi-realistic Marvel-tone style. Red neon glow mixed with cold blue rain. Emotional distance. NO text, NO watermark, NO words.' },
|
|
{ s:'06', t:'不凑合·最好的风水', p:'A cinematic vertical movie poster. A person confidently walking away from a crumbling grey building toward a vibrant sunlit path, head held high. Semi-realistic Marvel-tone. Warm golden sunlight breaking through, cold shadows behind. Empowering atmosphere. NO text, NO watermark, NO words.' },
|
|
{ s:'07', t:'跟对人·只管赢', p:'A cinematic vertical movie poster. A powerful mentor figure in shadow standing behind a rising challenger in spotlight, like a chess master and their strongest piece. Semi-realistic Marvel-tone style. Warm gold on the protégé, cool blue surrounding the mentor. Strategic power. NO text, NO watermark, NO words.' },
|
|
{ s:'10', t:'哭惨的人·榨你最狠', p:'A cinematic vertical movie poster. A person pulling away from grasping hands reaching from darkness, expression of cold clarity and self-preservation. Semi-realistic Marvel-tone style. The grasping hands in cold blue shadow, the escaping figure in warm protective light. Toxic relationship mood. NO text, NO watermark, NO words.' },
|
|
];
|
|
|
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
|
|
async function gptGen(prompt, size, retries=3) {
|
|
for (let a = 0; a <= retries; a++) {
|
|
if (a > 0) { console.log(` retry ${a}/${retries} after ${a*5}s...`); await sleep(a*5000); }
|
|
try {
|
|
const body = JSON.stringify({ model: 'gpt-image-2', prompt, n: 1, size, quality: 'auto', format: 'png' });
|
|
const res = await new Promise((resolve, reject) => {
|
|
const req = https.request({
|
|
hostname: 'yunwu.ai', port: 443, path: '/v1/images/generations', method: 'POST',
|
|
headers: { 'Authorization': 'Bearer ' + AK, 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(body)) },
|
|
timeout: 120000,
|
|
}, (res) => { const c = []; res.on('data', d => c.push(d)); res.on('end', () => { resolve({ status: res.statusCode, data: Buffer.concat(c).toString() }); }); });
|
|
req.on('error', reject);
|
|
req.write(body); req.end();
|
|
});
|
|
if (res.status === 200) {
|
|
const j = JSON.parse(res.data);
|
|
if (j.data?.[0]?.b64_json) return Buffer.from(j.data[0].b64_json, 'base64');
|
|
if (j.data?.[0]?.url) return j.data[0].url;
|
|
}
|
|
console.log(` ⚠️ ${res.status}: ${res.data.substring(0,120)}`);
|
|
if (res.status === 429) continue; // rate limit, retry
|
|
if (res.status >= 500) continue; // server error, retry
|
|
} catch (e) {
|
|
console.log(` ⚠️ network: ${e.message}`);
|
|
// network error, retry
|
|
}
|
|
}
|
|
throw new Error('GPT gen failed after retries');
|
|
}
|
|
|
|
async function addText(imgPath, text, outPath) {
|
|
const meta = await sharp(imgPath).metadata();
|
|
const { width: w, height: h } = meta;
|
|
const fs2 = Math.round(w * 0.10);
|
|
const parts = text.split('·'), lines = parts.length === 2 ? parts : [text];
|
|
const lh = fs2 * 1.3, startY = (h - lh * lines.length) / 2 + fs2 * 0.8;
|
|
let els = '';
|
|
lines.forEach((line, i) => {
|
|
const y = startY + i * lh;
|
|
els += `<text x="${w/2}" y="${y}" text-anchor="middle" font-size="${fs2}" font-family="'PingFang SC','Heiti SC','STHeiti','Arial',sans-serif" font-weight="900" fill="white" stroke="#C8960C" stroke-width="${Math.round(fs2*0.06)}" paint-order="stroke fill" letter-spacing="${Math.round(fs2*0.05)}">${line}</text>`;
|
|
});
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}"><style>text{filter:drop-shadow(0 ${Math.round(fs2*0.04)} ${Math.round(fs2*0.08)} rgba(0,0,0,0.7))}</style>${els}</svg>`;
|
|
await sharp(imgPath).composite([{ input: Buffer.from(svg), top: 0, left: 0 }]).png({ quality: 95 }).toFile(outPath);
|
|
}
|
|
|
|
async function doOne(item, idx) {
|
|
const { s, t, p } = item;
|
|
const dir = path.join(OUT, s); fs.mkdirSync(dir, { recursive: true });
|
|
console.log(`\n${'='.repeat(50)}\n[${idx+1}/8] ${t}\n${'='.repeat(50)}`);
|
|
|
|
const sizes = [
|
|
{ lb: '抖音', sz: '1088x1920', nm: `cover_${s}_抖音` },
|
|
{ lb: '小红书', sz: '1152x1536', nm: `cover_${s}_小红书` },
|
|
];
|
|
|
|
for (const { lb, sz, nm } of sizes) {
|
|
console.log(` 🎨 ${lb} (${sz})...`);
|
|
const result = await gptGen(p, sz);
|
|
const rawPath = path.join(dir, `_${lb}.png`);
|
|
if (Buffer.isBuffer(result)) {
|
|
fs.writeFileSync(rawPath, result);
|
|
} else {
|
|
await new Promise((resolve, reject) => {
|
|
const file = fs.createWriteStream(rawPath);
|
|
https.get(result, (res) => { res.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', reject);
|
|
});
|
|
}
|
|
console.log(` GPT done, adding text...`);
|
|
const final = path.join(dir, `${nm}.png`);
|
|
await addText(rawPath, t, final);
|
|
|
|
const ossName = `covers/执黑先行/t2i/${nm}.png`;
|
|
await oss.put(ossName, final);
|
|
const url = await oss.signatureUrl(ossName, { expires: cfg.ossExpires || 31536000 });
|
|
console.log(` ✅ ${lb}`);
|
|
try { fs.unlinkSync(rawPath); } catch (_) {}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
for (let i = 0; i < LIST.length; i++) {
|
|
try { await doOne(LIST[i], i); if (i < LIST.length-1) { console.log(' ⏸️ 3s...'); await sleep(3000); } }
|
|
catch (e) { console.error(`❌ [${LIST[i].s} ${LIST[i].t}]: ${e.message}`); }
|
|
}
|
|
console.log('\n✅ Done.');
|
|
}
|
|
main().catch(e => { console.error(e.message); process.exit(1); });
|