archy/neode-ui/scripts/generate-welcome-speech.js

78 lines
2.4 KiB
JavaScript
Raw Normal View History

release(v1.7.41-alpha): post-OTA auto-rollback so a bad release cannot strand the fleet Closes failure mode FM5 from docs/bulletproof-containers.md: the v1.7.38 + v1.7.39 rollouts left every affected node on an unreachable UI (nginx 500) with no recovery path short of SSH. This release adds a self-check guardrail to the update flow. What changed: - apply_update() writes a pending-verify marker with old+new version and a 150s deadline immediately before scheduling the service restart. - verify_pending_update() runs from main.rs startup. If the marker is present and within its freshness window, the new binary waits 15s for nginx + backend to settle, then probes https://127.0.0.1/ every 5s for up to 90s (self-signed certs accepted). - On any probe success within the window, the marker is cleared and nothing else happens. - On window-exhaust, the new binary: 1. Moves the broken /opt/archipelago/web-ui to web-ui.failed.<ts> (quarantined, not deleted, so we can post-mortem). 2. Restores web-ui.bak on top of web-ui. 3. Calls rollback_update() to restore the previous binary. 4. Updates state.current_version to reflect the rollback. 5. systemctl --no-block restart archipelago so the OLD binary boots. - Markers older than 10 minutes are treated as stale and cleared without probing, so a crashed-during-startup marker from weeks ago cannot spontaneously roll back a healthy node on a later reboot. - rollback_update() binary copy now goes through host_sudo instead of tokio::fs::copy, so it escapes the service's ProtectSystem=strict mount namespace. Without this, the rollback silently failed with EROFS on /usr/local/bin and orphaned the rollback - the exact opposite of what auto-rollback is for. Tests: 4 new unit tests in update::tests covering marker round-trip, absent-marker noop, no-panic on verify_pending_update with nothing to verify, and an invariant assert that the 90s probe window stays below the 600s stale threshold. All passing. Side fix: scripts/create-release-manifest.sh was dying with exit 141 (SIGPIPE from tar tvzf pipe head pipe awk) under set -euo pipefail. Replaced with a single awk NR==1 that doesn't short-circuit the upstream pipe, so the release-build flow is idempotent again.
2026-04-22 16:14:35 -04:00
#!/usr/bin/env node
/**
* Generate "Welcome Noderunner" speech using ElevenLabs AI voice.
* Slower, softer, sci-fi style with reverb/echo effects.
*
* Usage:
* ELEVENLABS_API_KEY=your_key node scripts/generate-welcome-speech.js
*
* Optional voice ID (browse https://elevenlabs.io/voice-library/sensual):
* ELEVENLABS_VOICE_ID=voice_id node scripts/generate-welcome-speech.js
*/
import { writeFileSync, mkdirSync, readFileSync, unlinkSync } from 'fs'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { execSync } from 'child_process'
const __dirname = dirname(fileURLToPath(import.meta.url))
const API_KEY = process.env.ELEVENLABS_API_KEY
// Sarah - mature, reassuring, confident female (softer than Rachel)
const VOICE_ID = process.env.ELEVENLABS_VOICE_ID || 'EXAVITQu4vr4xnSDxMaL'
const OUTPUT_PATH = join(__dirname, '../public/assets/audio/welcome-noderunner.mp3')
const RAW_PATH = join(__dirname, '../public/assets/audio/welcome-noderunner-raw.mp3')
if (!API_KEY) {
console.error('Set ELEVENLABS_API_KEY (get a free key at elevenlabs.io)')
process.exit(1)
}
// Slower (0.78), softer (higher stability 0.65), more expressive (style 0.6)
const res = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}?output_format=mp3_44100_128`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'xi-api-key': API_KEY,
},
body: JSON.stringify({
text: 'Welcome Noderunner',
model_id: 'eleven_multilingual_v2',
voice_settings: {
stability: 0.65,
similarity_boost: 0.8,
style: 0.6,
use_speaker_boost: true,
speed: 0.7,
},
}),
}
)
if (!res.ok) {
const err = await res.text()
console.error('ElevenLabs API error:', res.status, err)
process.exit(1)
}
const buf = Buffer.from(await res.arrayBuffer())
mkdirSync(dirname(OUTPUT_PATH), { recursive: true })
writeFileSync(RAW_PATH, buf)
// Add sci-fi reverb: dense short delays that blend (no distinct echo)
try {
execSync(
`ffmpeg -y -i "${RAW_PATH}" -af "aecho=0.6:0.15:25|45|70:0.55|0.45|0.35,highpass=f=80,equalizer=f=4000:t=q:w=1:g=-1" -q:a 2 "${OUTPUT_PATH}" 2>/dev/null`,
{ stdio: 'pipe' }
)
unlinkSync(RAW_PATH)
} catch {
writeFileSync(OUTPUT_PATH, buf)
try { unlinkSync(RAW_PATH) } catch {}
}
console.log('Generated:', OUTPUT_PATH)
console.log('Add this file to git and deploy.')