[object Object]

← 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

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 →