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
+84 -15
View File
@@ -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%">
{text}
</prosody>
<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: