Video Maker: size every end-card line to the frame width; two-line headline and footer in portrait

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-14 07:18:51 -05:00
parent 9316dfc0db
commit 96760158df
+13 -5
View File
@@ -107,13 +107,21 @@ async function render(job) {
await QR.toFile(qrFile, 'https://' + link, { width: Math.round(Math.min(w, h) * 0.34), margin: 1, color: { dark: '#061c17', light: '#ffffff' } });
const F = font(); const portrait = h > w; const big = Math.round(Math.min(w, h) * (portrait ? 0.075 : 0.07)), mid = Math.round(big * 0.62), small = Math.round(big * 0.48);
const qrSize = Math.round(Math.min(w, h) * 0.34);
const yTitle = Math.round(h * (portrait ? 0.18 : 0.14)), yQr = Math.round(h * (portrait ? 0.30 : 0.30)), yName = yQr + qrSize + Math.round(h * 0.03), yLink = yName + mid + Math.round(h * 0.015), yFoot = h - Math.round(h * 0.08);
// every line is sized to fit 90% of the frame width (DejaVu Bold averages ~0.62em per glyph); portrait splits the long lines in two
const maxW = Math.round(w * 0.9); const fit = (t, max) => Math.max(18, Math.min(max, Math.floor(maxW / (t.length * 0.62))));
const titles = portrait ? ['Join my line', 'on InstantAdPay'] : ['Join my line on InstantAdPay'];
const foots = portrait ? ['Free to join. Paid in the same transaction.', 'Not investment advice.'] : ['Free to join. Paid in the same transaction. Not investment advice.'];
const bigS = Math.min(...titles.map(t => fit(t, big))), midS = fit('@' + job.username, mid), smallS = fit(link, small), footS = Math.min(...foots.map(t => fit(t, Math.round(small * 0.8))));
const yTitle = Math.round(h * (portrait ? 0.16 : 0.14)), yQr = Math.round(h * 0.30), yName = yQr + qrSize + Math.round(h * 0.03), yLink = yName + midS + Math.round(h * 0.015), yFootEnd = h - Math.round(h * 0.08);
const draw = (t, color, size, y) => 'drawtext=fontfile=' + F + ":text='" + ffText(t) + "':fontcolor=" + color + ':fontsize=' + size + ':x=(w-text_w)/2:y=' + y;
const steps = [];
titles.forEach((t, i) => steps.push(draw(t, '0x43e8c3', bigS, yTitle + Math.round(i * bigS * 1.2))));
steps.push(draw('@' + job.username, '0xffffff', midS, yName));
steps.push(draw(link, '0xffd15c', smallS, yLink));
foots.forEach((t, i) => steps.push(draw(t, '0x8fd8c4', footS, yFootEnd - Math.round((foots.length - 1 - i) * footS * 1.35))));
const filters = [
'[0:v][1:v]overlay=(W-w)/2:' + yQr + '[b]',
'[b]drawtext=fontfile=' + F + ":text='" + ffText('Join my line on InstantAdPay') + "':fontcolor=0x43e8c3:fontsize=" + big + ':x=(w-text_w)/2:y=' + yTitle + '[c]',
'[c]drawtext=fontfile=' + F + ":text='" + ffText('@' + job.username) + "':fontcolor=0xffffff:fontsize=" + mid + ':x=(w-text_w)/2:y=' + yName + '[d]',
'[d]drawtext=fontfile=' + F + ":text='" + ffText(link) + "':fontcolor=0xffd15c:fontsize=" + small + ':x=(w-text_w)/2:y=' + yLink + '[e]',
'[e]drawtext=fontfile=' + F + ":text='" + ffText('Free to join. Paid in the same transaction. Not investment advice.') + "':fontcolor=0x8fd8c4:fontsize=" + Math.round(small * 0.8) + ':x=(w-text_w)/2:y=' + yFoot + ',format=yuv420p[v]'
'[b]' + steps.join(',') + ',format=yuv420p[v]'
].join(';');
await run('ffmpeg', ['-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'color=c=0x061c17:s=' + w + 'x' + h + ':r=30:d=4.5', '-i', qrFile, '-f', 'lavfi', '-i', 'anullsrc=r=44100:cl=stereo', '-filter_complex', filters, '-map', '[v]', '-map', '2:a', '-t', '4.5', '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '24', '-c:a', 'aac', '-shortest', endFile], 120000);
await run('ffmpeg', ['-y', '-loglevel', 'error', '-i', srcFile, '-i', endFile, '-filter_complex',