Files
audio-engine-hub/app/engines/kokoro_voices.py
stephan fff0252d52 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.
2025-12-09 12:45:17 +01:00

206 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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