#!/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 += `${line}`; }); const svg = `${els}`; 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); });