← back to Allnewsdaily
allnewsdaily Short: Steve's cloned voice + natural model + stock-photo card backgrounds
e68bd3975e1f5c4a33e9c92b07a0ae0df53fa150 · 2026-09-09 22:35:38 -0700 · Steve Abrams
Voice → Steve Abrams clone (Xa9qV4wNbvSkdUWsYLzq), model eleven_multilingual_v2 with
looser voice_settings (less mechanical). New fetch-stock.mjs pulls one commercial-safe
Openverse CC0/public-domain image per headline (keyword + generic-news fallback chain =
full coverage); render-short.js uses it as a darkened full-bleed card background with a
legibility scrim, falling back to the gradient card when no image. Addresses TK-11343 sameness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A scripts/short/fetch-stock.mjsM scripts/short/make-daily-short.mjsM scripts/short/render-short.jsM scripts/short/tts-elevenlabs.mjs
Diff
commit e68bd3975e1f5c4a33e9c92b07a0ae0df53fa150
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 9 22:35:38 2026 -0700
allnewsdaily Short: Steve's cloned voice + natural model + stock-photo card backgrounds
Voice → Steve Abrams clone (Xa9qV4wNbvSkdUWsYLzq), model eleven_multilingual_v2 with
looser voice_settings (less mechanical). New fetch-stock.mjs pulls one commercial-safe
Openverse CC0/public-domain image per headline (keyword + generic-news fallback chain =
full coverage); render-short.js uses it as a darkened full-bleed card background with a
legibility scrim, falling back to the gradient card when no image. Addresses TK-11343 sameness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
scripts/short/fetch-stock.mjs | 94 ++++++++++++++++++++++++++++++++++++++
scripts/short/make-daily-short.mjs | 2 +
scripts/short/render-short.js | 21 +++++++--
scripts/short/tts-elevenlabs.mjs | 9 ++--
4 files changed, 116 insertions(+), 10 deletions(-)
diff --git a/scripts/short/fetch-stock.mjs b/scripts/short/fetch-stock.mjs
new file mode 100644
index 0000000..391f94f
--- /dev/null
+++ b/scripts/short/fetch-stock.mjs
@@ -0,0 +1,94 @@
+#!/usr/bin/env node
+// fetch-stock.mjs — download ONE commercially-safe stock image per story for the Short's card
+// backgrounds. Source: Openverse (no API key), filtered to license=cc0,pdm (public domain / CC0)
+// so it's commercial-use safe with NO attribution required. Best-effort: always exits 0; a story
+// with no usable image just falls back to the gradient card in render-short.js.
+// Writes data/short/img/{intro,<beatN>,outro}.jpg. TK-11342 / TK-11343 (reduce template sameness).
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const DIR = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.resolve(DIR, '../..');
+const DATA = path.join(ROOT, 'data', 'short');
+const IMG = path.join(DATA, 'img');
+const OPENVERSE = 'https://api.openverse.org/v1/images/';
+
+const STOP = new Set(('a,an,the,of,to,in,on,for,and,or,but,after,before,as,at,by,with,from,into,over,under,is,are,was,were,' +
+ 'be,been,has,have,had,will,would,could,should,say,says,said,report,reports,reported,new,amid,than,that,this,these,those,' +
+ 'it,its,their,his,her,they,dozens,several,many,more,most,who,what,when,where,why,how,about,against,between,during,without,' +
+ 'first,second,two,three,four,five,near,off,out,up,down,per,via,plan,plans,set,gets,get').split(','));
+
+function queryFor(headline) {
+ const caps = (headline.match(/\b[A-Z][a-zA-Z]{2,}/g) || []).map((w) => w.toLowerCase()).filter((w) => !STOP.has(w));
+ const words = headline.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter((w) => w.length > 3 && !STOP.has(w));
+ const terms = [...new Set([...caps, ...words])].slice(0, 3);
+ return terms.join(' ') || 'breaking news';
+}
+
+async function tfetch(url, opts = {}, ms = 9000) {
+ const ac = new AbortController(); const to = setTimeout(() => ac.abort(), ms);
+ try { return await fetch(url, { ...opts, signal: ac.signal, headers: { 'user-agent': 'allnewsdaily-short/1.0', ...(opts.headers || {}) } }); }
+ finally { clearTimeout(to); }
+}
+
+async function downloadFirstUsable(results, outPath) {
+ for (const it of results) {
+ for (const src of [it.url, it.thumbnail]) { // try original, then the Openverse-proxied thumbnail
+ if (!src) continue;
+ try {
+ const r = await tfetch(src, {}, 10000);
+ if (!r.ok) continue;
+ const ct = r.headers.get('content-type') || '';
+ if (!/image\//.test(ct)) continue;
+ const buf = Buffer.from(await r.arrayBuffer());
+ if (buf.length < 6000) continue; // skip tiny/broken
+ fs.writeFileSync(outPath, buf);
+ return { ok: true, bytes: buf.length };
+ } catch { /* try next src */ }
+ }
+ }
+ return { ok: false };
+}
+
+// Rotating generic news imagery so a CC0-sparse headline still gets a (thematic) background,
+// and consecutive fallbacks differ instead of repeating the same photo.
+const GENERIC = ['news studio broadcast', 'city skyline', 'government capitol building', 'world globe earth',
+ 'newspaper headlines', 'crowd people street', 'flags government', 'stock market chart', 'satellite earth night'];
+
+async function grabWithFallback(primaryQuery, outPath, idx) {
+ let r = await grab(primaryQuery, outPath); if (r.ok) return { ...r, used: primaryQuery };
+ const t1 = primaryQuery.split(' ')[0];
+ if (t1 && t1 !== primaryQuery) { r = await grab(t1, outPath); if (r.ok) return { ...r, used: t1 }; }
+ const g = GENERIC[idx % GENERIC.length];
+ r = await grab(g, outPath); if (r.ok) return { ...r, used: g + ' (generic)' };
+ r = await grab('news', outPath); return r.ok ? { ...r, used: 'news (generic)' } : { ok: false };
+}
+
+async function grab(query, outPath) {
+ try {
+ const url = `${OPENVERSE}?q=${encodeURIComponent(query)}&license=cc0,pdm&size=large&page_size=8&mature=false`;
+ const r = await tfetch(url);
+ if (!r.ok) return { ok: false, note: `openverse HTTP ${r.status}` };
+ const j = await r.json();
+ const res = await downloadFirstUsable(j.results || [], outPath);
+ return res.ok ? { ok: true, query, ...res } : { ok: false, note: 'no usable result', query };
+ } catch (e) { return { ok: false, note: e.message }; }
+}
+
+(async () => {
+ const storiesDoc = JSON.parse(fs.readFileSync(path.join(DATA, 'stories.json'), 'utf8'));
+ fs.rmSync(IMG, { recursive: true, force: true });
+ fs.mkdirSync(IMG, { recursive: true });
+
+ const jobs = [];
+ jobs.push(grabWithFallback('news studio broadcast', path.join(IMG, 'intro.jpg'), 0).then((r) => ['intro', r]));
+ storiesDoc.stories.forEach((s, i) => jobs.push(grabWithFallback(queryFor(s.headline), path.join(IMG, `${s.n}.jpg`), i + 1).then((r) => [`beat${s.n}`, r])));
+ jobs.push(grabWithFallback('world globe earth', path.join(IMG, 'outro.jpg'), 8).then((r) => ['outro', r]));
+
+ const results = await Promise.all(jobs);
+ let ok = 0;
+ for (const [tag, r] of results) { if (r.ok) ok++; console.log(` ${tag}: ${r.ok ? '✓ ' + (r.used || '') : '✗ ' + (r.note || 'fail')}`); }
+ console.log(`[fetch-stock] ${ok}/${results.length} images downloaded (CC0/public-domain, commercial-safe).`);
+ process.exit(0); // best-effort — never block the pipeline
+})().catch((e) => { console.error('[fetch-stock] non-fatal:', e.message); process.exit(0); });
diff --git a/scripts/short/make-daily-short.mjs b/scripts/short/make-daily-short.mjs
index eef6dbc..79a9d86 100644
--- a/scripts/short/make-daily-short.mjs
+++ b/scripts/short/make-daily-short.mjs
@@ -115,6 +115,8 @@ const releaseLock = () => { try { fs.rmSync(LOCK); } catch {} };
throw e;
}
console.log('▸ 2/5 build-script'); node('build-script.js');
+ console.log('▸ stock imagery (Openverse CC0/public-domain)');
+ try { node('fetch-stock.mjs'); } catch (e) { console.error('[daily-short] fetch-stock non-fatal:', e.message); }
const storiesDoc = JSON.parse(fs.readFileSync(path.join(DATA, 'stories.json'), 'utf8'));
const stories = storiesDoc.stories;
diff --git a/scripts/short/render-short.js b/scripts/short/render-short.js
index e1edab4..951a255 100644
--- a/scripts/short/render-short.js
+++ b/scripts/short/render-short.js
@@ -23,6 +23,7 @@ const { execFileSync } = require('child_process');
const ROOT = path.resolve(__dirname, '..', '..'); // ~/Projects/allnewsdaily
const DATA = path.join(ROOT, 'data', 'short');
const TMP = path.join(DATA, '.rtmp');
+const IMG_DIR = path.join(DATA, 'img'); // stock backgrounds from fetch-stock.mjs (optional)
const STORIES_PATH = path.join(DATA, 'stories.json');
const SCRIPT_PATH = path.join(DATA, 'script.json');
@@ -147,15 +148,17 @@ if (target > HARD_CAP_SEC) {
// ---------------------------------------------------------------------------
function cleanIntro(t) { return String(t).replace(/^\s*All News Daily\.\s*/i, '').trim() || t; }
+function imgPath(name) { const p = path.join(IMG_DIR, `${name}.jpg`); return fs.existsSync(p) ? p : null; }
+
const segments = [];
segments.push({ kind: 'intro', main: cleanIntro(script.intro.text), outlet: null,
- est: Number(script.intro.estSec) || 3, mainSize: 62, tag: 'BRIEFING' });
+ est: Number(script.intro.estSec) || 3, mainSize: 62, tag: 'BRIEFING', img: imgPath('intro') });
for (const b of script.beats) {
segments.push({ kind: 'beat', main: b.headline || b.text, outlet: b.outlet || null,
- est: Number(b.estSec) || 5, mainSize: 74, n: b.n });
+ est: Number(b.estSec) || 5, mainSize: 74, n: b.n, img: imgPath(b.n) });
}
segments.push({ kind: 'outro', main: script.outro.text, outlet: null,
- est: Number(script.outro.estSec) || 5, mainSize: 60, tag: 'ALLNEWSDAILY.COM' });
+ est: Number(script.outro.estSec) || 5, mainSize: 60, tag: 'ALLNEWSDAILY.COM', img: imgPath('outro') });
const estSum = segments.reduce((s, x) => s + x.est, 0);
let durs = segments.map(x => Math.max(1.4, +(target * x.est / estSum).toFixed(3)));
@@ -183,8 +186,16 @@ function writeRaw(name, str) { const p = path.join(TMP, name); fs.writeFileSync(
function buildCardPng(seg, idx) {
const png = path.join(TMP, `card${idx}.png`);
- // 1) dark vertical gradient base
- run(MAGICK, ['-size', `${CARD_W}x${CARD_H}`, `gradient:${HEX_BG0}-${HEX_BG1}`, png]);
+ // 1) base: a darkened full-bleed stock image (real news look) if we have one, else the gradient.
+ if (seg.img) {
+ // fill-crop to the card, darken hard so white text stays legible, slight blur to sit behind text
+ run(MAGICK, [seg.img, '-resize', `${CARD_W}x${CARD_H}^`, '-gravity', 'center', '-extent', `${CARD_W}x${CARD_H}`,
+ '-modulate', '54', '-fill', 'black', '-colorize', '26%', '-blur', '0x1.2', png]);
+ // bottom-weighted dark scrim (transparent top → black bottom) for the outlet chip + lower text
+ run(MAGICK, [png, '(', '-size', `${CARD_W}x${CARD_H}`, 'gradient:none-black', ')', '-gravity', 'center', '-composite', png]);
+ } else {
+ run(MAGICK, ['-size', `${CARD_W}x${CARD_H}`, `gradient:${HEX_BG0}-${HEX_BG1}`, png]);
+ }
// 2) tracked wordmark + red accent bar
run(MAGICK, [png, '-font', FONT, '-gravity', 'North',
diff --git a/scripts/short/tts-elevenlabs.mjs b/scripts/short/tts-elevenlabs.mjs
index 35ce56a..8e740a7 100644
--- a/scripts/short/tts-elevenlabs.mjs
+++ b/scripts/short/tts-elevenlabs.mjs
@@ -17,10 +17,9 @@ function readEnv(file, key) {
} catch { return null; }
}
-const VOICE = process.env.AND_TTS_VOICE || 'EXAVITQu4vr4xnSDxMaL'; // Sarah
-const MODEL = process.env.AND_TTS_MODEL || 'eleven_turbo_v2_5';
-// eleven_turbo_v2_5 billed ~$0.50 / 1k chars at list, far less on paid tiers; we log actual chars.
-const RATE_PER_1K = Number(process.env.AND_TTS_RATE_PER_1K || 0.15);
+const VOICE = process.env.AND_TTS_VOICE || 'Xa9qV4wNbvSkdUWsYLzq'; // Steve Abrams (cloned) — "use my voice"
+const MODEL = process.env.AND_TTS_MODEL || 'eleven_multilingual_v2'; // richer/less-mechanical prosody than turbo
+const RATE_PER_1K = Number(process.env.AND_TTS_RATE_PER_1K || 0.30);
export async function synthesize({ text, out }) {
const KEY = process.env.ELEVENLABS_API_KEY || readEnv(SECRETS, 'ELEVENLABS_API_KEY');
@@ -36,7 +35,7 @@ export async function synthesize({ text, out }) {
body: JSON.stringify({
text,
model_id: MODEL,
- voice_settings: { stability: 0.5, similarity_boost: 0.75, style: 0.0, use_speaker_boost: true },
+ voice_settings: { stability: 0.42, similarity_boost: 0.8, style: 0.35, use_speaker_boost: true },
}),
});
if (!r.ok) {
← 42d9caf allnewsdaily Short: auto-public + delete-canary (DTD verdict
·
back to Allnewsdaily
·
allnewsdaily Short: publish 3x/day (6/12/18) + min-gap guard 1a87806 →