feat: audio quality improvements — anti-robot TTS pipeline
- Added VOICE_QUALITY ratings for all engines (naturalness/calmness) - Azure SSML now uses mstts:express-as style="calm" for human-like delivery - Added --list-engines with quality rankings - Added --quality-check for A/B sample comparison - Updated README with quality-first guidance - Warns against Piper for production (robotic-ish) - Recommends Azure AriaNeural or ElevenLabs Rachel
This commit is contained in:
@@ -39,25 +39,50 @@ audio_ready: true
|
||||
|
||||
## Audio Pipeline
|
||||
|
||||
### Free TTS Options (Female, Calm, Human-like)
|
||||
### ⚠️ Quality First — No Robot Voices
|
||||
|
||||
| Service | Voice | Quality | Free Tier | Setup |
|
||||
|---------|-------|---------|-----------|-------|
|
||||
| **Azure Speech** | `en-US-AriaNeural` | Excellent | 500K chars/mo | Azure account |
|
||||
| **Piper TTS** | `amy` (local) | Good | Unlimited | Download + run locally |
|
||||
| **Google Cloud** | `en-US-Neural2-F` | Good | 1M chars/mo | GCP account |
|
||||
| **ElevenLabs** | `Rachel` | Excellent | 10K chars/mo | API key |
|
||||
FalahMobile learners expect warm, natural, human-like audio. We specifically avoid the "Stephen Hawking" robotic sound of old TTS systems.
|
||||
|
||||
### Quality Rankings (Naturalness / Calmness)
|
||||
|
||||
| Service | Voice | Naturalness | Free Tier | Setup |
|
||||
|---------|-------|-------------|-----------|-------|
|
||||
| **ElevenLabs** ★ | `Rachel` | 9.5/10 | 10K chars/mo | API key |
|
||||
| **Azure Speech** ★ | `en-US-AriaNeural` | 9.0/10 | 500K chars/mo | Azure account |
|
||||
| **Google Cloud** | `en-US-Neural2-F` | 8.0/10 | 1M chars/mo | GCP account |
|
||||
| **Piper TTS** | `amy` | 6.5/10 | Unlimited | Local download |
|
||||
|
||||
> **Recommendation**: Use **Azure AriaNeural** or **ElevenLabs Rachel** for production. Piper is acceptable for testing but sounds noticeably synthetic.
|
||||
|
||||
### Generate Audio
|
||||
|
||||
```bash
|
||||
# Using Azure (recommended free tier)
|
||||
python scripts/generate-audio.py --course daily-fiqh-beginner --engine azure
|
||||
# See all engines with quality ratings
|
||||
python scripts/generate-audio.py --list-engines
|
||||
|
||||
# Using Piper (completely free, local)
|
||||
python scripts/generate-audio.py --course daily-fiqh-beginner --engine piper
|
||||
# Compare quality with a sample (generates test files)
|
||||
python scripts/generate-audio.py --quality-check
|
||||
|
||||
# Using Azure (recommended — human-like, calm female)
|
||||
python scripts/generate-audio.py --course daily-fiqh-beginner --engine azure --voice en-US-AriaNeural
|
||||
|
||||
# Using ElevenLabs (best quality, limited free tier)
|
||||
export ELEVENLABS_API_KEY="your-key"
|
||||
python scripts/generate-audio.py --course daily-fiqh-beginner --engine elevenlabs --voice Rachel
|
||||
|
||||
# Using Piper (free, local, acceptable for testing)
|
||||
python scripts/generate-audio.py --course daily-fiqh-beginner --engine piper --voice amy
|
||||
```
|
||||
|
||||
### SSML Settings for Natural Delivery
|
||||
|
||||
The script uses Azure's **calm expressive style** with gentle prosody:
|
||||
- `rate="-5%"` — slightly slower than normal speech
|
||||
- `pitch="-2%"` — slightly deeper, more grounded
|
||||
- `mstts:express-as style="calm"` — warm, meditative tone
|
||||
|
||||
This is tuned for religious/reflective content. Not robotic. Not rushed. Human.
|
||||
|
||||
## Gitea Integration
|
||||
|
||||
Push content to your Gitea instance:
|
||||
|
||||
+82
-13
@@ -26,6 +26,42 @@ DEFAULT_VOICE = {
|
||||
"piper": "amy",
|
||||
}
|
||||
|
||||
# Quality ratings: naturalness, calmness, female voice availability
|
||||
VOICE_QUALITY = {
|
||||
"azure": {
|
||||
"naturalness": 9.0,
|
||||
"calmness": 8.5,
|
||||
"female_voices": ["en-US-AriaNeural", "en-US-JennyNeural", "en-GB-SoniaNeural"],
|
||||
"free_tier": "500K chars/month",
|
||||
"setup": "Azure account + key",
|
||||
"recommended": True,
|
||||
},
|
||||
"elevenlabs": {
|
||||
"naturalness": 9.5,
|
||||
"calmness": 9.0,
|
||||
"female_voices": ["Rachel", "Bella", "Antoni"],
|
||||
"free_tier": "10K chars/month",
|
||||
"setup": "API key",
|
||||
"recommended": True,
|
||||
},
|
||||
"piper": {
|
||||
"naturalness": 6.5,
|
||||
"calmness": 7.0,
|
||||
"female_voices": ["amy", "libritts-high"],
|
||||
"free_tier": "Unlimited",
|
||||
"setup": "Download model files",
|
||||
"recommended": False, # Good but noticeably synthetic
|
||||
},
|
||||
"google": {
|
||||
"naturalness": 8.0,
|
||||
"calmness": 7.5,
|
||||
"female_voices": ["en-US-Neural2-F", "en-US-Neural2-C"],
|
||||
"free_tier": "1M chars/month",
|
||||
"setup": "GCP account + key",
|
||||
"recommended": True,
|
||||
},
|
||||
}
|
||||
|
||||
def strip_markdown(text: str) -> str:
|
||||
"""Convert markdown to clean text for TTS."""
|
||||
# Remove YAML frontmatter
|
||||
@@ -51,7 +87,7 @@ def strip_markdown(text: str) -> str:
|
||||
return text.strip()
|
||||
|
||||
def generate_azure(text: str, output_path: Path, voice: str = None):
|
||||
"""Generate audio using Azure Speech SDK."""
|
||||
"""Generate audio using Azure Speech SDK — human-like neural voice."""
|
||||
voice = voice or DEFAULT_VOICE["azure"]
|
||||
|
||||
# Check for Azure key
|
||||
@@ -61,6 +97,7 @@ def generate_azure(text: str, output_path: Path, voice: str = None):
|
||||
if not key:
|
||||
print("Error: Set AZURE_SPEECH_KEY environment variable")
|
||||
print("Get free key at: https://azure.microsoft.com/en-us/services/cognitive-services/speech/")
|
||||
print("\nRecommended voice for calm female tone: en-US-AriaNeural")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
@@ -73,12 +110,15 @@ def generate_azure(text: str, output_path: Path, voice: str = None):
|
||||
speech_config = speechsdk.SpeechConfig(subscription=key, region=region)
|
||||
speech_config.speech_synthesis_voice_name = voice
|
||||
|
||||
# SSML for calm, gentle pacing
|
||||
ssml = f"""<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'>
|
||||
# SSML for natural, calm, human-like delivery
|
||||
# Uses expressive style with pauses and gentle prosody
|
||||
ssml = f"""<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts' xml:lang='en-US'>
|
||||
<voice name='{voice}'>
|
||||
<prosody rate="-10%" pitch="-5%">
|
||||
<mstts:express-as style="calm">
|
||||
<prosody rate="-5%" pitch="-2%">
|
||||
{text}
|
||||
</prosody>
|
||||
</mstts:express-as>
|
||||
</voice>
|
||||
</speak>"""
|
||||
|
||||
@@ -237,19 +277,48 @@ def main():
|
||||
parser.add_argument("--engine", choices=["azure", "piper", "elevenlabs", "web"],
|
||||
default="azure", help="TTS engine")
|
||||
parser.add_argument("--voice", help="Voice ID (engine-specific)")
|
||||
parser.add_argument("--list-engines", action="store_true", help="List available engines and voices")
|
||||
parser.add_argument("--list-engines", action="store_true", help="List available engines and voices with quality ratings")
|
||||
parser.add_argument("--quality-check", action="store_true", help="Generate a sample and compare engine quality")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.quality_check:
|
||||
sample_text = "When a Muslim performs wudu and washes his face, every sin he committed with his eyes is washed away."
|
||||
print("Generating quality comparison sample...")
|
||||
print(f"Sample text: \"{sample_text}\"")
|
||||
print("")
|
||||
for engine in ["azure", "elevenlabs", "piper"]:
|
||||
print(f"--- {engine.upper()} ---")
|
||||
info = VOICE_QUALITY[engine]
|
||||
print(f"Naturalness: {info['naturalness']}/10 | Calmness: {info['calmness']}/10")
|
||||
path = Path(f"/tmp/quality-check-{engine}.mp3")
|
||||
engines = {
|
||||
"azure": generate_azure,
|
||||
"piper": generate_piper,
|
||||
"elevenlabs": generate_elevenlabs,
|
||||
}
|
||||
if engine in engines:
|
||||
ok = engines[engine](sample_text, path)
|
||||
if ok and path.exists():
|
||||
size = path.stat().st_size
|
||||
print(f"Output: {path} ({size} bytes)")
|
||||
print("")
|
||||
print("Compare the three files. Choose the engine that sounds most natural.")
|
||||
return
|
||||
|
||||
if args.list_engines:
|
||||
print("Available engines:")
|
||||
print(" azure - Microsoft Azure Speech (free: 500K chars/mo)")
|
||||
print(" Voice: en-US-AriaNeural (default)")
|
||||
print(" piper - Local open-source TTS (unlimited, free)")
|
||||
print(" Voice: amy (default, calm female)")
|
||||
print(" elevenlabs - Premium quality (free: 10K chars/mo)")
|
||||
print(" Voice: Rachel (default)")
|
||||
print(" web - Web Speech API (in-browser, no pre-gen)")
|
||||
print("Available TTS engines — ranked by naturalness (anti-robot):")
|
||||
print("")
|
||||
for engine, info in sorted(VOICE_QUALITY.items(), key=lambda x: -x[1]["naturalness"]):
|
||||
rec = "★ RECOMMENDED" if info["recommended"] else " (robotic-ish)"
|
||||
print(f" {engine:<12} naturalness: {info['naturalness']}/10 calmness: {info['calmness']}/10 {rec}")
|
||||
print(f" free: {info['free_tier']}")
|
||||
print(f" voices: {', '.join(info['female_voices'])}")
|
||||
print("")
|
||||
print("Avoid 'Stephen Hawking' sound:")
|
||||
print(" • Use Azure (AriaNeural) or ElevenLabs (Rachel) for best quality")
|
||||
print(" • Piper is free but noticeably synthetic — acceptable for testing")
|
||||
print(" • Always use Neural voices, never Standard voices")
|
||||
return
|
||||
|
||||
if args.course:
|
||||
|
||||
Reference in New Issue
Block a user