[object Object]

← back to Ventura Corridor

iter 123+124: AIFF→M4A audio conversion + scoreboard top-tap leaderboard — /api/magazine/:id/audio.(aiff|m4a|mp3) renders TTS via macOS 'say -v Samantha -r 180' and converts via afconvert (m4a, AAC 64kbps, ~10× smaller than AIFF) or lame if available (mp3); falls back to AIFF for raw if afconvert/lame missing; cached by editorial-hash + format extension; aiff stays as the source of truth, m4a/mp3 derived on demand. /api/magazine/scoreboard now also returns top_tapped (top-10 features with mf.taps > 0); /scoreboard.html grows 'Top tapped · features driving outbound clicks' board with taps_breakdown chip showing which destinations got the click (sponsor:N share:N etc)

ead33e9ff0a653464a72e96fa57775e4c2211745 · 2026-05-06 18:26:37 -0700 · SteveStudio2

Files touched

Diff

commit ead33e9ff0a653464a72e96fa57775e4c2211745
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Wed May 6 18:26:37 2026 -0700

    iter 123+124: AIFF→M4A audio conversion + scoreboard top-tap leaderboard — /api/magazine/:id/audio.(aiff|m4a|mp3) renders TTS via macOS 'say -v Samantha -r 180' and converts via afconvert (m4a, AAC 64kbps, ~10× smaller than AIFF) or lame if available (mp3); falls back to AIFF for raw if afconvert/lame missing; cached by editorial-hash + format extension; aiff stays as the source of truth, m4a/mp3 derived on demand. /api/magazine/scoreboard now also returns top_tapped (top-10 features with mf.taps > 0); /scoreboard.html grows 'Top tapped · features driving outbound clicks' board with taps_breakdown chip showing which destinations got the click (sponsor:N share:N etc)
---
 data/magazine-audio/1-6e3ce8d703a7.m4a | Bin 0 -> 403683 bytes
 public/scoreboard.html                 |   7 +++
 src/server/index.ts                    | 104 ++++++++++++++++++++-------------
 3 files changed, 71 insertions(+), 40 deletions(-)

diff --git a/data/magazine-audio/1-6e3ce8d703a7.m4a b/data/magazine-audio/1-6e3ce8d703a7.m4a
new file mode 100644
index 0000000..0e9a287
Binary files /dev/null and b/data/magazine-audio/1-6e3ce8d703a7.m4a differ
diff --git a/public/scoreboard.html b/public/scoreboard.html
index 2592b64..88e4bf0 100644
--- a/public/scoreboard.html
+++ b/public/scoreboard.html
@@ -84,6 +84,13 @@ async function load() {
       title: 'Sponsor candidates', deck: 'paid-ad runners worth pitching', rows: d.sponsor_candidates,
       render: r => `<a href="/magazine/${r.id}">${escHtml(r.headline || r.biz_name)}</a><span class="name">${escHtml(r.biz_name)}</span><span class="v">$ ${r.ads}× ads</span>`
     },
