← back to Voice Clone
say.sh
36 lines
#!/bin/bash
# say.sh — generate narration MP3 in Steve's cloned voice.
# Usage: ./say.sh "text to narrate" output.mp3
# Env: ELEVENLABS_API_KEY, ELEVENLABS_VOICE_STEVE (auto-loaded from secrets-manager)
set -euo pipefail
TEXT=${1:?usage: say.sh \"text\" [output.mp3]}
OUT=${2:-narration.mp3}
KEY=$(grep '^ELEVENLABS_API_KEY=' ~/Projects/secrets-manager/.env | cut -d= -f2)
VID=$(grep '^ELEVENLABS_VOICE_STEVE=' ~/Projects/secrets-manager/.env | cut -d= -f2)
[ -z "$KEY" ] && { echo "[abort] ELEVENLABS_API_KEY missing"; exit 1; }
[ -z "$VID" ] && { echo "[abort] ELEVENLABS_VOICE_STEVE not set — run clone.sh first"; exit 1; }
CHARS=${#TEXT}
echo "rendering $CHARS chars → $OUT (voice=$VID)"
curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/$VID" \
-H "xi-api-key: $KEY" -H "Content-Type: application/json" \
-d "$(python3 -c "
import json,sys
print(json.dumps({
'text': '''$TEXT''',
'model_id': 'eleven_multilingual_v2',
'voice_settings': {'stability': 0.45, 'similarity_boost': 0.85, 'style': 0.15, 'use_speaker_boost': True}
}))")" \
-o "$OUT"
if [ -s "$OUT" ]; then
SIZE=$(du -h "$OUT" | awk '{print $1}')
echo "✓ $OUT ($SIZE)"
command -v ffprobe >/dev/null && DUR=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$OUT" 2>/dev/null) && echo " duration: ${DUR}s"
else
echo "✗ render failed"; exit 2
fi