Spaces:
Runtime error
Runtime error
Commit ·
7a23caf
1
Parent(s): 425413b
feat: add app.py
Browse files- app.py +349 -0
- configs/dialects.yaml +2 -0
- configs/ipa.yaml +12 -0
- configs/models.yaml +2 -0
- ipa/__init__.py +27 -0
- ipa/convert_digits.py +180 -0
- ipa/ipa.py +133 -0
- ipa/proc_text.py +84 -0
- requirements.txt +3 -0
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
|