File size: 7,367 Bytes
cfb5e7f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
"""Text normalization module for Myanmar language."""

import re
import unicodedata
from pathlib import Path
from typing import Dict, List, Optional

import pandas as pd
import yaml

logger = __import__("loguru").logger


class MyanmarTextNormalizer:
    """Normalize Myanmar (Burmese) text for consistent processing."""

    # Myanmar Unicode ranges
    MYANMAR_CHARS = re.compile(
        r"[\u1000-\u100F\u1010-\u101F\u1020-\u102A\u102C-\u1030\u1031\u1032\u1036-\u1038\u1039\u103A]"
    )
    
    # Normalization rules
    NORMALIZATION_RULES = {
        # Zero-width characters
        "\u200B": "",  # Zero-width space
        "\u200C": "",  # Zero-width non-joiner
        "\u200D": "",  # Zero-width joiner
        "\u2060": "",  # Word joiner
        # Myanmar-specific normalizations
        "\u1031\u103B": "\u103B\u1031",  # medial order
        "\u103D\u103E": "\u103E\u103D",  # stack order
    }

    def __init__(self, custom_rules_path: Optional[str] = None):
        self.custom_rules = {}
        if custom_rules_path and Path(custom_rules_path).exists():
            with open(custom_rules_path, "r", encoding="utf-8") as f:
                self.custom_rules = yaml.safe_load(f) or {}
        
        self.rules = {**self.NORMALIZATION_RULES, **self.custom_rules}

    def normalize_unicode(self, text: str) -> str:
        """Standardize Unicode representation (NFC normalization)."""
        return unicodedata.normalize("NFC", text)

    def remove_diacritics(self, text: str) -> str:
        """Remove tone marks for simplified processing."""
        diacritics = re.compile(
            r"[\u102B-\u102D\u102F-\u1032\u1034\u1036\u1037\u1039]"
        )
        return diacritics.sub("", text)

    def remove_whitespace(self, text: str) -> str:
        """Remove excessive whitespace."""
        text = re.sub(r"\s+", " ", text)
        return text.strip()

    def normalize_punctuation(self, text: str) -> str:
        """Standardize punctuation marks."""
        replacements = {
            "แŠ": "แ‹",  # Myanmar comma to full stop
            "โ€ž": '"',
            "โ€Ÿ": '"',
            "'": "'",
            "`": "'",
            "โ€”": "โ€“",
            "โ€“": "-",
        }
        for old, new in replacements.items():
            text = text.replace(old, new)
        return text

    def apply_custom_rules(self, text: str) -> str:
        """Apply user-defined normalization rules."""
        for pattern, replacement in self.rules.items():
            text = text.replace(pattern, replacement)
        return text

    def expand_abbreviations(self, text: str, abbreviations: Dict[str, str] = None) -> str:
        """Expand common abbreviations."""
        if abbreviations is None:
            abbreviations = {
                "แ€ก.แ€•.แ€": "แ€กแ€„แ€ผแ€ญแ€™แ€บแ€ธแ€…แ€ฌแ€ธแ€•แ€ผแ€Šแ€บแ€‘แ€ฒแ€›แ€ฑแ€ธแ€แ€”แ€บแ€€แ€ผแ€ฎแ€ธ",
                "แ€’.แ€•.แ€œ": "แ€’แ€ฏแ€แ€ญแ€šแ€žแ€™แ€นแ€™แ€",
                "แ€•.แ€›.แ€™แ€พแ€ฐแ€ธ": "แ€•แ€ผแ€Šแ€บแ€žแ€ฐแ€ทแ€œแ€ฝแ€พแ€แ€บแ€แ€ฑแ€ฌแ€บแ€ฅแ€€แ€นแ€€แ€‹แ€นแ€Œ",
            }
        
        for abbr, full in abbreviations.items():
            text = re.sub(rf"\b{re.escape(abbr)}\b", full, text)
        
        return text

    def normalize_numbers(self, text: str) -> str:
        """Convert Myanmar numerals to Arabic (0-9)."""
        myanmar_digits = "แ€แแ‚แƒแ„แ…แ†แ‡แˆแ‰"
        arabic_digits = "0123456789"
        
        trans_table = str.maketrans(
            {myanmar_digits[i]: arabic_digits[i] for i in range(10)}
        )
        return text.translate(trans_table)

    def filter_non_myanmar(self, text: str, keep_english: bool = True) -> str:
        """Remove or keep non-Myanmar characters."""
        if keep_english:
            pattern = r"[^\u1000-\u109F\u0020-\u007E\u00A0-\u00FF]"
        else:
            pattern = r"[^\u1000-\u109F\s]"
        
        return re.sub(pattern, "", text)

    def normalize_line(self, text: str) -> str:
        """Apply all normalization steps to a single line."""
        text = self.normalize_unicode(text)
        text = self.apply_custom_rules(text)
        text = self.remove_whitespace(text)
        text = self.normalize_punctuation(text)
        return text

    def normalize_corpus(
        self,
        texts: List[str],
        remove_non_myanmar: bool = False,
    ) -> List[str]:
        """Normalize a list of texts."""
        normalized = []
        for text in texts:
            text = self.normalize_line(text)
            if remove_non_myanmar:
                text = self.filter_non_myanmar(text, keep_english=False)
            normalized.append(text)
        
        logger.info(f"Normalized {len(normalized)} texts")
        return normalized

    def normalize_dataset(
        self,
        input_path: str,
        output_path: str,
        text_column: str = "text",
    ) -> pd.DataFrame:
        """Normalize a dataset and save to file."""
        df = pd.read_csv(input_path)
        
        if text_column not in df.columns:
            raise ValueError(f"Column '{text_column}' not found in dataset")
        
        df[f"{text_column}_normalized"] = self.normalize_corpus(
            df[text_column].tolist()
        )
        
        df.to_csv(output_path, index=False)
        logger.info(f"Normalized dataset saved to {output_path}")
        
        return df


