← back to Cncp Failures Mockups
server.js
160 lines
// CNCP Failures Mockups — 6-variant web viewer.
// Serves /public statically + a /data endpoint returning sample-data.json
// so every mockup can render against the same realistic seed.
const express = require('express');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const { execFile } = require('child_process');
const PORT = parseInt(process.env.PORT, 10) || 9771;
const BIND = process.env.BIND || '127.0.0.1';
const CNCP_URL = process.env.CNCP_URL || 'http://127.0.0.1:3333';
// ElevenLabs config — Steve's cloned voice + per-band pitch/tempo shift via ffmpeg.
const EL_API_KEY = process.env.ELEVENLABS_API_KEY || readEnvFromSecrets('ELEVENLABS_API_KEY');
const STEVE_VOICE_ID = (() => {
try {
const active = JSON.parse(fs.readFileSync(path.join(process.env.HOME, '.claude/skills/clone-voice/active.json'), 'utf-8'));
return active.engines?.elevenlabs?.voice_id || null;
} catch { return null; }
})();
function readEnvFromSecrets(key) {
try {
const raw = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf-8');
const m = raw.match(new RegExp('^' + key + '=(.*)$', 'm'));
return m ? m[1].trim() : null;
} catch { return null; }
}
// Per-band ElevenLabs voice ids — natural stock voices picked from the library.
// No pitch-shifting; each age band gets a voice that naturally sounds the right age.
// Picked from `curl /v1/voices` after Steve said "just use suggested 11 labs voice".
const BAND_VOICE_IDS = {
toddler: 'c6cUlh8iJa74kYbBwDaZ', // Oceano — A very young narrator
kid: 'cgSgspJ2msm6clMCkdW9', // Jessica — Playful, Bright, Warm (young female)
tween: 'FGY2WhTYpPnrIDTdsKH5', // Laura — Enthusiast, Quirky Attitude (young female)
teen: 'TX3LPaxmHKxFdv7VOQHJ', // Liam — Energetic, Social Media Creator (young male)
adult: 'EXAVITQu4vr4xnSDxMaL', // Sarah — Mature, Reassuring, Confident
mature: 'JBFqnCBsd6RMkjVDRZzb', // George — Warm, Captivating Storyteller (middle-aged male)
senior: 'nPczCjzI2devNBz1zQrb', // Brian — Deep, Resonant and Comforting
elder: 'pqHfZKP75CvOlQylNhV4' // Bill — Wise, Mature, Balanced ("old" bucket)
};
const AUDIO_CACHE = path.join(__dirname, 'audio-cache');
fs.mkdirSync(AUDIO_CACHE, { recursive: true });
const app = express();
// 404-guard — never serve editor/build snapshot artifacts even if one accidentally
// lands under public/. Catches *.bak, *.bak.*, *.pre-*, *.swp paths anywhere in the URL.
app.use((req, res, next) => {
if (/\.(bak|swp)(\.|$|\/)/i.test(req.path) || /\.pre-/i.test(req.path)) {
return res.status(404).type('text/plain').send('Not found');
}
next();
});
app.use(express.static(path.join(__dirname, 'public')));
// GET /data
// default → LIVE from CNCP (failures + hermes status), merged in same schema as sample-data.json.
// ?seed=1 → force the static sample. Useful for screenshots / pixel-stable design comparison.
// if live fetch fails (CNCP down) → automatically falls back to seed with `_live: false, _fallback: true`.
app.get('/data', async (req, res) => {
const wantSeed = req.query.seed === '1';
res.set('Cache-Control', 'no-store');
if (wantSeed) return sendSeed(res, false);
try {
const ctrl = AbortSignal.timeout(2500);
const [failuresR, hermesR] = await Promise.all([
fetch(CNCP_URL + '/api/failures', { signal: ctrl }),
fetch(CNCP_URL + '/api/hermes/status', { signal: ctrl })
]);
if (!failuresR.ok) throw new Error('failures HTTP ' + failuresR.status);
if (!hermesR.ok) throw new Error('hermes HTTP ' + hermesR.status);
const failures = await failuresR.json();
const hermes = await hermesR.json();
return res.type('application/json').send(JSON.stringify({
_live: true,
_fetchedAt: Date.now(),
_source: 'cncp@' + CNCP_URL,
failures,
hermes
}));
} catch (e) {
return sendSeed(res, true, e.message);
}
});
function sendSeed(res, isFallback, errMsg) {
try {
const raw = JSON.parse(fs.readFileSync(path.join(__dirname, 'sample-data.json'), 'utf-8'));
res.type('application/json').send(JSON.stringify({
_live: false,
_fallback: !!isFallback,
_fallbackError: errMsg || null,
_fetchedAt: Date.now(),
_source: 'sample-data.json',
...raw
}));
} catch (e) {
res.status(500).json({ error: e.message });
}
}
// GET /api/voice?text=...&band=... → MP3 from the per-band stock ElevenLabs voice (no pitch shift).
// Caches by hash(text|band) so re-saying a phrase is free after the first generation.
app.get('/api/voice', async (req, res) => {
const text = String(req.query.text || '').slice(0, 400).trim();
const band = String(req.query.band || 'adult').trim();
if (!text) return res.status(400).json({ error: 'text required' });
if (!EL_API_KEY) return res.status(503).json({ error: 'ELEVENLABS_API_KEY not configured' });
const voiceId = BAND_VOICE_IDS[band];
if (!voiceId) return res.status(400).json({ error: 'unknown band: ' + band });
const hash = crypto.createHash('sha256').update(text + '|' + voiceId).digest('hex').slice(0, 24);
const cached = path.join(AUDIO_CACHE, `${band}-${hash}.mp3`);
res.set('Cache-Control', 'public, max-age=86400');
res.set('Content-Type', 'audio/mpeg');
if (fs.existsSync(cached)) {
res.set('X-Cache', 'HIT');
return fs.createReadStream(cached).pipe(res);
}
try {
const r = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
method: 'POST',
headers: { 'xi-api-key': EL_API_KEY, 'Content-Type': 'application/json', 'Accept': 'audio/mpeg' },
body: JSON.stringify({
text,
model_id: 'eleven_turbo_v2_5',
voice_settings: { stability: 0.55, similarity_boost: 0.85, style: 0.0, use_speaker_boost: true }
})
});
if (!r.ok) {
const body = await r.text().catch(() => '');
console.error('[voice]', band, voiceId, r.status, body.slice(0, 200));
return res.status(502).json({ error: 'elevenlabs ' + r.status, detail: body.slice(0, 200) });
}
const mp3 = Buffer.from(await r.arrayBuffer());
fs.writeFileSync(cached, mp3);
res.set('X-Cache', 'MISS');
return res.send(mp3);
} catch (e) {
console.error('[voice] threw:', e.message);
return res.status(502).json({ error: 'elevenlabs unreachable: ' + e.message });
}
});
app.get('/health', (_, res) => res.json({
ok: true,
variants: 7,
cncp: CNCP_URL,
voice: { el_key_present: !!EL_API_KEY, bands: BAND_VOICE_IDS }
}));
app.listen(PORT, BIND, () => {
console.log(`cncp-failures-mockups listening at http://${BIND}:${PORT}/`);
});