[object Object]

← back to Allnewsdaily

allnewsdaily Short: show each network's real logo on the source chip

54ada90c07f6fca4bfc6351fe5445fe3d52bca72 · 2026-09-09 23:26:45 -0700 · Steve Abrams

fetch-stock.mjs downloads one logo per story (domain-derived: Clearbit → DuckDuckGo icon
proxy → Google favicons). render-short.js composites the logo + outlet name into a white
lower-third chip, replacing the red VIA {OUTLET} text (which stays as fallback when a logo
misses). Nominative-use attribution logos, not article imagery. Verified 6/6 logos + legible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 54ada90c07f6fca4bfc6351fe5445fe3d52bca72
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 9 23:26:45 2026 -0700

    allnewsdaily Short: show each network's real logo on the source chip
    
    fetch-stock.mjs downloads one logo per story (domain-derived: Clearbit → DuckDuckGo icon
    proxy → Google favicons). render-short.js composites the logo + outlet name into a white
    lower-third chip, replacing the red VIA {OUTLET} text (which stays as fallback when a logo
    misses). Nominative-use attribution logos, not article imagery. Verified 6/6 logos + legible.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/short/fetch-stock.mjs | 53 +++++++++++++++++++++++++++++++++++--------
 scripts/short/render-short.js | 33 ++++++++++++++++++++-------
 2 files changed, 68 insertions(+), 18 deletions(-)

diff --git a/scripts/short/fetch-stock.mjs b/scripts/short/fetch-stock.mjs
index 391f94f..5891aa1 100644
--- a/scripts/short/fetch-stock.mjs
+++ b/scripts/short/fetch-stock.mjs
@@ -12,6 +12,7 @@ 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 LOGO = path.join(DATA, 'logo');
 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,' +
