Files
diodiogod-TTS-Audio-Suite/utils/text/phonemizer_utils.py
T
diodiogod 4a4df30db8 Disable old auto-phonemization system
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.
2025-09-23 01:19:03 -03:00

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()