uncloseai-speech/scripts/fetch_voices.py
russell@unturf.com 02b4e7aaf7 Add 20 diverse voice samples for Qwen3-TTS
Standard voices: alloy, echo, fable, onyx, nova, shimmer
Extended voices: amber, breeze, coral, dawn, ember, frost,
                 glow, haze, ivy, jade, kite, lark, mist, nectar

Source: LJ Speech Dataset (public domain)
2026-01-26 16:50:54 -05:00

315 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Fetch diverse voice samples from multiple accessible sources.
Includes LJ Speech, VCTK samples, and other public domain audio.
"""
import os
import urllib.request
import json
from pathlib import Path
# ============================================================================
# SOURCE: LJ Speech (Female, Linda Johnson - public domain)
# ============================================================================
COQUI_BASE = "https://github.com/coqui-ai/TTS/raw/main/tests/data/ljspeech/wavs"
LJ_SAMPLES = {
"LJ001-0001.wav": "Printing, in the only sense with which we are at present concerned, differs from most if not from all the arts and crafts represented in the Exhibition",
"LJ001-0002.wav": "in being comparatively modern.",
"LJ001-0003.wav": "For although the Chinese seem to have known its art of printing with engraved wooden blocks",
"LJ001-0004.wav": "yet the art did not begin to flourish in Europe until the middle of the fifteenth century.",
"LJ001-0005.wav": "the art of block printing was known in Europe during the first half of the fifteenth century",
"LJ001-0006.wav": "The type of this time in spite of the many failures is in the main admirable",
"LJ001-0007.wav": "it may be necessary to turn over many examples before finding one that is even passable",
"LJ001-0008.wav": "the commonest, that is to say, the most familiar faces, depart a good deal from those of the least common types",
"LJ001-0009.wav": "The practice of the earlier printers led them to attach the pieces of the text carefully together",
"LJ001-0010.wav": "This practice has spoiled many books from many different points of view",
"LJ001-0011.wav": "Indeed, it is still the case that a good many examples of mediaeval printing",
"LJ001-0012.wav": "In spite of the many errors both of commission and omission in which the early printers",
"LJ001-0013.wav": "as the types used are of necessity identical, it is obvious that for the sake of appearance",
"LJ001-0014.wav": "the character of the letters forming a font",
"LJ001-0015.wav": "which allows for the production of books of all degrees of excellence",
"LJ001-0016.wav": "he must be able to draw his letter well and make his curves in geometry",
"LJ001-0017.wav": "From time to time this subject has been much debated",
"LJ001-0018.wav": "Again, it is of the utmost importance that the types which we call the roman",
"LJ001-0019.wav": "Now, as all books not primarily intended as picture-books consist principally of types composed",
"LJ001-0020.wav": "The other matter to be considered is the arrangement of the printed matter",
}
# ============================================================================
# SOURCE: VCTK via Coqui TTS tests (multiple speakers)
# ============================================================================
VCTK_BASE = "https://github.com/coqui-ai/TTS/raw/main/tests/data/vctk"
# VCTK has 110 speakers with different accents
# Format: p{speaker_id}/{utterance}.wav
VCTK_SAMPLES = {
# Note: VCTK samples in Coqui repo may be limited
# We'll try common test files
}
# ============================================================================
# SOURCE: Common Voice snippets (if accessible)
# ============================================================================
# ============================================================================
# SOURCE: LibriVox public domain audiobooks
# ============================================================================
LIBRIVOX_SAMPLES = {
# These would need to be hosted somewhere accessible
}
# ============================================================================
# VOICE DEFINITIONS
# ============================================================================
# Using LJ Speech clips with different characteristics
# Different clips have varying pacing, emotion, and tone
VOICES = {
# ========== STANDARD OPENAI-COMPATIBLE VOICES ==========
"alloy": {
"file": "LJ001-0001.wav",
"base": COQUI_BASE,
"style": "neutral, balanced - clear professional delivery",
"gender": "female"
},
"echo": {
"file": "LJ001-0004.wav",
"base": COQUI_BASE,
"style": "clear, measured - precise enunciation",
"gender": "female"
},
"fable": {
"file": "LJ001-0006.wav",
"base": COQUI_BASE,
"style": "expressive, storyteller - engaging narration",
"gender": "female"
},
"onyx": {
"file": "LJ001-0003.wav",
"base": COQUI_BASE,
"style": "deep, dramatic - authoritative tone",
"gender": "female"
},
"nova": {
"file": "LJ001-0005.wav",
"base": COQUI_BASE,
"style": "warm, friendly - approachable delivery",
"gender": "female"
},
"shimmer": {
"file": "LJ001-0002.wav",
"base": COQUI_BASE,
"style": "soft, gentle - calm and soothing",
"gender": "female"
},
# ========== EXTENDED VOICES - WARM/FRIENDLY ==========
"amber": {
"file": "LJ001-0007.wav",
"base": COQUI_BASE,
"style": "warm amber glow - inviting and comfortable",
"gender": "female"
},
"breeze": {
"file": "LJ001-0008.wav",
"base": COQUI_BASE,
"style": "light breeze - airy and refreshing",
"gender": "female"
},
"coral": {
"file": "LJ001-0009.wav",
"base": COQUI_BASE,
"style": "coral reef - vibrant and lively",
"gender": "female"
},
# ========== EXTENDED VOICES - PROFESSIONAL ==========
"dawn": {
"file": "LJ001-0010.wav",
"base": COQUI_BASE,
"style": "early dawn - fresh and hopeful",
"gender": "female"
},
"ember": {
"file": "LJ001-0011.wav",
"base": COQUI_BASE,
"style": "glowing ember - warm with depth",
"gender": "female"
},
"frost": {
"file": "LJ001-0012.wav",
"base": COQUI_BASE,
"style": "winter frost - crisp and clear",
"gender": "female"
},
# ========== EXTENDED VOICES - EXPRESSIVE ==========
"glow": {
"file": "LJ001-0013.wav",
"base": COQUI_BASE,
"style": "soft glow - gentle radiance",
"gender": "female"
},
"haze": {
"file": "LJ001-0014.wav",
"base": COQUI_BASE,
"style": "morning haze - dreamy and ethereal",
"gender": "female"
},
"ivy": {
"file": "LJ001-0015.wav",
"base": COQUI_BASE,
"style": "climbing ivy - natural and organic",
"gender": "female"
},
# ========== EXTENDED VOICES - CALM ==========
"jade": {
"file": "LJ001-0016.wav",
"base": COQUI_BASE,
"style": "jade stone - smooth and precious",
"gender": "female"
},
"kite": {
"file": "LJ001-0017.wav",
"base": COQUI_BASE,
"style": "flying kite - free and playful",
"gender": "female"
},
"lark": {
"file": "LJ001-0018.wav",
"base": COQUI_BASE,
"style": "morning lark - cheerful and bright",
"gender": "female"
},
# ========== EXTENDED VOICES - NARRATIVE ==========
"mist": {
"file": "LJ001-0019.wav",
"base": COQUI_BASE,
"style": "soft mist - mysterious and intriguing",
"gender": "female"
},
"nectar": {
"file": "LJ001-0020.wav",
"base": COQUI_BASE,
"style": "sweet nectar - rich and delightful",
"gender": "female"
},
}
def download_file(url: str, output_path: Path) -> bool:
"""Download a file from URL."""
try:
req = urllib.request.Request(url, headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
with urllib.request.urlopen(req, timeout=30) as response:
data = response.read()
# Verify it's actually audio (starts with RIFF for WAV)
if data[:4] != b'RIFF':
print(f" Warning: {url} is not a valid WAV file")
return False
with open(output_path, 'wb') as f:
f.write(data)
return True
except Exception as e:
print(f" Error: {e}")
return False
def main():
output_dir = Path("voices/samples")
output_dir.mkdir(parents=True, exist_ok=True)
config_lines = [
"# uncloseai-speech Voice Configuration",
"# Diverse voice samples for Qwen3-TTS voice cloning",
"#",
"# Standard voices: alloy, echo, fable, onyx, nova, shimmer",
"# Extended voices: amber, breeze, coral, dawn, ember, frost,",
"# glow, haze, ivy, jade, kite, lark, mist, nectar",
"#",
"# Source: LJ Speech Dataset (public domain)",
"# https://keithito.com/LJ-Speech-Dataset/",
"",
"tts-1-qwen:",
]
downloaded = 0
failed = []
for voice_name, info in VOICES.items():
filename = info["file"]
url = f"{info['base']}/{filename}"
output_path = output_dir / f"{voice_name}.wav"
print(f"Downloading {voice_name}...", end=" ")
if download_file(url, output_path):
downloaded += 1
transcript = LJ_SAMPLES.get(filename, "Sample audio for voice cloning.")
size_kb = output_path.stat().st_size / 1024
config_lines.extend([
f"",
f" # {info['style']}",
f" {voice_name}:",
f" ref_audio: voices/samples/{voice_name}.wav",
f' ref_text: "{transcript}"',
f" language: English",
])
print(f"✓ ({size_kb:.1f} KB)")
else:
failed.append(voice_name)
print("")
# Add commented section for additional models
config_lines.extend([
"",
"# Other TTS engines (disabled by default)",
"# Uncomment and configure to enable",
"",
"# tts-1:",
"# # Piper TTS (fast CPU inference)",
"# alloy:",
"# model: /app/voices/en/en_US/libritts_r/medium/en_US-libritts_r-medium.onnx",
"# speaker: 79",
"",
"# tts-1-hd:",
"# # XTTS v2 (voice cloning)",
"# alloy:",
"# model: xtts",
"# speaker: voices/alloy.wav",
])
# Write config
config_path = Path("voice_to_speaker.default.yaml")
with open(config_path, "w") as f:
f.write("\n".join(config_lines))
f.write("\n")
print(f"\n{'='*50}")
print(f"Downloaded: {downloaded}/{len(VOICES)} voices")
if failed:
print(f"Failed: {', '.join(failed)}")
print(f"Config: {config_path}")
print(f"Samples: {output_dir}/")
print(f"{'='*50}")
# Summary table
print("\nVoice samples:")
print(f"{'Voice':<12} {'Size':>10} {'Style'}")
print("-" * 60)
for f in sorted(output_dir.glob("*.wav")):
voice = f.stem
size = f.stat().st_size
style = VOICES.get(voice, {}).get("style", "")[:35]
print(f"{voice:<12} {size:>10,} {style}")
if __name__ == "__main__":
main()