@@ -56,6 +57,33 @@ async function downloadFirstUsable(results, outPath) {
 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'];
 
+function domainOf(link) { try { return new URL(link).hostname.replace(/^www\./, ''); } catch { return null; } }
+const EXT = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/x-icon': 'ico', 'image/vnd.microsoft.icon': 'ico', 'image/svg+xml': 'svg', 'image/webp': 'webp', 'image/gif': 'gif' };
+
+// Each network's real logo: Clearbit (best) → DuckDuckGo icon proxy → Google favicons (always returns something).
+async function grabLogo(domain, outBase) {
+  if (!domain) return { ok: false, note: 'no domain' };
+  const sources = [
+    `https://logo.clearbit.com/${domain}?size=256`,
+    `https://icons.duckduckgo.com/ip3/${domain}.ico`,
+    `https://www.google.com/s2/favicons?sz=256&domain=${domain}`,
+  ];
+  for (const src of sources) {
+    try {
+      const r = await tfetch(src, {}, 8000);
+      if (!r.ok) continue;
+      const ct = (r.headers.get('content-type') || '').split(';')[0].trim();
+      if (!/image\//.test(ct) || ct === 'image/svg+xml') continue; // skip svg (IM raster handling varies)
+      const buf = Buffer.from(await r.arrayBuffer());
+      if (buf.length < 400) continue;
+      const out = `${outBase}.${EXT[ct] || 'png'}`;
+      fs.writeFileSync(out, buf);
+      return { ok: true, domain, via: src.split('/')[2], bytes: buf.length };
+    } catch { /* next source */ }
+  }
+  return { ok: false, domain, note: 'no logo source hit' };
+}
+
 async function grabWithFallback(primaryQuery, outPath, idx) {
   let r = await grab(primaryQuery, outPath); if (r.ok) return { ...r, used: primaryQuery };
   const t1 = primaryQuery.split(' ')[0];
@@ -78,17 +106,22 @@ async function grab(query, outPath) {
 
 (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 });
+  for (const dir of [IMG, LOGO]) { fs.rmSync(dir, { recursive: true, force: true }); fs.mkdirSync(dir, { recursive: true }); }
+
+  const imgJobs = [];
+  imgJobs.push(grabWithFallback('news studio broadcast', path.join(IMG, 'intro.jpg'), 0).then((r) => ['img:intro', r]));
+  storiesDoc.stories.forEach((s, i) => imgJobs.push(grabWithFallback(queryFor(s.headline), path.join(IMG, `${s.n}.jpg`), i + 1).then((r) => [`img:beat${s.n}`, r])));
+  imgJobs.push(grabWithFallback('world globe earth', path.join(IMG, 'outro.jpg'), 8).then((r) => ['img:outro', r]));
 
-  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]));
+  // one real network logo per story (Clearbit → DuckDuckGo → Google favicons)
+  const logoJobs = storiesDoc.stories.map((s) => grabLogo(domainOf(s.link), path.join(LOGO, String(s.n))).then((r) => [`logo:${s.outlet}`, 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).`);
+  const results = await Promise.all([...imgJobs, ...logoJobs]);
+  let img = 0, logo = 0;
+  for (const [tag, r] of results) {
+    if (r.ok) { if (tag.startsWith('logo')) logo++; else img++; }
+    console.log(`  ${tag}: ${r.ok ? '✓ ' + (r.used || r.via || r.domain || '') : '✗ ' + (r.note || 'fail')}`);
+  }
+  console.log(`[fetch-stock] ${img} stock images (CC0/public-domain) + ${logo}/${storiesDoc.stories.length} network logos.`);
   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/render-short.js b/scripts/short/render-short.js
index 951a255..5f0a294 100644
--- a/scripts/short/render-short.js
+++ b/scripts/short/render-short.js
@@ -24,6 +24,7 @@ const ROOT = path.resolve(__dirname, '..', '..');            // ~/Projects/allne
 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 LOGO_DIR = path.join(DATA, 'logo'); // per-network source logos from fetch-stock.mjs (optional)
 
 const STORIES_PATH = path.join(DATA, 'stories.json');
 const SCRIPT_PATH = path.join(DATA, 'script.json');
@@ -149,13 +150,14 @@ 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; }
+function logoPath(n) { try { const f = fs.readdirSync(LOGO_DIR).find((x) => x.startsWith(String(n) + '.')); return f ? path.join(LOGO_DIR, f) : null; } catch { return null; } }
 
 const segments = [];
 segments.push({ kind: 'intro', main: cleanIntro(script.intro.text), outlet: null,
   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, img: imgPath(b.n) });
+    est: Number(b.estSec) || 5, mainSize: 74, n: b.n, img: imgPath(b.n), logo: logoPath(b.n) });
 }
 segments.push({ kind: 'outro', main: script.outro.text, outlet: null,
   est: Number(script.outro.estSec) || 5, mainSize: 60, tag: 'ALLNEWSDAILY.COM', img: imgPath('outro') });
@@ -208,13 +210,28 @@ function buildCardPng(seg, idx) {
     '-size', '1160x1040', '-gravity', 'center', `caption:@${hlFile}`, ')',
     '-gravity', 'center', '-geometry', '+0+0', '-composite', png]);
 
-  // 4) lower-third chip — outlet (beats) or a brand tag (intro/outro)
-  const chipText = seg.outlet ? ('VIA ' + String(seg.outlet).toUpperCase()) : (seg.tag || null);
-  if (chipText) {
-    const chFile = writeRaw(`chip${idx}.txt`, chipText);
-    run(MAGICK, [png, '(', '-background', HEX_RED, '-fill', 'white', '-font', FONT,
-      '-pointsize', '46', `label:@${chFile}`, '-bordercolor', HEX_RED, '-border', '28x18', ')',
-      '-gravity', 'North', '-geometry', '+0+1900', '-composite', png]);
+  // 4) lower-third source chip — the network's real LOGO + name (beats with a logo),
+  //    else the red "VIA {OUTLET}" text chip (intro/outro, or if the logo fetch missed).
+  if (seg.logo && seg.outlet) {
+    const logoSq = path.join(TMP, `logosq${idx}.png`);
+    // [0] = first frame (multi-size .ico); flatten transparency onto white; square to 104px
+    run(MAGICK, [`${seg.logo}[0]`, '-background', 'white', '-alpha', 'remove', '-alpha', 'off',
+      '-resize', '104x104', '-gravity', 'center', '-extent', '104x104', logoSq]);
+    const nmFile = writeRaw(`nm${idx}.txt`, ` ${seg.outlet} `);
+    const nameImg = path.join(TMP, `name${idx}.png`);
+    run(MAGICK, ['-background', 'white', '-fill', '#0A0A0A', '-font', FONT, '-pointsize', '54',
+      `label:@${nmFile}`, '-gravity', 'center', '-background', 'white', '-extent', 'x104', nameImg]);
+    const chip = path.join(TMP, `chip${idx}.png`);
+    run(MAGICK, [logoSq, nameImg, '+append', '-bordercolor', 'white', '-border', '26x22', chip]);
+    run(MAGICK, [png, chip, '-gravity', 'North', '-geometry', '+0+1852', '-composite', png]);
+  } else {
+    const chipText = seg.outlet ? ('VIA ' + String(seg.outlet).toUpperCase()) : (seg.tag || null);
+    if (chipText) {
+      const chFile = writeRaw(`chip${idx}.txt`, chipText);
+      run(MAGICK, [png, '(', '-background', HEX_RED, '-fill', 'white', '-font', FONT,
+        '-pointsize', '46', `label:@${chFile}`, '-bordercolor', HEX_RED, '-border', '28x18', ')',
+        '-gravity', 'North', '-geometry', '+0+1900', '-composite', png]);
+    }
   }
   return png;
 }

← c16abe8 allnewsdaily: cut outlet directory to top-20 most-popular gl  ·  back to Allnewsdaily  ·  allnewsdaily Short: up to 4x/day (6/11/16/21) + guard follow 944b120 →