The old auto-phonemization system is now disabled in favor of the
dedicated 📝 Phoneme Text Normalizer node which gives users full control.
Changes:
- Modified should_use_phonemization() to always return False
- Added deprecation notice explaining the replacement
- Users now have explicit control via the Phoneme Text Normalizer node
This prevents unexpected automatic phonemization and respects the
auto_phonemization=False setting properly.
403 lines
17 KiB
Python
403 lines
17 KiB
Python
"""
|
|
Universal Phonemizer Utilities for F5-TTS Multilingual Support
|
|
|
|
This module provides a unified interface for phonemization that works with either:
|
|
- phonemizer (Linux/Mac with system espeak)
|
|
- espeak-phonemizer-windows (Windows with bundled binaries)
|
|
|
|
Automatically detects which package is available and provides fallbacks.
|
|
"""
|
|
|
|
import re
|
|
from typing import List, Optional, Tuple
|
|
|
|
class UniversalPhonemizer:
|
|
"""
|
|
Universal phonemizer that works with either phonemizer or espeak-phonemizer-windows.
|
|
Provides automatic detection and fallback to character-based processing.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.backend = None
|
|
self.phonemizer = None
|
|
self._initialize_backend()
|
|
|
|
def _initialize_backend(self):
|
|
"""Initialize the best available phonemization backend"""
|
|
|
|
# Try espeak-phonemizer-windows first (Windows, bundled binaries)
|
|
try:
|
|
from espeak_phonemizer import Phonemizer
|
|
self.phonemizer = Phonemizer()
|
|
# Test it works
|
|
self.phonemizer.phonemize("test", voice="en")
|
|
self.backend = "espeak-phonemizer-windows"
|
|
return
|
|
except Exception:
|
|
pass
|
|
|
|
# Try standard phonemizer (Linux/Mac with system espeak)
|
|
try:
|
|
from phonemizer import phonemize
|
|
# Test if espeak backend works
|
|
phonemize("test", language="en", backend="espeak")
|
|
self.backend = "phonemizer"
|
|
return
|
|
except Exception:
|
|
pass
|
|
|
|
# No phonemization available - will use fallback
|
|
self.backend = "fallback"
|
|
|
|
def is_available(self) -> bool:
|
|
"""Check if phonemization is available"""
|
|
return self.backend in ["phonemizer", "espeak-phonemizer-windows"]
|
|
|
|
def get_backend_info(self) -> str:
|
|
"""Get information about current backend"""
|
|
if self.backend == "espeak-phonemizer-windows":
|
|
return "espeak-phonemizer-windows (bundled Windows binaries)"
|
|
elif self.backend == "phonemizer":
|
|
return "phonemizer + system espeak"
|
|
else:
|
|
return "fallback (character-based processing)"
|
|
|
|
def phonemize_text(self, text: str, language: str = "en") -> str:
|
|
"""
|
|
Convert text to IPA phonemes using available backend.
|
|
|
|
Args:
|
|
text: Text to phonemize
|
|
language: Language code (e.g. 'pl', 'de', 'fr', 'es')
|
|
|
|
Returns:
|
|
Phonemized text (IPA) or original text if phonemization fails
|
|
"""
|
|
if not text.strip():
|
|
return text
|
|
|
|
try:
|
|
if self.backend == "espeak-phonemizer-windows":
|
|
return self._phonemize_with_espeak_windows(text, language)
|
|
elif self.backend == "phonemizer":
|
|
return self._phonemize_with_standard(text, language)
|
|
else:
|
|
return text # Fallback: return original text
|
|
except Exception as e:
|
|
# If phonemization fails, return original text
|
|
print(f"Warning: Phonemization failed for '{text[:50]}...': {e}")
|
|
return text
|
|
|
|
def _phonemize_with_espeak_windows(self, text: str, language: str) -> str:
|
|
"""Phonemize using espeak-phonemizer-windows"""
|
|
# Map language codes to espeak voices
|
|
voice_map = {
|
|
'en': 'en',
|
|
'pl': 'pl',
|
|
'de': 'de',
|
|
'fr': 'fr',
|
|
'es': 'es',
|
|
'it': 'it',
|
|
'pt': 'pt',
|
|
'ru': 'ru',
|
|
'zh': 'zh',
|
|
'ja': 'ja',
|
|
'ko': 'ko',
|
|
'ar': 'ar',
|
|
'hi': 'hi',
|
|
'th': 'th'
|
|
}
|
|
|
|
voice = voice_map.get(language, 'en') # Default to English
|
|
|
|
ipa_text = self.phonemizer.phonemize(
|
|
text,
|
|
voice=voice,
|
|
keep_clause_breakers=True,
|
|
word_separator=' ',
|
|
no_stress=False
|
|
)
|
|
|
|
# Clean up IPA text
|
|
return self._clean_ipa_text(ipa_text)
|
|
|
|
def _phonemize_with_standard(self, text: str, language: str) -> str:
|
|
"""Phonemize using standard phonemizer package"""
|
|
from phonemizer import phonemize
|
|
|
|
# Map our language codes to phonemizer language codes
|
|
language_map = {
|
|
'en': 'en-us',
|
|
'pl': 'pl',
|
|
'de': 'de',
|
|
'fr': 'fr',
|
|
'es': 'es',
|
|
'it': 'it',
|
|
'pt': 'pt',
|
|
'ru': 'ru',
|
|
'zh': 'zh',
|
|
'ja': 'ja',
|
|
'ko': 'ko',
|
|
'ar': 'ar',
|
|
'hi': 'hi',
|
|
'th': 'th'
|
|
}
|
|
|
|
phonemizer_lang = language_map.get(language, 'en-us')
|
|
|
|
ipa_text = phonemize(
|
|
text,
|
|
language=phonemizer_lang,
|
|
backend='espeak',
|
|
strip=False,
|
|
preserve_punctuation=True,
|
|
with_stress=True
|
|
)
|
|
|
|
# Clean up IPA text
|
|
return self._clean_ipa_text(ipa_text)
|
|
|
|
def _clean_ipa_text(self, ipa_text: str) -> str:
|
|
"""Clean up IPA text output"""
|
|
# Remove language markings like (en), (pl), (de), etc.
|
|
ipa_text = re.sub(r'\([a-z]{2,3}\)', '', ipa_text)
|
|
|
|
# Remove extra whitespace
|
|
ipa_text = ' '.join(ipa_text.split())
|
|
|
|
return ipa_text
|
|
|
|
|
|
# Global phonemizer instance
|
|
_global_phonemizer = None
|
|
|
|
def get_phonemizer() -> UniversalPhonemizer:
|
|
"""Get global phonemizer instance (singleton)"""
|
|
global _global_phonemizer
|
|
if _global_phonemizer is None:
|
|
_global_phonemizer = UniversalPhonemizer()
|
|
return _global_phonemizer
|
|
|
|
|
|
def should_use_phonemization(model_name: str, text_list: List[str], auto_phonemization: bool = None) -> bool:
|
|
"""
|
|
DEPRECATED: Auto-phonemization is now disabled in favor of the dedicated
|
|
📝 Phoneme Text Normalizer node which gives users full control.
|
|
|
|
Args:
|
|
model_name: F5-TTS model name
|
|
text_list: List of text strings to analyze
|
|
auto_phonemization: Override for phonemization setting (from UI toggle)
|
|
|
|
Returns:
|
|
Always False - use 📝 Phoneme Text Normalizer node instead
|
|
"""
|
|
# Always return False - users should use the dedicated Phoneme Text Normalizer node
|
|
return False
|
|
# Check user setting first (UI toggle overrides everything)
|
|
import os
|
|
if auto_phonemization is not None:
|
|
# Use explicit parameter if provided (more reliable than environment variable)
|
|
if not auto_phonemization:
|
|
return False # No debug message when disabled
|
|
else:
|
|
# Fall back to environment variable for backward compatibility
|
|
env_value = os.environ.get('F5TTS_AUTO_PHONEMIZATION', 'true')
|
|
auto_phonemization = env_value.lower() == 'true'
|
|
if not auto_phonemization:
|
|
return False # No debug message when disabled
|
|
|
|
# Check if phonemization is available
|
|
phonemizer = get_phonemizer()
|
|
if not phonemizer.is_available():
|
|
return False
|
|
|
|
# IMPORTANT: Some models don't work well with phonemization
|
|
# These models were trained on text, not IPA phonemes
|
|
# Using phonemization makes them worse, not better
|
|
model_lower = model_name.lower()
|
|
models_to_skip = ['ptbr', 'pt-br', 'pt_br', 'it'] # These work better without phonemization
|
|
|
|
if any(indicator in model_lower for indicator in models_to_skip):
|
|
import sys
|
|
print(f"🦜 Skipping phonemization for {model_name} - model trained on native text, not IPA", file=sys.stderr)
|
|
return False
|
|
|
|
# Check if model path suggests non-English language
|
|
non_english_indicators = [
|
|
'polish', 'german', 'french', 'spanish', 'italian', 'portuguese',
|
|
'russian', 'arabic', 'hindi', 'thai', 'japanese', 'korean',
|
|
'pl', 'de', 'fr', 'es', 'it', 'pt', 'ru', 'ar', 'hi', 'th', 'ja', 'ko'
|
|
]
|
|
|
|
if any(indicator in model_lower for indicator in non_english_indicators):
|
|
import sys
|
|
print(f"🦜 DEBUG: Model '{model_name}' triggered phonemization (non-English model detected)", file=sys.stderr)
|
|
print(f"🦜 EXPERIMENTAL: Using phonemization for {model_name}. Please test quality and report results!", file=sys.stderr)
|
|
return True
|
|
|
|
# Check for special characters in text that suggest non-English
|
|
special_chars = set('ąćęłńóśźżĄĆĘŁŃÓŚŹŻäöüßÄÖÜàâæçéèêëîïôùûÀÂÆÇÉÈÊËÎÏÔÙÛáéíñóúüÁÉÍÑÓÚÜ')
|
|
|
|
for text in text_list:
|
|
if any(char in special_chars for char in text):
|
|
import sys
|
|
print(f"🦜 DEBUG: Text contains special characters, triggering phonemization", file=sys.stderr)
|
|
print(f"🦜 EXPERIMENTAL: Using phonemization for special characters in text. Please test quality and report results!", file=sys.stderr)
|
|
return True
|
|
|
|
# Check for tokenizer.json (indicates IPA-based model)
|
|
if hasattr(model_name, 'startswith') and model_name.startswith('local:'):
|
|
# For local models, we could check for tokenizer.json file existence
|
|
# This would require path resolution, skip for now
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
def detect_language_from_text(text: str) -> str:
|
|
"""
|
|
Detect language from text content based on character patterns.
|
|
Uses comprehensive character sets for 50+ languages.
|
|
|
|
Args:
|
|
text: Text to analyze
|
|
|
|
Returns:
|
|
Canonical language code (e.g. 'pl', 'de', 'fr')
|
|
"""
|
|
# Extended language detection using character sets
|
|
language_chars = {
|
|
# European languages with special characters
|
|
'pl': 'ąćęłńóśźżĄĆĘŁŃÓŚŹŻ', # Polish
|
|
'de': 'äöüßÄÖÜ', # German
|
|
'fr': 'àâæçéèêëîïôùûÀÂÆÇÉÈÊËÎÏÔÙÛ', # French
|
|
'es': 'áéíñóúüÁÉÍÑÓÚÜ', # Spanish
|
|
'it': 'àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ', # Italian
|
|
'pt': 'àáâãçéêíóôõúÀÁÂÃÇÉÊÍÓÔÕÚ', # Portuguese
|
|
'cs': 'áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ', # Czech
|
|
'sk': 'áäčďéíľĺňóôŕšťúýžÁÄČĎÉÍĽĹŇÓÔŔŠŤÚÝŽ', # Slovak
|
|
'hu': 'áéíóöőúüűÁÉÍÓÖŐÚÜŰ', # Hungarian
|
|
'ro': 'ăâîșțĂÂÎȘȚ', # Romanian
|
|
'hr': 'čćđšžČĆĐŠŽ', # Croatian
|
|
'sr': 'чћџшжЧЋЏШЖ', # Serbian (Cyrillic)
|
|
'bg': 'абвгдежзийклмнопрстуфхцчшщъьюяАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯ', # Bulgarian
|
|
'ru': 'абвгдежзийклмнопрстуфхцчшщъыьэюяАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ', # Russian
|
|
'el': 'αβγδεζηθικλμνξοπρστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ', # Greek
|
|
'tr': 'çğıöşüÇĞIİÖŞÜ', # Turkish
|
|
'is': 'áðéíóúýþæöÁÐÉÍÓÚÝÞÆÖ', # Icelandic
|
|
'da': 'æøåÆØÅ', # Danish
|
|
'no': 'æøåÆØÅ', # Norwegian
|
|
'sv': 'äöåÄÖÅ', # Swedish
|
|
'fi': 'äöåÄÖÅ', # Finnish
|
|
'et': 'äöõüšžÄÖÕÜŠŽ', # Estonian
|
|
'lv': 'āčēģīķļņšūžĀČĒĢĪĶĻŅŠŪŽ', # Latvian
|
|
'lt': 'ąčęėįšųūžĄČĘĖĮŠŲŪŽ', # Lithuanian
|
|
'sl': '蚞ȊŽ', # Slovenian
|
|
'nl': '', # Dutch (uses mostly standard Latin)
|
|
|
|
# Asian languages with distinct scripts
|
|
'zh': '一丁七万三不与专且世丘中为主举了事二于五些什人', # Chinese (sample chars)
|
|
'ja': 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをんアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン', # Japanese
|
|
'ko': '가나다라마바사아자차카타파하거너더러머버서어저처커터퍼허고노도로모보소오조초코토포호구누두루무부수우주추쿠투푸후', # Korean
|
|
'th': 'กขคงจซดตนบปผฟมยรลวศษสหอะาิีืึุู', # Thai
|
|
'vi': 'àáạảãâầấậẩẫăằắặẳẵèéẹẻẽêềếệểễìíịỉĩòóọỏõôồốộổỗơờớợởỡùúụủũưừứựửữỳýỵỷỹđ', # Vietnamese
|
|
|
|
# Middle Eastern and South Asian
|
|
'ar': 'ابتثجحخدذرزسشصضطظعغفقكلمنهوي', # Arabic
|
|
'fa': 'ابپتثجچحخدذرزژسشصضطظعغفقکگلمنوهی', # Persian/Farsi
|
|
'he': 'אבגדהוזחטיכלמנסעפצקרשת', # Hebrew
|
|
'ur': 'آابپتٹثجچحخدڈذرڑزژسشصضطظعغفقکگلمنوہی', # Urdu
|
|
'hi': 'अआइईउऊएऐओऔकखगघचछजझटठडढणतथदधनपफबभमयरलवशषसह', # Hindi
|
|
'bn': 'অআইঈউঊএঐওঔকখগঘচছজঝটঠডঢণতথদধনপফবভমযরলশষসহ', # Bengali
|
|
'ta': 'அஆஇஈউஊஎஏஐஒஓஔகஙசஞடணதநபமயரலவழளறன', # Tamil
|
|
'te': 'అఆఇఈఉఊఎఏఐఒఓఔకఖగఘచఛజఝటఠడఢణతథదధనపఫబభమయరలవశషసహ', # Telugu
|
|
'ml': 'അആഇഈഉഊഎഏഐഒഓഔകഖഗഘങചഛജഝഞടഠഡഢണതഥദധനപഫബഭമയരലവശഷസഹ', # Malayalam
|
|
'kn': 'ಅಆಇಈಉಊಎಏಐಒಓಔಕಖಗಘಙಚಛಜಝಞಟಠಡಢಣತಥದಧನಪಫಬಭಮಯರಲವಶಷಸಹ', # Kannada
|
|
'gu': 'અઆઇઈઉઊએઐઓઔકખગઘચછજઝટઠડઢણતથદધનપફબભમયરલવશષસહ', # Gujarati
|
|
|
|
# Southeast Asian and Others
|
|
'id': '', # Indonesian (uses standard Latin)
|
|
'ms': '', # Malay (uses standard Latin)
|
|
'tl': '', # Filipino/Tagalog (mostly standard Latin with some Spanish influence)
|
|
'sw': '', # Swahili (uses standard Latin)
|
|
'af': '', # Afrikaans (uses standard Latin)
|
|
}
|
|
|
|
# Check for languages with distinctive character sets
|
|
for lang, chars in language_chars.items():
|
|
if chars and any(char in text for char in chars):
|
|
# Use language mapper's alias resolution to get canonical form
|
|
try:
|
|
from utils.models.language_mapper import resolve_language_alias
|
|
return resolve_language_alias(lang)
|
|
except ImportError:
|
|
return lang
|
|
|
|
# Default to English for standard Latin text
|
|
return 'en'
|
|
|
|
|
|
def convert_text_with_smart_phonemization(text_list: List[str], model_name: str = "", auto_phonemization: bool = None) -> List[List[str]]:
|
|
"""
|
|
Convert text using smart phonemization or fallback to character-based processing.
|
|
|
|
This is the main entry point that F5-TTS should use instead of convert_char_to_pinyin.
|
|
|
|
Args:
|
|
text_list: List of text strings to process
|
|
model_name: F5-TTS model name for context
|
|
auto_phonemization: Override for phonemization setting (from UI toggle)
|
|
|
|
Returns:
|
|
List of processed text (as character lists for model input)
|
|
"""
|
|
# Import here to avoid circular dependencies
|
|
try:
|
|
from engines.f5_tts.model.utils import convert_char_to_pinyin
|
|
except ImportError:
|
|
# Fallback if F5-TTS not available
|
|
def convert_char_to_pinyin(texts):
|
|
return [list(text) for text in texts]
|
|
|
|
# Check if we should use phonemization
|
|
if should_use_phonemization(model_name, text_list, auto_phonemization):
|
|
phonemizer = get_phonemizer()
|
|
|
|
import sys
|
|
print(f"🦜 Using phonemization for F5-TTS: {phonemizer.get_backend_info()}", file=sys.stderr)
|
|
|
|
processed_texts = []
|
|
for text in text_list:
|
|
# Detect language from text
|
|
detected_lang = detect_language_from_text(text)
|
|
|
|
# Phonemize the text
|
|
phonemized = phonemizer.phonemize_text(text, detected_lang)
|
|
|
|
# Convert to character list for model input
|
|
processed_texts.append(list(phonemized))
|
|
|
|
return processed_texts
|
|
else:
|
|
# Use standard character-to-pinyin conversion
|
|
return convert_char_to_pinyin(text_list)
|
|
|
|
|
|
# Convenience functions for direct use
|
|
def phonemize(text: str, language: str = "en") -> str:
|
|
"""Convenience function for direct phonemization"""
|
|
phonemizer = get_phonemizer()
|
|
return phonemizer.phonemize_text(text, language)
|
|
|
|
|
|
def is_phonemization_available() -> bool:
|
|
"""Check if phonemization is available"""
|
|
phonemizer = get_phonemizer()
|
|
return phonemizer.is_available()
|
|
|
|
|
|
def get_phonemization_info() -> str:
|
|
"""Get information about phonemization backend"""
|
|
phonemizer = get_phonemizer()
|
|
return phonemizer.get_backend_info() |