feat: Add OpenAI-compatible TTS endpoint and engines
- 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.
This commit is contained in:
205
app/engines/kokoro_voices.py
Normal file
205
app/engines/kokoro_voices.py
Normal file
@ -0,0 +1,205 @@
|
||||
"""
|
||||
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'
|
||||
})
|
||||
Reference in New Issue
Block a user