class ProsodyNormalizer:
    """Normalize prosodic features for consistent representation."""

    def normalize_pitch(self, pitch_values: List[float]) -> List[float]:
        """Normalize pitch values to semitones from mean."""
        import numpy as np
        pitch_arr = np.array(pitch_values)
        mean_pitch = np.mean(pitch_arr[pitch_arr > 0])
        if mean_pitch == 0:
            return pitch_values
        semitones = 12 * np.log2(pitch_arr / mean_pitch)
        return semitones.tolist()

    def normalize_energy(self, energy_values: List[float]) -> List[float]:
        """Normalize energy values to 0-1 range."""
        import numpy as np
        energy_arr = np.array(energy_values)
        min_e, max_e = energy_arr.min(), energy_arr.max()
        if max_e - min_e == 0:
            return [0.5] * len(energy_values)
        return ((energy_arr - min_e) / (max_e - min_e)).tolist()

    def quantize_prosody(
        self,
        prosody: dict,
        num_levels: int = 5,
    ) -> dict:
        """Quantize prosodic features for categorical representation."""
        quantized = {}
        for key, value in prosody.items():
            if isinstance(value, (int, float)) and key != "pitch_range":
                normalized = max(0, min(1, (value + 100) / 200))
                quantized[key] = int(normalized * (num_levels - 1))
            else:
                quantized[key] = value
        return quantized


def create_normalizer(config: dict = None) -> MyanmarTextNormalizer:
    """Factory function to create normalizer from config."""
    if config is None:
        config = {}
    
    return MyanmarTextNormalizer(
        custom_rules_path=config.get("custom_rules_path")
    )


if __name__ == "__main__":
    normalizer = create_normalizer()
    test_text = "  แ€™แ€„แ€บแ€นแ€‚แ€œแ€ฌแ€•แ€ซ   แŠ  แ€€แ€ปแ€ฑแ€ธแ€‡แ€ฐแ€ธแ€•แ€ซ  แ€•แ€ซ   แ€žแ€Šแ€บ  "
    print(f"Original: {test_text}")
    print(f"Normalized: {normalizer.normalize_line(test_text)}")