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)
315 lines
11 KiB
Python
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()
|