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:
wmj2024
2026-06-28 02:09:10 +08:00
parent cb22804a1f
commit cd35b46393
2 changed files with 120 additions and 26 deletions
+36 -11
View File
@@ -39,25 +39,50 @@ audio_ready: true
## Audio Pipeline ## Audio Pipeline
### Free TTS Options (Female, Calm, Human-like) ### ⚠️ Quality First — No Robot Voices
| Service | Voice | Quality | Free Tier | Setup | FalahMobile learners expect warm, natural, human-like audio. We specifically avoid the "Stephen Hawking" robotic sound of old TTS systems.
|---------|-------|---------|-----------|-------|
| **Azure Speech** | `en-US-AriaNeural` | Excellent | 500K chars/mo | Azure account | ### Quality Rankings (Naturalness / Calmness)
| **Piper TTS** | `amy` (local) | Good | Unlimited | Download + run locally |
| **Google Cloud** | `en-US-Neural2-F` | Good | 1M chars/mo | GCP account | | Service | Voice | Naturalness | Free Tier | Setup |
| **ElevenLabs** | `Rachel` | Excellent | 10K chars/mo | API key | |---------|-------|-------------|-----------|-------|
| **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 ### Generate Audio
```bash ```bash
# Using Azure (recommended free tier) # See all engines with quality ratings
python scripts/generate-audio.py --course daily-fiqh-beginner --engine azure python scripts/generate-audio.py --list-engines
# Using Piper (completely free, local) # Compare quality with a sample (generates test files)
python scripts/generate-audio.py --course daily-fiqh-beginner --engine piper 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 ## Gitea Integration
Push content to your Gitea instance: Push content to your Gitea instance:
+84 -15
View File
@@ -26,6 +26,42 @@ DEFAULT_VOICE = {
"piper": "amy", "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: def strip_markdown(text: str) -> str:
"""Convert markdown to clean text for TTS.""" """Convert markdown to clean text for TTS."""
# Remove YAML frontmatter # Remove YAML frontmatter
@@ -51,7 +87,7 @@ def strip_markdown(text: str) -> str:
return text.strip() return text.strip()
def generate_azure(text: str, output_path: Path, voice: str = None): 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"] voice = voice or DEFAULT_VOICE["azure"]
# Check for Azure key # Check for Azure key
@@ -61,6 +97,7 @@ def generate_azure(text: str, output_path: Path, voice: str = None):
if not key: if not key:
print("Error: Set AZURE_SPEECH_KEY environment variable") print("Error: Set AZURE_SPEECH_KEY environment variable")
print("Get free key at: https://azure.microsoft.com/en-us/services/cognitive-services/speech/") 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) sys.exit(1)
try: 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 = speechsdk.SpeechConfig(subscription=key, region=region)
speech_config.speech_synthesis_voice_name = voice speech_config.speech_synthesis_voice_name = voice
# SSML for calm, gentle pacing # SSML for natural, calm, human-like delivery
ssml = f"""<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'> # 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}'> <voice name='{voice}'>
<prosody rate="-10%" pitch="-5%"> <mstts:express-as style="calm">
{text} <prosody rate="-5%" pitch="-2%">
</prosody> {text}
</prosody>
</mstts:express-as>
</voice> </voice>
</speak>""" </speak>"""
@@ -237,19 +277,48 @@ def main():
parser.add_argument("--engine", choices=["azure", "piper", "elevenlabs", "web"], parser.add_argument("--engine", choices=["azure", "piper", "elevenlabs", "web"],
default="azure", help="TTS engine") default="azure", help="TTS engine")
parser.add_argument("--voice", help="Voice ID (engine-specific)") 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() 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: if args.list_engines:
print("Available engines:") print("Available TTS engines — ranked by naturalness (anti-robot):")
print(" azure - Microsoft Azure Speech (free: 500K chars/mo)") print("")
print(" Voice: en-US-AriaNeural (default)") for engine, info in sorted(VOICE_QUALITY.items(), key=lambda x: -x[1]["naturalness"]):
print(" piper - Local open-source TTS (unlimited, free)") rec = "★ RECOMMENDED" if info["recommended"] else " (robotic-ish)"
print(" Voice: amy (default, calm female)") print(f" {engine:<12} naturalness: {info['naturalness']}/10 calmness: {info['calmness']}/10 {rec}")
print(" elevenlabs - Premium quality (free: 10K chars/mo)") print(f" free: {info['free_tier']}")
print(" Voice: Rachel (default)") print(f" voices: {', '.join(info['female_voices'])}")
print(" web - Web Speech API (in-browser, no pre-gen)") 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 return
if args.course: if args.course: