- Implements POST /v1/audio/speech endpoint (OpenAI API compatible). - Integrates Kokoro and XTTS engines (including dependencies and implementations). - Updates main application to register new engines and router. - Adds unit tests for OpenAI compatibility. - Updates requirements.txt for new engines.
206 lines
8.6 KiB
Python
206 lines
8.6 KiB
Python
"""
|
||
NovaAi – TTS-Engine-Hub
|
||
engines/kokoro_voices.py
|
||
Version: v0.1.0
|
||
|
||
Description:
|
||
Voice metadata for Kokoro TTS engine.
|
||
Complete list of 54 voices across 8 languages with metadata.
|
||
|
||
Source: https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md
|
||
|
||
Author: Claude Code (Anthropic)
|
||
Date: 2025-12-05
|
||
"""
|
||
|
||
# Complete list of all 54 Kokoro voices
|
||
ALL_VOICES = [
|
||
# American English (20 voices)
|
||
'af_heart', 'af_alloy', 'af_aoede', 'af_bella', 'af_jessica', 'af_kore',
|
||
'af_nicole', 'af_nova', 'af_river', 'af_sarah', 'af_sky',
|
||
'am_adam', 'am_echo', 'am_eric', 'am_fenrir', 'am_liam', 'am_michael',
|
||
'am_onyx', 'am_puck', 'am_santa',
|
||
|
||
# British English (8 voices)
|
||
'bf_alice', 'bf_emma', 'bf_isabella', 'bf_lily',
|
||
'bm_daniel', 'bm_fable', 'bm_george', 'bm_lewis',
|
||
|
||
# Japanese (5 voices)
|
||
'jf_alpha', 'jf_gongitsune', 'jf_nezumi', 'jf_tebukuro',
|
||
'jm_kumo',
|
||
|
||
# Mandarin Chinese (8 voices)
|
||
'zf_xiaobei', 'zf_xiaoni', 'zf_xiaoxiao', 'zf_xiaoyi',
|
||
'zm_yunjian', 'zm_yunxi', 'zm_yunxia', 'zm_yunyang',
|
||
|
||
# Spanish (3 voices)
|
||
'ef_dora', 'em_alex', 'em_santa',
|
||
|
||
# French (1 voice)
|
||
'ff_siwis',
|
||
|
||
# Hindi (4 voices)
|
||
'hf_alpha', 'hf_beta', 'hm_omega', 'hm_psi',
|
||
|
||
# Italian (2 voices)
|
||
'if_sara', 'im_nicola',
|
||
|
||
# Brazilian Portuguese (3 voices)
|
||
'pf_dora', 'pm_alex', 'pm_santa',
|
||
]
|
||
|
||
# Voice metadata with gender and language information
|
||
VOICE_METADATA = {
|
||
# American English - Female
|
||
'af_heart': {'gender': 'F', 'language': 'en-us', 'description': 'Clear, warm female voice'},
|
||
'af_alloy': {'gender': 'F', 'language': 'en-us', 'description': 'Professional female voice'},
|
||
'af_aoede': {'gender': 'F', 'language': 'en-us', 'description': 'Expressive female voice'},
|
||
'af_bella': {'gender': 'F', 'language': 'en-us', 'description': 'Warm, friendly female voice'},
|
||
'af_jessica': {'gender': 'F', 'language': 'en-us', 'description': 'Natural female voice'},
|
||
'af_kore': {'gender': 'F', 'language': 'en-us', 'description': 'Energetic female voice'},
|
||
'af_nicole': {'gender': 'F', 'language': 'en-us', 'description': 'Smooth female voice'},
|
||
'af_nova': {'gender': 'F', 'language': 'en-us', 'description': 'Bright female voice'},
|
||
'af_river': {'gender': 'F', 'language': 'en-us', 'description': 'Calm female voice'},
|
||
'af_sarah': {'gender': 'F', 'language': 'en-us', 'description': 'Professional female voice'},
|
||
'af_sky': {'gender': 'F', 'language': 'en-us', 'description': 'Cheerful female voice'},
|
||
|
||
# American English - Male
|
||
'am_adam': {'gender': 'M', 'language': 'en-us', 'description': 'Deep male voice'},
|
||
'am_echo': {'gender': 'M', 'language': 'en-us', 'description': 'Resonant male voice'},
|
||
'am_eric': {'gender': 'M', 'language': 'en-us', 'description': 'Professional male voice'},
|
||
'am_fenrir': {'gender': 'M', 'language': 'en-us', 'description': 'Strong male voice'},
|
||
'am_liam': {'gender': 'M', 'language': 'en-us', 'description': 'Friendly male voice'},
|
||
'am_michael': {'gender': 'M', 'language': 'en-us', 'description': 'Clear male voice'},
|
||
'am_onyx': {'gender': 'M', 'language': 'en-us', 'description': 'Smooth male voice'},
|
||
'am_puck': {'gender': 'M', 'language': 'en-us', 'description': 'Playful male voice'},
|
||
'am_santa': {'gender': 'M', 'language': 'en-us', 'description': 'Warm, jolly male voice'},
|
||
|
||
# British English - Female
|
||
'bf_alice': {'gender': 'F', 'language': 'en-gb', 'description': 'British female voice'},
|
||
'bf_emma': {'gender': 'F', 'language': 'en-gb', 'description': 'British female voice'},
|
||
'bf_isabella': {'gender': 'F', 'language': 'en-gb', 'description': 'British female voice'},
|
||
'bf_lily': {'gender': 'F', 'language': 'en-gb', 'description': 'British female voice'},
|
||
|
||
# British English - Male
|
||
'bm_daniel': {'gender': 'M', 'language': 'en-gb', 'description': 'British male voice'},
|
||
'bm_fable': {'gender': 'M', 'language': 'en-gb', 'description': 'British male voice'},
|
||
'bm_george': {'gender': 'M', 'language': 'en-gb', 'description': 'British male voice'},
|
||
'bm_lewis': {'gender': 'M', 'language': 'en-gb', 'description': 'British male voice'},
|
||
|
||
# Japanese - Female
|
||
'jf_alpha': {'gender': 'F', 'language': 'ja', 'description': 'Japanese female voice'},
|
||
'jf_gongitsune': {'gender': 'F', 'language': 'ja', 'description': 'Japanese female voice'},
|
||
'jf_nezumi': {'gender': 'F', 'language': 'ja', 'description': 'Japanese female voice'},
|
||
'jf_tebukuro': {'gender': 'F', 'language': 'ja', 'description': 'Japanese female voice'},
|
||
|
||
# Japanese - Male
|
||
'jm_kumo': {'gender': 'M', 'language': 'ja', 'description': 'Japanese male voice'},
|
||
|
||
# Mandarin Chinese - Female
|
||
'zf_xiaobei': {'gender': 'F', 'language': 'zh', 'description': 'Chinese female voice'},
|
||
'zf_xiaoni': {'gender': 'F', 'language': 'zh', 'description': 'Chinese female voice'},
|
||
'zf_xiaoxiao': {'gender': 'F', 'language': 'zh', 'description': 'Chinese female voice'},
|
||
'zf_xiaoyi': {'gender': 'F', 'language': 'zh', 'description': 'Chinese female voice'},
|
||
|
||
# Mandarin Chinese - Male
|
||
'zm_yunjian': {'gender': 'M', 'language': 'zh', 'description': 'Chinese male voice'},
|
||
'zm_yunxi': {'gender': 'M', 'language': 'zh', 'description': 'Chinese male voice'},
|
||
'zm_yunxia': {'gender': 'M', 'language': 'zh', 'description': 'Chinese male voice'},
|
||
'zm_yunyang': {'gender': 'M', 'language': 'zh', 'description': 'Chinese male voice'},
|
||
|
||
# Spanish - Female
|
||
'ef_dora': {'gender': 'F', 'language': 'es', 'description': 'Spanish female voice'},
|
||
|
||
# Spanish - Male
|
||
'em_alex': {'gender': 'M', 'language': 'es', 'description': 'Spanish male voice'},
|
||
'em_santa': {'gender': 'M', 'language': 'es', 'description': 'Spanish male voice'},
|
||
|
||
# French - Female
|
||
'ff_siwis': {'gender': 'F', 'language': 'fr', 'description': 'French female voice'},
|
||
|
||
# Hindi - Female
|
||
'hf_alpha': {'gender': 'F', 'language': 'hi', 'description': 'Hindi female voice'},
|
||
'hf_beta': {'gender': 'F', 'language': 'hi', 'description': 'Hindi female voice'},
|
||
|
||
# Hindi - Male
|
||
'hm_omega': {'gender': 'M', 'language': 'hi', 'description': 'Hindi male voice'},
|
||
'hm_psi': {'gender': 'M', 'language': 'hi', 'description': 'Hindi male voice'},
|
||
|
||
# Italian - Female
|
||
'if_sara': {'gender': 'F', 'language': 'it', 'description': 'Italian female voice'},
|
||
|
||
# Italian - Male
|
||
'im_nicola': {'gender': 'M', 'language': 'it', 'description': 'Italian male voice'},
|
||
|
||
# Brazilian Portuguese - Female
|
||
'pf_dora': {'gender': 'F', 'language': 'pt', 'description': 'Portuguese female voice'},
|
||
|
||
# Brazilian Portuguese - Male
|
||
'pm_alex': {'gender': 'M', 'language': 'pt', 'description': 'Portuguese male voice'},
|
||
'pm_santa': {'gender': 'M', 'language': 'pt', 'description': 'Portuguese male voice'},
|
||
}
|
||
|
||
# Language mapping for voice filtering
|
||
VOICES_BY_LANGUAGE = {
|
||
'en-us': [v for v in ALL_VOICES if v.startswith('a')],
|
||
'en-gb': [v for v in ALL_VOICES if v.startswith('b')],
|
||
'ja': [v for v in ALL_VOICES if v.startswith('j')],
|
||
'zh': [v for v in ALL_VOICES if v.startswith('z')],
|
||
'es': [v for v in ALL_VOICES if v.startswith('e')],
|
||
'fr': [v for v in ALL_VOICES if v.startswith('f')],
|
||
'hi': [v for v in ALL_VOICES if v.startswith('h')],
|
||
'it': [v for v in ALL_VOICES if v.startswith('i')],
|
||
'pt': [v for v in ALL_VOICES if v.startswith('p')],
|
||
}
|
||
|
||
|
||
def get_voices_for_model(model: str) -> list:
|
||
"""
|
||
Get voices compatible with a specific model/language.
|
||
|
||
Args:
|
||
model: Model name (e.g., 'kokoro-en-us', 'kokoro-ja')
|
||
|
||
Returns:
|
||
List of compatible voice IDs
|
||
"""
|
||
# Extract language code from model name
|
||
if model == 'kokoro-en-us':
|
||
return VOICES_BY_LANGUAGE['en-us']
|
||
elif model == 'kokoro-en-gb':
|
||
return VOICES_BY_LANGUAGE['en-gb']
|
||
elif model == 'kokoro-ja':
|
||
return VOICES_BY_LANGUAGE['ja']
|
||
elif model == 'kokoro-zh':
|
||
return VOICES_BY_LANGUAGE['zh']
|
||
elif model == 'kokoro-es':
|
||
return VOICES_BY_LANGUAGE['es']
|
||
elif model == 'kokoro-fr':
|
||
return VOICES_BY_LANGUAGE['fr']
|
||
elif model == 'kokoro-hi':
|
||
return VOICES_BY_LANGUAGE['hi']
|
||
elif model == 'kokoro-it':
|
||
return VOICES_BY_LANGUAGE['it']
|
||
elif model == 'kokoro-pt':
|
||
return VOICES_BY_LANGUAGE['pt']
|
||
else:
|
||
# Return all voices if model not recognized
|
||
return ALL_VOICES
|
||
|
||
|
||
def get_voice_info(voice_id: str) -> dict:
|
||
"""
|
||
Get metadata for a specific voice.
|
||
|
||
Args:
|
||
voice_id: Voice identifier (e.g., 'af_bella')
|
||
|
||
Returns:
|
||
Dictionary with voice metadata
|
||
"""
|
||
return VOICE_METADATA.get(voice_id, {
|
||
'gender': 'Unknown',
|
||
'language': 'unknown',
|
||
'description': 'No description available'
|
||
})
|