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
|
## 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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user