Spaces:
Sleeping
Sleeping
| import os | |
| import re | |
| try: | |
| from g2p_en import G2p | |
| except ImportError: | |
| G2p = None | |
| # Mapping from ARPAbet (g2p-en) to the specific IPA set used in the NPTEL model vocab | |
| ARPABET_TO_IPA = { | |
| "AA": "ɑ", "AE": "a", "AH": "ə", "AO": "ɒ", "AW": "aw", "AY": "aj", | |
| "B": "b", "CH": "tʃ", "D": "ɖ", "DH": "d̪", "EH": "ɛ", "ER": "ɜ", | |
| "EY": "eː", "F": "f", "G": "ɡ", "HH": "h", "IH": "ɪ", "IY": "iː", | |
| "JH": "dʒ", "K": "k", "L": "l", "M": "m", "N": "n", "NG": "ŋ", | |
| "OW": "oː", "OY": "ɔj", "P": "p", "R": "ɹ", "S": "s", "SH": "ʃ", | |
| "T": "ʈ", "TH": "t̪", "UH": "ʊ", "UW": "ʉ", "V": "ʋ", "W": "ʋ", | |
| "Y": "j", "Z": "z", "ZH": "ʒ" | |
| } | |
| # Regex to strip common IPA stress marks not in the model vocab. | |
| # We KEEP the length mark (ː) because the model vocab includes tokens like eː, iː. | |
| IPA_CLEAN_REGEX = re.compile(r'[ˈˌ]') | |
| def clean_phoneme(p): | |
| return IPA_CLEAN_REGEX.sub('', p) | |
| class G2PManager: | |
| """ | |
| Manages Grapheme-to-Phoneme conversion. | |
| Strategy: | |
| 1. Dictionary Lookup (MFA Gold Standard) | |
| 2. Neural Fallback (g2p-en) -> Mapped to IPA | |
| 3. Identity Mapping (Last Resort) | |
| """ | |
| def __init__(self, dict_path=None): | |
| if dict_path is None: | |
| # Default to the local dictionary in the same folder | |
| dict_path = os.path.join(os.path.dirname(__file__), "output_v2_detailed.dict") | |
| self.dict_path = dict_path | |
| self.phoneme_dict = self._load_dict(dict_path) | |
| # Load and merge patch vocabulary if it exists | |
| patch_path = os.path.join(os.path.dirname(dict_path), "patch_vocab.dict") | |
| if os.path.exists(patch_path): | |
| print(f"Merging patch dictionary from {patch_path}...") | |
| patch_dict = self._load_dict(patch_path) | |
| self.phoneme_dict.update(patch_dict) | |
| print(f"Total vocabulary size after patch merge: {len(self.phoneme_dict)}") | |
| # Initialize Neural G2P | |
| if G2p is not None: | |
| print("Initializing Neural G2P fallback (g2p-en)...") | |
| self.neural_g2p = G2p() | |
| else: | |
| print("Warning: g2p-en not found. Neural fallback disabled.") | |
| self.neural_g2p = None | |
| self.oov_cache = {} | |
| print(f"Loaded {len(self.phoneme_dict)} words from {dict_path}") | |
| def _load_dict(self, path): | |
| mapping = {} | |
| if not os.path.exists(path): | |
| print(f"Warning: Dictionary not found at {path}") | |
| return mapping | |
| with open(path, "r", encoding="utf8") as f: | |
| for line in f: | |
| parts = line.strip().split("\t") | |
| if len(parts) >= 2: | |
| word = parts[0].lower() | |
| # Apply IPA cleaning to dictionary phonemes as well | |
| phonemes = [clean_phoneme(p) for p in parts[1].split()] | |
| mapping[word] = phonemes | |
| return mapping | |
| def tokenize(self, text): | |
| """Cleans and splits text into words.""" | |
| return re.findall(r"[A-Za-z']+", text.lower()) | |
| def convert_sentence(self, text): | |
| """Converts a full sentence to a list of phonemes.""" | |
| words = self.tokenize(text) | |
| all_phonemes = [] | |
| for word in words: | |
| phonemes = self.convert_word(word) | |
| all_phonemes.extend(phonemes) | |
| return all_phonemes | |
| def convert_word(self, word): | |
| """Converts a single word to phonemes with fallback logic.""" | |
| word = word.lower() | |
| # 1. First Priority: Dictionary Lookup | |
| if word in self.phoneme_dict: | |
| return self.phoneme_dict[word] | |
| # Check OOV cache | |
| if word in self.oov_cache: | |
| return self.oov_cache[word] | |
| # 2. Second Priority: Neural G2P Fallback + IPA Mapping | |
| if self.neural_g2p is not None: | |
| # g2p-en returns phonemes in ARPAbet (with digits) | |
| arpabet_phonemes = self.neural_g2p(word) | |
| ipa_phonemes = [] | |
| for p in arpabet_phonemes: | |
| # Strip digits (stress) | |
| clean_p = re.sub(r'\d', '', p).upper() | |
| # Map to model's IPA set | |
| mapped = ARPABET_TO_IPA.get(clean_p, None) | |
| if mapped: | |
| # Final clean (some mapped IPA might have extra marks) | |
| ipa_phonemes.append(clean_phoneme(mapped)) | |
| elif clean_p.isalpha(): | |
| # Last resort fallback (lowercase and clean) | |
| ipa_phonemes.append(clean_phoneme(p.lower())) | |
| self.oov_cache[word] = ipa_phonemes | |
| return ipa_phonemes | |
| # 3. Final Resort: Identity Mapping | |
| return [word] | |
| if __name__ == "__main__": | |
| # Quick test | |
| g2p = G2PManager() | |
| print(f"Test sentence: 'I am going to the CDAC university'") | |
| # 'CDAC' is likely an OOV, let's see how it handles it | |
| print(f"Phonemes: {g2p.convert_sentence('I am going to the CDAC university')}") | |