+    {
+      title: 'Top tapped', deck: 'features driving outbound clicks', rows: d.top_tapped,
+      render: r => {
+        const breakdown = Object.entries(r.taps_breakdown || {}).map(([k, v]) => `${k}:${v}`).join(' ');
+        return `<a href="/magazine/${r.id}">${escHtml(r.headline || r.biz_name)}</a><span class="name">${escHtml(breakdown)}</span><span class="v">↗ ${r.taps}</span>`;
+      }
+    },
     {
       title: 'Latest features', deck: 'fresh off the press', rows: d.latest,
       render: r => `<a href="/magazine/${r.id}">${escHtml(r.headline || r.biz_name)}</a><span class="name">${escHtml((r.category_tag || '').replace(/-/g,' '))}</span><span class="v">${new Date(r.generated_at).toLocaleDateString('en-US',{month:'short',day:'numeric'})}</span>`
diff --git a/src/server/index.ts b/src/server/index.ts
index f898742..8ae9fc5 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1847,48 +1847,70 @@ import { existsSync as _fs_exists, mkdirSync as _fs_mkdir, statSync as _fs_stat,
 import { spawnSync as _spawn_sync } from 'node:child_process';
 import { createHash as _crypto_hash } from 'node:crypto';
 
-app.get('/api/magazine/:id/audio.aiff', async (req, res) => {
+async function renderFeatureAudio(id: number, format: 'aiff' | 'm4a' | 'mp3'): Promise<{ filepath: string; mime: string } | { error: string; status: number }> {
+  const r = await query(
+    `SELECT mf.headline, mf.subhead, mf.editorial, mf.pull_quote, b.name AS biz_name
+     FROM magazine_features mf JOIN businesses b ON b.id = mf.business_id
+     WHERE mf.id = $1`,
+    [id]
+  );
+  if (r.rowCount === 0) return { error: 'not found', status: 404 };
+  const f = r.rows[0];
+  const text = [
+    'From The Corridor.',
+    f.headline || f.biz_name,
+    f.subhead || '',
+    f.editorial || '',
+    f.pull_quote ? `Quote: ${f.pull_quote}` : '',
+    `End of feature.`
+  ].filter(Boolean).join('\n\n');
+
+  const audioDir = path.resolve(__dirname, '../../data/magazine-audio');
+  if (!_fs_exists(audioDir)) _fs_mkdir(audioDir, { recursive: true });
+  const hash = _crypto_hash('sha1').update(text).digest('hex').slice(0, 12);
+  const aiffPath = path.join(audioDir, `${id}-${hash}.aiff`);
+  const outPath  = path.join(audioDir, `${id}-${hash}.${format}`);
+
+  if (!_fs_exists(aiffPath)) {
+    const voice = process.env.SAY_VOICE || 'Samantha';
+    const result = _spawn_sync('say', ['-v', voice, '-r', '180', '-o', aiffPath, text], { timeout: 60_000 });
+    if (result.status !== 0 || !_fs_exists(aiffPath)) {
+      return { error: 'say failed: ' + (result.stderr?.toString() || ''), status: 500 };
+    }
+  }
+
+  if (format === 'aiff') return { filepath: aiffPath, mime: 'audio/aiff' };
+
+  if (!_fs_exists(outPath)) {
+    // macOS afconvert: AIFF → AAC m4a (or LAME for mp3)
+    if (format === 'm4a') {
+      const result = _spawn_sync('afconvert', ['-f', 'm4af', '-d', 'aac', '-b', '64000', aiffPath, outPath], { timeout: 60_000 });
+      if (result.status !== 0 || !_fs_exists(outPath)) {
+        return { error: 'afconvert failed: ' + (result.stderr?.toString() || ''), status: 500 };
+      }
+    } else if (format === 'mp3') {
+      // try lame if available, else fallback to m4a-as-mp3
+      const lameResult = _spawn_sync('lame', ['-h', '-b', '64', aiffPath, outPath], { timeout: 60_000 });
+      if (lameResult.status !== 0 || !_fs_exists(outPath)) {
+        return { error: 'lame not available; use /audio.m4a or /audio.aiff', status: 501 };
+      }
+    }
+  }
+  return { filepath: outPath, mime: format === 'm4a' ? 'audio/mp4' : 'audio/mpeg' };
+}
+
+app.get('/api/magazine/:id/audio.:fmt(aiff|m4a|mp3)', async (req, res) => {
   try {
     const id = parseInt(req.params.id, 10);
     if (!Number.isFinite(id)) return res.status(400).send('bad id');
-    const r = await query(
-      `SELECT mf.headline, mf.subhead, mf.editorial, mf.pull_quote, b.name AS biz_name
-       FROM magazine_features mf JOIN businesses b ON b.id = mf.business_id
-       WHERE mf.id = $1`,
-      [id]
-    );
-    if (r.rowCount === 0) return res.status(404).send('not found');
-    const f = r.rows[0];
-    const text = [
-      'From The Corridor.',
-      f.headline || f.biz_name,
-      f.subhead || '',
-      f.editorial || '',
-      f.pull_quote ? `Quote: ${f.pull_quote}` : '',
-      `End of feature.`
-    ].filter(Boolean).join('\n\n');
-
-    const audioDir = path.resolve(__dirname, '../../data/magazine-audio');
-    if (!_fs_exists(audioDir)) _fs_mkdir(audioDir, { recursive: true });
-    const hash = _crypto_hash('sha1').update(text).digest('hex').slice(0, 12);
-    const filename = `${id}-${hash}.aiff`;
-    const filepath = path.join(audioDir, filename);
-
-    if (!_fs_exists(filepath)) {
-      // Use a Steve-friendly voice. macOS default voices: Samantha, Karen, Daniel, Alex.
-      const voice = process.env.SAY_VOICE || 'Samantha';
-      const result = _spawn_sync('say', ['-v', voice, '-r', '180', '-o', filepath, text], {
-        timeout: 60_000
-      });
-      if (result.status !== 0 || !_fs_exists(filepath)) {
-        return res.status(500).send('say failed: ' + (result.stderr?.toString() || ''));
-      }
-    }
-    const stat = _fs_stat(filepath);
-    res.setHeader('Content-Type', 'audio/aiff');
+    const fmt = (req.params as any).fmt as 'aiff' | 'm4a' | 'mp3';
+    const r = await renderFeatureAudio(id, fmt);
+    if ('error' in r) return res.status(r.status).send(r.error);
+    const stat = _fs_stat(r.filepath);
+    res.setHeader('Content-Type', r.mime);
     res.setHeader('Content-Length', String(stat.size));
     res.setHeader('Cache-Control', 'public, max-age=86400');
-    _fs_stream(filepath).pipe(res);
+    _fs_stream(r.filepath).pipe(res);
   } catch (e: any) {
     res.status(500).send('error: ' + e.message);
   }
@@ -2018,19 +2040,21 @@ app.get('/api/magazine/ad-heatmap', async (_req, res) => {
 
 app.get('/api/magazine/scoreboard', async (_req, res) => {
   try {
-    const [topViewed, hotVerticals, latest, longest, sponsors] = await Promise.all([
+    const [topViewed, hotVerticals, latest, longest, sponsors, topTapped] = await Promise.all([
       query(`SELECT mf.id, mf.headline, mf.views, b.name AS biz_name FROM magazine_features mf JOIN businesses b ON b.id=mf.business_id WHERE mf.views > 0 ORDER BY mf.views DESC LIMIT 10`),
       query(`SELECT category_tag, count(*) AS n FROM magazine_features GROUP BY category_tag ORDER BY n DESC LIMIT 10`),
       query(`SELECT mf.id, mf.headline, mf.generated_at, b.name AS biz_name, mf.category_tag FROM magazine_features mf JOIN businesses b ON b.id=mf.business_id ORDER BY mf.generated_at DESC LIMIT 10`),
       query(`SELECT mf.id, mf.headline, length(mf.editorial) AS len, b.name AS biz_name FROM magazine_features mf JOIN businesses b ON b.id=mf.business_id WHERE mf.editorial IS NOT NULL ORDER BY length(mf.editorial) DESC LIMIT 10`),
-      query(`SELECT mf.id, mf.headline, b.name AS biz_name, COALESCE((be.ad_signals->>'paid_ads_count')::int,0) AS ads FROM magazine_features mf JOIN businesses b ON b.id=mf.business_id LEFT JOIN business_enrichment be ON be.business_id=mf.business_id WHERE COALESCE((be.ad_signals->>'paid_ads_count')::int,0) > 0 ORDER BY ads DESC LIMIT 10`)
+      query(`SELECT mf.id, mf.headline, b.name AS biz_name, COALESCE((be.ad_signals->>'paid_ads_count')::int,0) AS ads FROM magazine_features mf JOIN businesses b ON b.id=mf.business_id LEFT JOIN business_enrichment be ON be.business_id=mf.business_id WHERE COALESCE((be.ad_signals->>'paid_ads_count')::int,0) > 0 ORDER BY ads DESC LIMIT 10`),
+      query(`SELECT mf.id, mf.headline, mf.taps, mf.taps_breakdown, b.name AS biz_name FROM magazine_features mf JOIN businesses b ON b.id=mf.business_id WHERE mf.taps > 0 ORDER BY mf.taps DESC LIMIT 10`)
     ]);
     res.json({
       top_viewed: topViewed.rows,
       hot_verticals: hotVerticals.rows,
       latest: latest.rows,
       longest_editorial: longest.rows,
-      sponsor_candidates: sponsors.rows
+      sponsor_candidates: sponsors.rows,
+      top_tapped: topTapped.rows
     });
   } catch (e: any) {
     res.status(500).json({ error: e.message });

← bd681e7 iter 121+122: og+JSON-LD on per-vertical /issue/:cat + per-f  ·  back to Ventura Corridor  ·  iter 125+126: /sponsor.html public landing + sponsor-inquiry 26b38e1 →