← 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