← 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
A data/magazine-audio/1-6e3ce8d703a7.m4aM public/scoreboard.htmlM src/server/index.ts
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 →