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:
2025-12-09 12:45:17 +01:00
parent c06fd677dc
commit fff0252d52
9 changed files with 985 additions and 3 deletions

View 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'
})