txya900619 commited on
Commit
7a23caf
·
1 Parent(s): 425413b

feat: add app.py

Browse files
app.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tempfile
2
+
3
+ import gradio as gr
4
+ import soundfile as sf
5
+ import torchaudio
6
+ from cached_path import cached_path
7
+ from f5_tts.infer.utils_infer import (
8
+ infer_process,
9
+ load_model,
10
+ load_vocoder,
11
+ preprocess_ref_audio_text,
12
+ remove_silence_for_generated_wav,
13
+ save_spectrogram,
14
+ )
15
+ from f5_tts.model import DiT
16
+ from omegaconf import OmegaConf
17
+
18
+ from ipa.ipa import get_ipa, parse_ipa
19
+
20
+ try:
21
+ import spaces
22
+
23
+ USING_SPACES = True
24
+ except ImportError:
25
+ USING_SPACES = False
26
+
27
+
28
+ def gpu_decorator(func):
29
+ if USING_SPACES:
30
+ return spaces.GPU(func)
31
+ else:
32
+ return func
33
+
34
+
35
+ vocoder = load_vocoder()
36
+
37
+
38
+ def load_f5tts(ckpt_path, vocab_path):
39
+ ckpt_path = str(cached_path(ckpt_path))
40
+ F5TTS_model_cfg = dict(
41
+ dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4
42
+ )
43
+ vocab_path = str(cached_path(vocab_path))
44
+ return load_model(DiT, F5TTS_model_cfg, ckpt_path, vocab_file=vocab_path)
45
+
46
+
47
+ OmegaConf.register_new_resolver("load_f5tts", load_f5tts)
48
+
49
+ models_config = OmegaConf.to_object(OmegaConf.load("configs/models.yaml"))
50
+ dialects = OmegaConf.to_object(OmegaConf.load("configs/dialects.yaml"))
51
+
52
+
53
+ DEFAULT_MODEL_ID = list(models_config.keys())[0]
54
+ DEFAULT_DIALECT = list(dialects.values())[0]
55
+
56
+
57
+ @gpu_decorator
58
+ def infer(
59
+ ref_audio_orig,
60
+ ref_text,
61
+ gen_text,
62
+ model,
63
+ remove_silence,
64
+ cross_fade_duration=0.15,
65
+ nfe_step=32,
66
+ fix_duration=1,
67
+ show_info=gr.Info,
68
+ ):
69
+ if not ref_audio_orig:
70
+ gr.Warning("Please provide reference audio.")
71
+ return gr.update(), gr.update(), ref_text
72
+
73
+ if not gen_text.strip():
74
+ gr.Warning("Please enter text to generate.")
75
+ return gr.update(), gr.update(), ref_text
76
+
77
+ ref_audio, ref_text = preprocess_ref_audio_text(
78
+ ref_audio_orig, ref_text, show_info=show_info
79
+ )
80
+
81
+ final_wave, final_sample_rate, combined_spectrogram = infer_process(
82
+ ref_audio,
83
+ ref_text,
84
+ gen_text,
85
+ model,
86
+ vocoder,
87
+ cross_fade_duration=cross_fade_duration,
88
+ nfe_step=nfe_step,
89
+ fix_duration=fix_duration,
90
+ show_info=show_info,
91
+ progress=gr.Progress(),
92
+ )
93
+
94
+ # Remove silence
95
+ if remove_silence:
96
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as f:
97
+ sf.write(f.name, final_wave, final_sample_rate)
98
+ remove_silence_for_generated_wav(f.name)
99
+ final_wave, _ = torchaudio.load(f.name)
100
+ final_wave = final_wave.squeeze().cpu().numpy()
101
+
102
+ print(f"Final wave duration: {final_wave.shape[0] / final_sample_rate:.2f}s")
103
+ # Save the spectrogram
104
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_spectrogram:
105
+ spectrogram_path = tmp_spectrogram.name
106
+ save_spectrogram(combined_spectrogram, spectrogram_path)
107
+
108
+ return (final_sample_rate, final_wave), spectrogram_path
109
+
110
+
111
+ demo = gr.Blocks(
112
+ title="臺灣客語語音生成系統",
113
+ css="@import url(https://tauhu.tw/tauhu-oo.css);",
114
+ theme=gr.themes.Default(
115
+ font=(
116
+ "tauhu-oo",
117
+ gr.themes.GoogleFont("Source Sans Pro"),
118
+ "ui-sans-serif",
119
+ "system-ui",
120
+ "sans-serif",
121
+ )
122
+ ),
123
+ )
124
+
125
+ with demo:
126
+ gr.Markdown(
127
+ """
128
+ # 臺灣客語語音合成系統
129
+ ### Taiwanese Hakka Text-to-Speech System
130
+ ### 研發團隊
131
+ - **[李鴻欣 Hung-Shin Lee](mailto:hungshinlee@gmail.com)([聯和科創](https://www.104.com.tw/company/1a2x6bmu75))**
132
+ - **[陳力瑋 Li-Wei Chen](mailto:wayne900619@gmail.com)([聯和科創](https://www.104.com.tw/company/1a2x6bmu75))**
133
+ ### 合作單位
134
+ - **[國立聯合大學智慧客家實驗室](https://www.gohakka.org)**
135
+ """
136
+ )
137
+ with gr.Row():
138
+ with gr.Column():
139
+ model_drop_down = gr.Dropdown(
140
+ models_config.keys(),
141
+ value=DEFAULT_MODEL_ID,
142
+ label="模型",
143
+ )
144
+
145
+ ref_audio_input = gr.Audio(
146
+ type="filepath",
147
+ waveform_options=gr.WaveformOptions(
148
+ sample_rate=24000,
149
+ ),
150
+ label="Reference Audio",
151
+ )
152
+ ref_text_input = gr.Textbox(
153
+ value="",
154
+ label="Reference Text",
155
+ )
156
+
157
+ ref_dialect_radio = gr.Radio(
158
+ choices=[(k, v) for k, v in dialects.items()],
159
+ value=DEFAULT_DIALECT,
160
+ label="ref 腔調",
161
+ interactive=len(dialects.keys()) > 1,
162
+ )
163
+
164
+ gen_text_input = gr.Textbox(
165
+ label="Text to Generate",
166
+ value="",
167
+ )
168
+
169
+ dialect_radio = gr.Radio(
170
+ choices=[(k, v) for k, v in dialects.items()],
171
+ value=DEFAULT_DIALECT,
172
+ label="腔調",
173
+ interactive=len(dialects.keys()) > 1,
174
+ )
175
+
176
+ generate_btn = gr.Button("Synthesize", variant="primary")
177
+
178
+ with gr.Accordion("Advanced Settings", open=False):
179
+ remove_silence = gr.Checkbox(
180
+ label="Remove Silences",
181
+ info="The model tends to produce silences, especially on longer audio. We can manually remove silences if needed. Note that this is an experimental feature and may produce strange results. This will also increase generation time.",
182
+ value=False,
183
+ )
184
+ speed_slider = gr.Slider(
185
+ label="Speed",
186
+ minimum=0.3,
187
+ maximum=2.0,
188
+ value=1.0,
189
+ step=0.1,
190
+ info="語速(越小越慢)",
191
+ )
192
+ nfe_slider = gr.Slider(
193
+ label="NFE Steps",
194
+ minimum=4,
195
+ maximum=64,
196
+ value=32,
197
+ step=2,
198
+ info="Set the number of denoising steps.",
199
+ )
200
+ cross_fade_duration_slider = gr.Slider(
201
+ label="Cross-Fade Duration (s)",
202
+ minimum=0.0,
203
+ maximum=1.0,
204
+ value=0.15,
205
+ step=0.01,
206
+ info="Set the duration of the cross-fade between audio clips.",
207
+ )
208
+ with gr.Column():
209
+ audio_output = gr.Audio(label="Synthesized Audio")
210
+ spectrogram_output = gr.Image(label="Spectrogram")
211
+
212
+ @gpu_decorator
213
+ def basic_tts(
214
+ model_drop_down: str,
215
+ ref_audio_input: str,
216
+ ref_text_input: str,
217
+ ref_dialect_radio: str,
218
+ gen_text_input: str,
219
+ dialect_radio: str,
220
+ remove_silence: bool,
221
+ cross_fade_duration_slider: float,
222
+ nfe_slider: int,
223
+ speed_slider: float,
224
+ ):
225
+ ref_audio_info = torchaudio.info(ref_audio_input)
226
+ ref_duration = ref_audio_info.num_frames / ref_audio_info.sample_rate
227
+ target_duration = (
228
+ ref_duration
229
+ * len(gen_text_input.replace(" ", ""))
230
+ / len(ref_text_input.replace(" ", ""))
231
+ / speed_slider
232
+ )
233
+ print(f"Reference duration: {ref_duration}")
234
+ print(f"Target duration: {target_duration}")
235
+ if len(ref_text_input) == 0:
236
+ raise gr.Error("請勿輸入空字串。")
237
+ words, ipa, pinyin, missing_words = get_ipa(
238
+ ref_text_input, dialect=ref_dialect_radio
239
+ )
240
+ if len(missing_words) > 0:
241
+ raise gr.Error(
242
+ f"參考句子中的[{','.join(missing_words)}]目前無法轉成 ipa。請嘗試其他句子。"
243
+ )
244
+ ref_text_input = parse_ipa(ipa)
245
+
246
+ if len(gen_text_input) == 0:
247
+ raise gr.Error("請勿輸入空字串。")
248
+ words, ipa, pinyin, missing_words = get_ipa(
249
+ gen_text_input, dialect=dialect_radio
250
+ )
251
+ if len(missing_words) > 0:
252
+ raise gr.Error(
253
+ f"生成句子中的[{','.join(missing_words)}]目前無法轉成 ipa。請嘗試其他句子。"
254
+ )
255
+ gen_text_input = parse_ipa(ipa)
256
+
257
+ audio_out, spectrogram_path = infer(
258
+ ref_audio_input,
259
+ ref_text_input,
260
+ gen_text_input,
261
+ models_config[model_drop_down],
262
+ remove_silence,
263
+ cross_fade_duration=cross_fade_duration_slider,
264
+ nfe_step=nfe_slider,
265
+ fix_duration=ref_duration + target_duration,
266
+ )
267
+ return audio_out, spectrogram_path
268
+
269
+ generate_btn.click(
270
+ basic_tts,
271
+ inputs=[
272
+ model_drop_down,
273
+ ref_audio_input,
274
+ ref_text_input,
275
+ ref_dialect_radio,
276
+ gen_text_input,
277
+ dialect_radio,
278
+ remove_silence,
279
+ cross_fade_duration_slider,
280
+ nfe_slider,
281
+ speed_slider,
282
+ ],
283
+ outputs=[audio_output, spectrogram_output],
284
+ )
285
+ gr.Examples(
286
+ [
287
+ [
288
+ "./ref_wav/0000001_0.15-0.93.wav",
289
+ "恁早",
290
+ "sixian",
291
+ "食飯愛正經食,正毋會食到半出半入",
292
+ "sixian",
293
+ ],
294
+ [
295
+ "./ref_wav/0000002_0.15-2.73.wav",
296
+ "你今晡日著到恁派頭",
297
+ "sixian",
298
+ "食飯愛正經食,正毋會食到半出半入",
299
+ "sixian",
300
+ ],
301
+ [
302
+ "./ref_wav/0000002_0.15-2.73.wav",
303
+ "你今晡日著到恁派頭",
304
+ "sixian",
305
+ "歸條路吊等長長个花燈,祈求風調雨順,歸屋下人个心願,親像花燈下燒暖个光華",
306
+ "sixian",
307
+ ],
308
+ # [
309
+ # "預設語者",
310
+ # "戴君儒",
311
+ # "hailu",
312
+ # "男女平等个時代,平平做得受教育",
313
+ # ],
314
+ # [
315
+ # "預設語者",
316
+ # "宋涵葳",
317
+ # "dapu",
318
+ # "客家山城乜跈緊鬧熱䟘來咧",
319
+ # ],
320
+ # [
321
+ # "預設語者",
322
+ # "江芮敏",
323
+ # "raoping",
324
+ # "頭擺匱人,戴个毋係菅草屋,个創商品哦",
325
+ # ],
326
+ # [
327
+ # "預設語者",
328
+ # "洪藝晅",
329
+ # "zhaoan",
330
+ # "歇熱个時務,阿松歸屋下轉去在客莊个老屋",
331
+ # ],
332
+ # [
333
+ # "預設語者",
334
+ # "江芮敏",
335
+ # "nansixian",
336
+ # "在𠊎讀小學一年生个時節,阿爸輒常用自轉車載𠊎去學校讀書",
337
+ # ],
338
+ ],
339
+ label="範例",
340
+ inputs=[
341
+ ref_audio_input,
342
+ ref_text_input,
343
+ ref_dialect_radio,
344
+ gen_text_input,
345
+ dialect_radio,
346
+ ],
347
+ )
348
+
349
+ demo.launch()
configs/dialects.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ 四縣: sixian
2
+ 海陸: hailu
configs/ipa.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ delimiter_list: ${gh_download:FormoSpeech/FormoG2P, hakka/normalize/delimiters.json}
2
+ replace_dict: ${gh_download:FormoSpeech/FormoG2P, hakka/normalize/replaced_words_htia.json}
3
+ v2f_dict: ${gh_download:FormoSpeech/FormoG2P, [hakka/normalize/v2f_goyu.json, hakka/normalize/v2f_htia.json]}
4
+ preserved_list: ${gh_download:FormoSpeech/FormoG2P, hakka/normalize/preserved_words_htia.json}
5
+ pinyin_to_ipa_dict: ${gh_download:FormoSpeech/FormoG2P, hakka/normalize/pinyin_to_ipa_htia.json}
6
+ lexicon:
7
+ sixian: ${gh_download:FormoSpeech/FormoG2P, hakka/sixian.json}
8
+ hailu: ${gh_download:FormoSpeech/FormoG2P, hakka/hailu.json}
9
+ dapu: ${gh_download:FormoSpeech/FormoG2P, hakka/dapu.json}
10
+ nansixian: ${gh_download:FormoSpeech/FormoG2P, hakka/nansixian.json}
11
+ raoping: ${gh_download:FormoSpeech/FormoG2P, hakka/raoping.json}
12
+ zhaoan: ${gh_download:FormoSpeech/FormoG2P, hakka/zhaoan.json}
configs/models.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ step-97614: ${load_f5tts:hf://formospeech/f5-tts-hakka-finetune/model_97614.safetensors,hf://formospeech/f5-tts-hakka-finetune/vocab.txt}
2
+ step-195228: ${load_f5tts:hf://formospeech/f5-tts-hakka-finetune/model_195228.safetensors,hf://formospeech/f5-tts-hakka-finetune/vocab.txt}
ipa/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+
3
+ import requests
4
+ from omegaconf import OmegaConf
5
+
6
+
7
+ def gh_download(repo, path):
8
+ paths = [path] if isinstance(path, str) else path
9
+ result = None
10
+ for path in paths:
11
+ url = f"https://raw.githubusercontent.com/{repo}/refs/heads/main/{path}"
12
+ response = requests.get(url)
13
+ if response.status_code != 200:
14
+ print(f"Status code: {response.status_code}")
15
+ raise Exception(f"Failed to download {path} from {repo}")
16
+
17
+ if result is None:
18
+ result = response.json()
19
+ elif isinstance(result, list):
20
+ result.extend(response.json())
21
+ elif isinstance(result, dict):
22
+ result.update(response.json())
23
+ time.sleep(0.5)
24
+ return result
25
+
26
+
27
+ OmegaConf.register_new_resolver("gh_download", gh_download)
ipa/convert_digits.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Hung-Shin Lee (hungshinlee@gmail.com)
2
+ # Apache 2.0
3
+
4
+ import itertools
5
+ import re
6
+
7
+ c_basic = "零一二三四五六七八九"
8
+ d2c = {str(d): c for d, c in enumerate(c_basic)}
9
+ d2c["."] = "點"
10
+
11
+
12
+ def num4year(matched):
13
+ def _num4year(num):
14
+ return "{}".format("".join([c_basic[int(i)] for i in num]))
15
+
16
+ matched_str = matched.group(0)
17
+ for m in matched.groups():
18
+ matched_str = matched_str.replace(m, _num4year(m))
19
+ return matched_str
20
+
21
+
22
+ def num2chines_simple(matched):
23
+ return "{}".format("".join([d2c[i] for i in matched]))
24
+
25
+
26
+ def num4percent(matched):
27
+ matched = matched.group(1)
28
+ return "百分之{}".format(num2chinese(matched[:-1]))
29
+
30
+
31
+ def num4cellphone(matched):
32
+ matched = matched.group(1)
33
+ matched = matched.replace(" ", "").replace("-", "")
34
+ return "".join([c_basic[int(i)] for i in matched])
35
+
36
+
37
+ def num4er(matched): # 2 to 二
38
+ matched = matched.group(1)
39
+ return matched.replace("2", "二")
40
+
41
+
42
+ def num4liang(matched): # 2 to 兩
43
+ matched = matched.group(1)
44
+ return matched.replace("2", "兩")
45
+
46
+
47
+ def num4general(matched):
48
+ num = matched.group(1)
49
+ if re.match("[A-Za-z-─]", num[0]):
50
+ if len(num[1:]) < 3:
51
+ # MP3 or F-16
52
+ return "{}{}".format(num[0], num2chinese(num[1:]))
53
+ else:
54
+ # AM104
55
+ return "{}{}".format(num[0], num2chines_simple(num[1:]))
56
+
57
+ else:
58
+ if re.match("[0-9]", num[0]):
59
+ return "{}".format(num2chinese(num))
60
+ else:
61
+ return "{}{}".format(num[0], num2chinese(num[1:]))
62
+
63
+
64
+ def parse_num(text: str) -> str:
65
+ # year
66
+ text = re.sub("([0-9]{4})[到至]([0-9]{4})年", num4year, text)
67
+ text = re.sub("([0-9]{4})年", num4year, text)
68
+
69
+ # percentage
70
+ text = re.sub(r"([0-9]+\.?[0-9]?%)", num4percent, text)
71
+
72
+ # cellphone
73
+ text = re.sub(r"([0-9]{4}\s?-\s?[0-9]{6})", num4cellphone, text)
74
+
75
+ # single 2 to 二
76
+ text = re.sub(r"([^\d]2[診樓月號])", num4er, text)
77
+ text = re.sub(r"([初]2[^\d])", num4er, text)
78
+
79
+ # single 2 to 兩
80
+ text = re.sub(r"([^\d]2[^\d])", num4liang, text)
81
+
82
+ # general number
83
+ text = re.sub(r"([^0-9]?[0-9]+\.?[0-9]?)", num4general, text)
84
+
85
+ return text
86
+
87
+
88
+ def num2chinese(num, big=False, simp=False, o=False, twoalt=True) -> str:
89
+ """
90
+ Converts numbers to Chinese representations.
91
+ https://gist.github.com/gumblex/0d65cad2ba607fd14de7
92
+ `big` : use financial characters.
93
+ `simp` : use simplified characters instead of traditional characters.
94
+ `o` : use 〇 for zero.
95
+ `twoalt`: use 两/兩 for two when appropriate.
96
+ Note that `o` and `twoalt` is ignored when `big` is used,
97
+ and `twoalt` is ignored when `o` is used for formal representations.
98
+ """
99
+ # check num first
100
+ nd = str(num)
101
+ if abs(float(nd)) >= 1e48:
102
+ raise ValueError("number out of range")
103
+ elif "e" in nd:
104
+ raise ValueError("scientific notation is not supported")
105
+ c_symbol = "正负点" if simp else "正負點"
106
+ if o: # formal
107
+ twoalt = False
108
+ if big:
109
+ c_basic = "零壹贰叁肆伍陆柒捌玖" if simp else "零壹貳參肆伍陸柒捌玖"
110
+ c_unit1 = "拾佰仟"
111
+ c_twoalt = "贰" if simp else "貳"
112
+ else:
113
+ c_basic = "〇一二三四五六七八九" if o else "零一二三四五六七八九"
114
+ c_unit1 = "十百千"
115
+ if twoalt:
116
+ c_twoalt = "两" if simp else "兩"
117
+ else:
118
+ c_twoalt = "二"
119
+ c_unit2 = "万亿兆京垓秭穰沟涧正载" if simp else "萬億兆京垓秭穰溝澗正載"
120
+
121
+ def revuniq(l):
122
+ return "".join(k for k, g in itertools.groupby(reversed(l)))
123
+
124
+ nd = str(num)
125
+ result = []
126
+ if nd[0] == "+":
127
+ result.append(c_symbol[0])
128
+ elif nd[0] == "-":
129
+ result.append(c_symbol[1])
130
+ if "." in nd:
131
+ integer, remainder = nd.lstrip("+-").split(".")
132
+ else:
133
+ integer, remainder = nd.lstrip("+-"), None
134
+ if int(integer):
135
+ splitted = [integer[max(i - 4, 0) : i] for i in range(len(integer), 0, -4)]
136
+ intresult = []
137
+ for nu, unit in enumerate(splitted):
138
+ # special cases
139
+ if int(unit) == 0: # 0000
140
+ intresult.append(c_basic[0])
141
+ continue
142
+ elif nu > 0 and int(unit) == 2: # 0002
143
+ intresult.append(c_twoalt + c_unit2[nu - 1])
144
+ continue
145
+ ulist = []
146
+ unit = unit.zfill(4)
147
+ for nc, ch in enumerate(reversed(unit)):
148
+ if ch == "0":
149
+ if ulist: # ???0
150
+ ulist.append(c_basic[0])
151
+ elif nc == 0:
152
+ ulist.append(c_basic[int(ch)])
153
+ elif nc == 1 and ch == "1" and all([i == "0" for i in unit[: nc + 1]]):
154
+ # special case for tens
155
+ # edit the 'elif' if you don't like
156
+ # 十四, 三千零十四, 三千三百一十���
157
+ ulist.append(c_unit1[0])
158
+ elif nc > 1 and ch == "2":
159
+ ulist.append(c_twoalt + c_unit1[nc - 1])
160
+ else:
161
+ ulist.append(c_basic[int(ch)] + c_unit1[nc - 1])
162
+ # print(ulist)
163
+ ustr = revuniq(ulist)
164
+ if nu == 0:
165
+ intresult.append(ustr)
166
+ else:
167
+ intresult.append(ustr + c_unit2[nu - 1])
168
+ result.append(revuniq(intresult).strip(c_basic[0]))
169
+ else:
170
+ result.append(c_basic[0])
171
+ if remainder:
172
+ result.append(c_symbol[2])
173
+ result.append("".join(c_basic[int(ch)] for ch in remainder))
174
+ return "".join(result)
175
+
176
+
177
+ if __name__ == "__main__":
178
+ text = "若手機仔幾多號?吾手機仔係0964-498042。"
179
+
180
+ print(f"{text} -> {parse_num(text)}")
ipa/ipa.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+
5
+ import jieba
6
+ from omegaconf import OmegaConf
7
+
8
+ from ipa.convert_digits import parse_num
9
+ from ipa.proc_text import (
10
+ apply_v2f,
11
+ normalize_text,
12
+ prep_regex,
13
+ run_jieba,
14
+ update_jieba_dict,
15
+ )
16
+
17
+ ipa_configs = OmegaConf.to_object(OmegaConf.load("configs/ipa.yaml"))
18
+ for key in ipa_configs["preserved_list"]:
19
+ ipa_configs["v2f_dict"].pop(key, None)
20
+ delimiter_regex, replace_regex, v2f_regex = prep_regex(
21
+ ipa_configs["delimiter_list"], ipa_configs["replace_dict"], ipa_configs["v2f_dict"]
22
+ )
23
+
24
+
25
+ def get_ipa(raw_text: str, dialect: str) -> tuple[str, str, str, list[str]]:
26
+ pinyin_split = re.split(r"([a-z]+\d+)", raw_text)
27
+
28
+ final_words = []
29
+ final_pinyin = []
30
+ final_ipa = []
31
+ final_missing_words = []
32
+ for hanzi_or_pinyin in pinyin_split:
33
+ if len(hanzi_or_pinyin.strip()) == 0:
34
+ continue
35
+
36
+ if re.search(r"[a-z]+\d+", hanzi_or_pinyin):
37
+ final_words.append(hanzi_or_pinyin)
38
+ final_pinyin.append(hanzi_or_pinyin)
39
+ pinyin, tone = re.match(r"([a-z]+)(\d+)?", hanzi_or_pinyin).groups()
40
+ tone = f"_{tone}" if tone else ""
41
+
42
+ ipa = parse_pinyin_to_ipa(pinyin)
43
+ if ipa is None:
44
+ final_missing_words.append(pinyin)
45
+ continue
46
+
47
+ final_ipa.append(ipa + tone)
48
+ else:
49
+ words, ipa, pinyin, missing_words = parse_hanzi_to_ipa(
50
+ hanzi_or_pinyin, dialect
51
+ )
52
+ final_words.extend(words)
53
+ final_ipa.extend(ipa)
54
+ final_pinyin.extend(pinyin)
55
+ final_missing_words.extend(missing_words)
56
+
57
+ if len(final_ipa) == 0 or len(final_missing_words) > 0:
58
+ return final_words, final_ipa, final_pinyin, final_missing_words
59
+
60
+ final_words = " ".join(final_words).replace(" , ", ",")
61
+ final_ipa = " ".join(final_ipa)
62
+ final_pinyin = " ".join(final_pinyin).replace(" , ", ",")
63
+
64
+ return final_words, final_ipa, final_pinyin, final_missing_words
65
+
66
+
67
+ def parse_ipa(ipa: str, delete_chars="\+\|", as_space="") -> list[str]:
68
+ text = []
69
+
70
+ ipa_list = re.split(r"(?<![\d])(?=[\d])|(?<=[\d])(?![\d])", ipa)
71
+ for word in ipa_list:
72
+ if word.isdigit():
73
+ phrases = text[-1].split("-")
74
+ for i, phrase in enumerate(phrases):
75
+ phrases[i] += word
76
+ text[-1] = "-".join(phrases)
77
+
78
+ else:
79
+ text.append(word)
80
+
81
+ text = "".join(text)
82
+ text = re.sub(f"[{delete_chars}]", "-", text)
83
+ text = text.replace("_", "")
84
+
85
+ return text
86
+
87
+
88
+ def parse_pinyin_to_ipa(pinyin: str) -> str | None:
89
+ if pinyin not in ipa_configs["pinyin_to_ipa_dict"]:
90
+ return None
91
+
92
+ ipa_dict_result = ipa_configs["pinyin_to_ipa_dict"][pinyin]
93
+ ipa = "+".join(ipa_dict_result).replace(" ", "-")
94
+ return ipa
95
+
96
+
97
+ def parse_hanzi_to_ipa(
98
+ hanzi: str, dialect: str
99
+ ) -> tuple[list[str], list[str], list[str], list[str]]:
100
+ lexicon = ipa_configs["lexicon"][dialect]
101
+ update_jieba_dict(
102
+ list(lexicon.keys()), Path(os.path.dirname(jieba.__file__)) / "dict.txt"
103
+ )
104
+
105
+ text = normalize_text(hanzi, ipa_configs["replace_dict"], replace_regex)
106
+ text = parse_num(text)
107
+ text_parts = [s.strip() for s in re.split(delimiter_regex, text) if s.strip()]
108
+ text = ",".join(text_parts)
109
+ word_list = run_jieba(text)
110
+ word_list = apply_v2f(word_list, ipa_configs["v2f_dict"], v2f_regex)
111
+ word_list = run_jieba("".join(word_list))
112
+
113
+ final_words = []
114
+ final_pinyin = []
115
+ final_ipa = []
116
+ missing_words = []
117
+ for word in word_list:
118
+ if not bool(word.strip()):
119
+ continue
120
+ if word == ",":
121
+ final_words.append(",")
122
+ final_pinyin.append(",")
123
+ final_ipa.append(",")
124
+ elif word not in lexicon:
125
+ final_words.append(word)
126
+ missing_words.append(word)
127
+ else:
128
+ final_words.append(f"{word}")
129
+ final_pinyin.append(lexicon[word]["pinyin"][0])
130
+ # NOTE 只有 lexicon[word] 中的第一個 ipa 才被考慮
131
+ final_ipa.append(lexicon[word]["ipa"][0])
132
+
133
+ return final_words, final_ipa, final_pinyin, missing_words
ipa/proc_text.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Hung-Shin Lee (hungshinlee@gmail.com)
2
+ # Apache 2.0
3
+
4
+ import re
5
+ from pathlib import Path
6
+ from unicodedata import normalize
7
+
8
+ import jieba
9
+ import opencc
10
+
11
+ jieba.setLogLevel(20)
12
+ jieba.re_han_default = re.compile("([\u2e80-\U000e01efa-zA-Z0-9+#&\._%\-']+)", re.U)
13
+
14
+ s2tw_converter = opencc.OpenCC("s2tw.json")
15
+
16
+
17
+ def update_jieba_dict(
18
+ lexicon: list,
19
+ jieba_dict_path: Path,
20
+ high_freq_words: list = [],
21
+ high_freq_words_weight: int = 10,
22
+ ) -> list:
23
+ lexicon = sorted(set(lexicon))
24
+
25
+ jieba_dict_path.unlink(missing_ok=True)
26
+ Path("/tmp/jieba.cache").unlink(missing_ok=True)
27
+
28
+ with jieba_dict_path.open("w", encoding="utf-8") as file:
29
+ for word in lexicon:
30
+ if word in high_freq_words:
31
+ file.write(f"{word} {len(word) * high_freq_words_weight}\n")
32
+ else:
33
+ file.write(f"{word} {len(word)}\n")
34
+
35
+ jieba.dt.initialized = False
36
+
37
+ return lexicon
38
+
39
+
40
+ def run_jieba(line: str) -> list:
41
+ # NOTE JIEBA 處理多行文本的結果會失去原本的行結構
42
+
43
+ seg_list = list(jieba.cut(line, cut_all=False, HMM=False))
44
+
45
+ return seg_list
46
+
47
+
48
+ def normalize_text(text: str, replace_dict: dict, replace_regex: str) -> str:
49
+ def replace_match(match):
50
+ return replace_dict[match.group(0)]
51
+
52
+ text = re.sub("\x08", "", text)
53
+ text = re.sub("\ufeff", "", text)
54
+ text = re.sub("\u0010", "", text)
55
+ text = normalize("NFKC", text)
56
+ text = re.sub(replace_regex, replace_match, text)
57
+ text = " ".join(text.split()).upper()
58
+
59
+ return text
60
+
61
+
62
+ def apply_v2f(word_list: list, v2f_dict: dict, v2f_regex: str) -> list:
63
+ result = []
64
+ for word in word_list:
65
+ result.append(re.sub(v2f_regex, lambda x: v2f_dict[x.group(0)], word))
66
+
67
+ return result
68
+
69
+
70
+ def prep_regex(
71
+ delimiter_list: list, replace_dict: dict = {}, v2f_dict: dict = {}
72
+ ) -> tuple[str, str, str]:
73
+ delimiter_regex = "|".join(map(re.escape, delimiter_list))
74
+
75
+ replace_regex = ""
76
+ if len(replace_dict):
77
+ sorted_keys = sorted(replace_dict.keys(), key=len, reverse=True)
78
+ replace_regex = "|".join(map(re.escape, sorted_keys))
79
+
80
+ v2f_regex = ""
81
+ if len(v2f_dict):
82
+ v2f_regex = "|".join(map(re.escape, v2f_dict.keys()))
83
+
84
+ return delimiter_regex, replace_regex, v2f_regex
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ omegaconf
2
+ opencc
3
+ git+https://github.com/SWivid/F5-TTS.git