feat: change waveform

This commit is contained in:
2026-05-07 11:29:21 +06:00
commit d31233a79a
21 changed files with 5330 additions and 0 deletions
File diff suppressed because one or more lines are too long
+70
View File
@@ -0,0 +1,70 @@
from pathlib import Path
from tqdm import tqdm
from mutagen.mp3 import MP3
from mutagen import MutagenError
import pandas as pd
workspace_dir = Path(__file__).parent.parent.parent
def format_duration(seconds):
"""将秒数格式化为 时:分:秒 的格式"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
if hours > 0:
return f"{hours}小时{minutes}{secs}"
elif minutes > 0:
return f"{minutes}{secs}"
else:
return f"{secs}"
def get_mp3_duration(file_path: Path):
"""获取单个MP3文件的时长(秒)"""
try:
audio = MP3(file_path)
return audio.info.length
except MutagenError as e:
print(f"错误:无法读取文件 {file_path} - {e}")
return None
except Exception as e:
print(f"错误:处理文件 {file_path} 时出错 - {e}")
return None
def analyze_mp3_files(directory: Path, ):
"""分析目录中的所有MP3文件"""
results = []
total_duration = 0
df = pd.read_csv(workspace_dir / '.data/ug/train_new.tsv', sep='\t')
for _, row in tqdm(df.iterrows(), total=len(df), desc="anlayze audio"):
mp3_file: Path = workspace_dir / '.data/ug/clips' / row['path']
duration = get_mp3_duration(mp3_file)
if duration is not None:
total_duration += duration
duration_str = format_duration(duration)
file_size = mp3_file.stat().st_size / (1024 * 1024) # 转换为MB
result_line = f"{mp3_file.name:<50} {duration_str:>20} ({file_size:.2f} MB)"
results.append(result_line)
else:
error_line = f"{mp3_file.name:<50} {'读取失败':>20}"
print(error_line)
results.append(error_line)
mp3_files: list[Path] = []
for ext in ['*.mp3', '*.MP3']:
mp3_files.extend(directory.rglob(ext))
print(f"tsv 找到 {len(results)} 个MP3文件\n")
print(f"找到 {len(mp3_files)} 个MP3文件\n")
print(f"\n总时长: {format_duration(total_duration)}")
print(f"总时长(秒): {total_duration:.2f}")
print(f"总时长(分钟): {total_duration/60:.2f} 分钟")
print(f"总时长(小时): {total_duration/3600:.2f} 小时")
print(f"文件总数: {len(mp3_files)}")
audio_directory = Path(workspace_dir / '.data/ug/clips')
analyze_mp3_files(directory=audio_directory)
+13
View File
@@ -0,0 +1,13 @@
from pathlib import Path
import torch
device = "cuda:0"
workspace_dir = Path(__file__).parent.parent.parent
input_checkpoint = workspace_dir.joinpath('.checkpoints/best_wer_model.pt')
output_checkpoint = workspace_dir.joinpath('.checkpoints/prodect_best_wer_model.pt')
checkpoint = torch.load(input_checkpoint, map_location=device)
torch.save(checkpoint['model_state_dict'], output_checkpoint)
+37
View File
@@ -0,0 +1,37 @@
from pathlib import Path
import pandas as pd
workspace_dir = Path(__file__).parent.parent.parent
# 读取你合并后的总表 (253,430条)
df = pd.read_csv(workspace_dir / ".data/ug/validated.tsv", sep='\t')
# 1. 获取所有唯一的说话人
all_speakers = df['client_id'].unique()
# 2. 随机打乱说话人顺序
import random
random.seed(42) # 固定随机种子,保证实验可重复
random.shuffle(all_speakers)
# 3. 挑选验证集说话人,直到录音总数达到 ~8000 条
val_indices = []
val_count = 0
target_val_size = 8000
for speaker in all_speakers:
speaker_data = df[df['client_id'] == speaker]
val_indices.extend(speaker_data.index.tolist())
val_count += len(speaker_data)
if val_count >= target_val_size:
break
# 4. 划分文件
df_val = df.loc[val_indices]
df_train = df.drop(val_indices)
print(f"训练集条数: {len(df_train)}")
print(f"验证集条数: {len(df_val)}")
df_train.to_csv(workspace_dir / ".data/ug/train_new.tsv", sep='\t', index=False)
df_val.to_csv(workspace_dir / ".data/ug/val_new.tsv", sep='\t', index=False)
+92
View File
@@ -0,0 +1,92 @@
import json
from pathlib import Path
from typing import Counter
import pandas as pd
from tqdm import tqdm
from text_handle import process_text
workspace_dir = Path(__file__).parent.parent.parent
data_dir = Path("/home/blacksheep/projekts/study")
input_files = [
data_dir / "data/multilingual/hanziDB_translated-simplified.jsonl",
data_dir / "data/multilingual/hanziDB_translated_validationset-simplified.jsonl",
data_dir / "data/multilingual/tatoeba-tr-en-ug-uz-kz-zh-simplified.jsonl",
data_dir / "data/multilingual/export_csv_database.jsonl",
data_dir / "data/multilingual/tatoeba_sentences.jsonl",
]
output_file = workspace_dir / "config/asr_vocab_1.json"
syllabizes = []
# langs = ["uig_Arab"]
# for input_file in input_files:
# with open(input_file, 'r', encoding='utf-8') as f:
# total_lines = sum(1 for _ in f)
# f.seek(0)
# for line in tqdm(f, total=total_lines, desc=f"Processing lines {input_file}"):
# data: dict[str, str] = json.loads(line)
# for lang in langs:
# if lang in data:
# syllabizes.extend(export_syllabize(data[lang]))
# print(f'data_dir syllabize len: {len(syllabizes):,}, set: {len(set(syllabizes)):,}')
tsv_files = [
# workspace_dir / "data/ug/test.tsv",
# workspace_dir / "data/ug/invalidated.tsv",
# workspace_dir / "data/ug/train.tsv",
# workspace_dir / "data/ug/validated.tsv",
# workspace_dir / "data/ug/reported.tsv",
# workspace_dir / "data/ug/dev.tsv",
# workspace_dir / "data/ug/other.tsv",
# workspace_dir / ".data/ug/invalidated.tsv",
workspace_dir / ".data/ug/train.tsv",
# workspace_dir / ".data/ug/clip_durations.tsv", # not sentence
# workspace_dir / ".data/ug/test.tsv",
# workspace_dir / ".data/ug/validated_sentences.tsv",
# workspace_dir / ".data/ug/other.tsv",
# workspace_dir / ".data/ug/validated.tsv",
# workspace_dir / ".data/ug/dev.tsv",
# workspace_dir / ".data/ug/unvalidated_sentences.tsv",
# workspace_dir / ".data/ug/reported.tsv" # Lacking sentence
]
for tsv_file in tsv_files:
data = pd.read_csv(tsv_file, sep='\t')
# 带进度条处理每行数据
for index, row in tqdm(data.iterrows(), total=len(data), desc=f"Processing {tsv_file}"):
syllabizes.extend(process_text(row['sentence'].strip()))
# 统计所有音节出现次数
syllable_counter = Counter(syllabizes)
# 过滤出出现100次以上的音节
freq_100_plus = {k: v for k, v in syllable_counter.items() if v >= 150}
freq_100_minus = {k: v for k, v in syllable_counter.items() if v <= 100}
# 保存100次以上的音节列表(只有音节,排序)
vocab = sorted(list(freq_100_plus.keys()), key=len)
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(vocab, f, ensure_ascii=False, indent=2)
# # # 保存100次以上的音节及次数
sorted_freq_100_plus = dict(sorted(freq_100_plus.items(), key=lambda x: x[1], reverse=True))
with open(workspace_dir / 'config/syllables_freq_100_plus.json', 'w', encoding='utf-8') as f:
json.dump(sorted_freq_100_plus, f, ensure_ascii=False, indent=2)
# # 统计信息
print(f"总音节数: {len(syllabizes):,}")
print(f"唯一音节数: {len(syllable_counter):,}")
print(f"出现100次以上的音节数: {len(freq_100_plus):,}")
print(f"出现100次以下的音节数: {len(freq_100_minus):,}")
# 区间统计(不累积)
print("\n=== 音节使用次数统计(区间) ===")
for low in range(0, 100, 10):
high = low + 9
count = sum(1 for freq in syllable_counter.values() if low <= freq <= high)
print(f"出现 {low}-{high} 次: {count:,} 个音节")
# 100次以上
count_100plus = sum(1 for freq in syllable_counter.values() if freq >= 100)
print(f"出现 100+ 次: {count_100plus} 个音节")
File diff suppressed because one or more lines are too long
+220
View File
@@ -0,0 +1,220 @@
import os
import random
import sys
import pandas as pd
import torchaudio
from pathlib import Path
from tqdm import tqdm
workspace_dir = Path(__file__).parent.parent.parent
sys.path.append(str(workspace_dir.joinpath('src')))
# sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from handle.text_handle import process_text
from tokenizer import ASRTokenizer
# 读取 TSV
# df = pd.read_csv(workspace_dir / '.data/ug/train.tsv', sep='\t')
# # 计算每个音频的时长(秒)
# durations = []
# for audio_path in tqdm(workspace_dir / '.data/ug/clips' / df['path'], desc="计算音频时长"):
# try:
# # 获取音频信息(不加载整个文件,速度快)
# info = torchaudio.info(audio_path)
# duration = info.num_frames / info.sample_rate
# durations.append(duration)
# except Exception as e:
# print(f"读取失败: {audio_path}, 错误: {e}")
# durations.append(None)
# # 添加到 DataFrame
# df['duration'] = durations
# # 统计
# print(df['duration'].describe())
# print(f"超过20秒的样本: {(df['duration'] > 20).sum()}")
# 保存结果(可选)
# df.to_csv('data/audio/train_with_duration.tsv', sep='\t', index=False)
# audio_path = workspace_dir / '.data/ug/clips/common_voice_ug_40252722.mp3'
# info = torchaudio.info(audio_path)
# print("info:", info)
import pandas as pd
import torch
import torchaudio
import numpy as np
from pathlib import Path
from tqdm import tqdm
def analyze_audio_quality(audio_path, sample_rate_target=16000):
"""
评估音频质量,返回多个指标
"""
try:
# 加载音频
waveform, sr = torchaudio.load(str(audio_path))
# 重采样 + 单声道
if sr != sample_rate_target:
resampler = torchaudio.transforms.Resample(sr, sample_rate_target)
waveform = resampler(waveform)
if waveform.shape[0] > 1:
waveform = waveform.mean(dim=0, keepdim=True)
# 归一化
max_val = waveform.abs().max()
if max_val > 0:
waveform = waveform / max_val
# ===== 指标 1: 静音比例 =====
silence_threshold = 0.01
is_silence = waveform.abs() < silence_threshold
silence_ratio = is_silence.float().mean().item()
# ===== 指标 2: 动态范围 =====
frame_len = int(0.025 * sample_rate_target) # 25ms
hop_len = int(0.010 * sample_rate_target) # 10ms
num_frames = (waveform.shape[1] - frame_len) // hop_len + 1
if num_frames <= 0:
return None
energies = []
for i in range(num_frames):
start = i * hop_len
frame = waveform[:, start:start + frame_len]
rms = frame.pow(2).mean().sqrt().item()
energies.append(rms)
energies = np.array(energies)
energies = np.clip(energies, 1e-10, None)
log_energies = 20 * np.log10(energies)
dynamic_range = log_energies.max() - log_energies.min()
# ===== 指标 3: 语音活跃度 =====
energy_threshold = np.percentile(energies, 30)
voice_ratio = (energies > energy_threshold).mean()
# ===== 指标 4: 频谱中心频率 =====
spec_transform = torchaudio.transforms.Spectrogram(n_fft=512)
spec = spec_transform(waveform) # [1, freq, time]
# 用 torch 计算,避免 numpy axis 问题
freqs = torch.fft.rfftfreq(512, d=1.0/sample_rate_target)
spec_mean = spec.mean(dim=-1).squeeze() # [freq_bins] torch 张量
# 确保类型一致,用 torch 计算
spectral_centroid = (freqs * spec_mean).sum() / (spec_mean.sum() + 1e-10)
spectral_centroid = spectral_centroid.item()
# ===== 指标 5: 频谱通量 =====
spec_np = spec.squeeze().numpy() # [freq, time]
flux = np.abs(np.diff(spec_np, axis=1)).mean()
return {
'duration': waveform.shape[1] / sample_rate_target,
'silence_ratio': silence_ratio,
'dynamic_range_db': float(dynamic_range),
'voice_ratio': float(voice_ratio),
'spectral_centroid': spectral_centroid,
'spectral_flux': float(flux),
'energy_std': float(energies.std()),
}
except Exception as e:
print(f"失败: {audio_path}, {e}")
return None
# ===== 批量处理 =====
# df = pd.read_csv(workspace_dir / '.data/ug/train.tsv', sep='\t')
# results = []
# for _, row in tqdm(df.iterrows(), total=len(df), desc="评估音频质量"):
# audio_path = workspace_dir / '.data/ug/clips' / row['path']
# metrics = analyze_audio_quality(audio_path)
# if metrics:
# metrics['path'] = row['path']
# results.append(metrics)
# # 汇总
# quality_df = pd.DataFrame(results)
# print(quality_df.describe())
# # ===== 质量评分 =====
# def score_quality(row):
# score = 100
# if row['silence_ratio'] > 0.5:
# score -= 30
# elif row['silence_ratio'] > 0.3:
# score -= 15
# if row['dynamic_range_db'] < 10:
# score -= 25
# elif row['dynamic_range_db'] < 20:
# score -= 10
# if row['voice_ratio'] < 0.3:
# score -= 20
# if row['spectral_centroid'] > 4000:
# score -= 15
# return max(0, score)
# quality_df['quality_score'] = quality_df.apply(score_quality, axis=1)
# quality_df['grade'] = pd.cut(
# quality_df['quality_score'],
# bins=[0, 40, 60, 80, 100],
# labels=['差(弃用)', '较差', '一般', '良好']
# )
# print("\n质量分级统计:")
# print(quality_df['grade'].value_counts())
# # 保存结果
# quality_df.to_csv('audio_quality_analysis.csv', index=False)
# print("\n结果已保存到 audio_quality_analysis.csv")
# Test
# text = "مەن مەكتەپكە باردىم"
# text = "ئاۋۋال كومپىيوتىرنى بىر كۆرسەم شۇنىڭغا قاراپ ماسلاشتۇرسام بوللاتتى"
text = "قاپاقنى پۇلغا ئالماي، باراڭنى پۇلغا ئاپتۇ"
# text = "ئامېىقى جاھان گۈرلىكىە قارشتۇرۇپ چوكشەڭگە ياردەم بېرىش ئۇرۇشىنىڭ ئلۇغ غەلبىسى جۇڭگو خەلقى ئورندىن دەستۇرغاندىكىن، دۇنيانىڭ شەرقىدە قەت كۆتۈرگەنلكىنى خىتاب لامىسى."
# text = "ئاۋسترالىيە"
text = " ھەر قانداق ئىشتا كىشىلەر بىلەن كېڭىشىش كېرەك. لېكىن كۆڭۈل تارتقان ئىشنى قىلىۋېرىش كېرەك."
text = "\" 15 يىل بۇرۇن مەن بىلەن سەي چۇڭشىن (ئالى بابانىڭ قۇرغۇچىسىدىن بىرى) ئامېرىكىغا بېرىپ 30 نەچچە مەبلەغ سالغۇچى شىركەتلەر بىلەن كۆرۈشكىنىمىزدە ئۇلار بىزنى رەت قىلىپ ئىشىك سىرتىدا قالدۇرغاندى. ھېچكىم بىزگە ۋە كەلگۈسىمىزگە ئىشەنمىگەن ئىدى، ھېچكىممۇ بىزنىڭ ھازىر 300 مىليارد سودىنى تاماملىيالايدىغانلىقىمىزنى ئويلاپ خىيالىغىمۇ كەلتۈرمىگەن ئىدى.\" ئالى بابانىنىڭ ئورگىنى تەرىپىدىن ئىشلەنگەن ئىگىلىك تىكلەش ھۆججەتلىك فىلىمى \" بۇ چۈش ئەمەس\" نىڭ باش قىسمىدا مۇنداق بىر داڭلىق سۆز چىقىدۇ:\" مەن تاغدىن ئۆتكەن ۋاقتىمدا تاغ ماڭا گەپ قىلمىدى، مەن دېڭىزنى كېچىپ ئۆتكىنىمدە دېڭىز ماڭا گەپ قىلمىدى.\" بۇ فىلىمنىڭ قوشۇمچە ئىسمى بولسا \" مايۈن ۋە ئۇنىڭ مەڭگۈلۈك ' ياش ئالى'سى\" بولۇپ، ھەرخىل خەتەر ۋە قىيىنلىقنى بىرمۇ بىر يەڭگەن مايۈن ۋە ئۇنىڭ ئالى بابا قوشۇنىدىكى ھەمكارلاشقۇچىلىرىنىڭ قەيسەر كەچمىشى جانلىق بايان قىلىنغان."
# text = "يۆ جيەنتاۋ يېقىنقى بىر مەزگىلدە، خىزمەتداشلىرى بىلەن نۇرغۇن قىيىنچىلىققا ئۇچرىغان شوپۇرغا ياردەم قىلغانلىقىنى، شۇنداقلا يېمەكلىك ۋە سۇ يەتكۈزۈپ بەرگەنلىكىنى، ئەمما ئاياغ سوۋغا قىلىشى تۇنجى قېتىم ئىكەنلىكىنى، بۈگۈنكىسى 20 يىللىق ساقچىلىق جەريانىدا تۇنجى قېتىم شوپۇرغا ئاياغ سوۋغا قىلىشى ئىكەنلىكىنى ئېيتتى."
text = "يۆ جۇڭمىڭ مۇنداق دېدى: 2019- يىلى 12-ئايدا، مەملىكەتلىك خەلق قۇرۇلتىيى دائىمىي كومىتېتىنىڭ 44- قېتىملىق كومىتېت باشلىقلىرى يىغىنى مەملىكەتلىك خەلق قۇرۇلتىيى دائىمىي كومىتېتىنىڭ 2020-يىللىق قانۇن چىقىرىش خىزمىتى پىلانىنى پىرىنسىپ جەھەتتىن ماقۇللىدى، خىزمەت تەرتىپى بويىچە، 13-نۆۋەتلىك مەملىكەتلىك خەلق قۇرۇلتىيى 3-يىغىنىنىڭ روھى ۋە ۋەكىللەرنىڭ تەكلىپ-تەۋسىيەلىرىگە ئاساسەن پىلاننى تەڭشەش كېرەك. بۇ يىل 6-ئاينىڭ 1-كۈنى، 58-قېتىملىق كومىتېت باشلىقلىرى يىغىنى تەڭشەلگەندىن كېيىنكى يىللىق قانۇن چىقىرىش خىزمىتى پىلانىنى قاراپ چىقىپ ماقۇللىدى."
text = " ئاشقازان-ئۈچەينىڭ لۆمۈلدىشىنى ئىلگىرى سۈرىدىغان دورىنى تاماقتىن بۇرۇن ئىستېمال قىلىش كېرەك."
text = "مەن مەكتەپكە باردىم"
# text = "غەرىپئەللىرى"
# text = "ئىكەنلىكىنى، بۈگۈنكىسى 20 يىللىق ساقچىلىق جەريانىدا تۇنجى"
# text = " يېزىدىكى كەڭ، ئازادە ئۆي، باغلىرىنى تاشلاپ، خەقنىڭ ھويلىسىدا قورۇنۇپ-ئەيمىنىپ يەر دەسسەپ يۈردى. "
# text = "بۇ يىللىق ئەمگەكچىلەر بايرىمىدا، 1-مايدىن 5-مايغىچە جەمئىي بەش كۈن دەم ئېلىشقا قويۇپ بېرىلىدۇ. 9-ماي (شەنبە) نورمال خىزمەت قىلىنىدۇ. شۇنىڭ بىلەن بىر ۋاقىتتا، ئۈرۈمچى شەھىرى تىيانشان رايونى، سايباغ رايونى، داۋانچىڭ رايونى، ئۈرۈمچى ناھىيەسىدىكى ئوتتۇرا، باشلانغۇچ مەكتەپلەر 29-ئاپرېلدىن 30-ئاپرېلغىچە ئەتىيازلىق دەم ئېلىشقا قويۇپ بېرىدۇ؛ سانجى ئوبلاستىدىكى ئوتتۇرا، باشلانغۇچ مەكتەپلەرنىڭ ئەتىيازلىق دەم ئېلىشى ئۈچ كۈن بولۇپ، بۇنىڭ ئىچىدە ئىككى كۈن 29-، 30-ئاپرېلغا ئورۇنلاشتۇرۇلىدۇ، قالغان بىر كۈن 1-ئىيۇنغا ئورۇنلاشتۇرۇلىدۇ، قۇربان ھېيتلىق دەم ئېلىش بىلەن بىرلەشتۈرۈلۈپ، ئۇدا ئالتە كۈن دەم ئېلىنىدۇ."
# text = "ئەڭ يېڭى دەم ئېلىشقا قويۇپ بېرىش ئۇقتۇرۇشى! تەقەززالىق بىلەن كۈتكەن يەنە بىر دەم ئېلىش كېلەي دەپ قالدى! گوۋۇيۈەن بەنگۇڭتىڭىنىڭ «2025-يىللىق قىسمەن بايرام، دەم ئېلىش كۈنلىرىنى ئورۇنلاشتۇرۇش توغرىسىدىكى ئۇقتۇرۇشى»غا ئاساسەن، دۈەنۋۇ بايرىمىلىق دەم ئېلىشقا قويۇپ بېرىش ئورۇنلاشتۇرۇلۇشى تۆۋەندىكىچە: 5-ئاينىڭ 31-كۈنى (شەنبە)دىن 6-ئاينىڭ 2-كۈنىگىچە جەمئىي ئۈچ كۈن دەم ئېلىشقا قويۇپ بېرىلىدۇ! بۇ قېتىمقى دۈەنۋۇ بايرىمىلىق دەم ئېلىش ۋاقتى تەڭشەلمەيدۇ!"
# text = "مەن شۇ چاغدا چۈشۈمدىمۇ كۆرۈپ باقمىغان مۇنچىۋالا جىق قېرىنداشلىرىم، جىگەرلىرىمنىڭ بارلىقىدىن سۆيۈندۈم."
# text = "ئامېرىكا جاھان گىرلىكىگە قارشى تۇرۇپ، چاۋشەنگە ياردەم بىرىش ئۇرۇشىنىڭ ئۇلۇغ غەلبىسى، جۇڭگو خەلقى ئورۇندىن دەس تۇرغاندىنكىن دۇنيانىڭ شەرقىدە قەد كۆتۈرگەنلىكىنىڭ خىتاپنامىس"
# result = export_syllabize(text)
result = process_text(text)
print(f"Original: {text}")
print(f"Syllables: {result}")
# tokenizer = ASRTokenizer(vocab_path=workspace_dir / 'config/asr_vocab.json')
# print(text)
# ids = tokenizer.encode(text=text)
# # print(ids)
# print('|'.join(tokenizer.decode([id]) for id in ids))
+195
View File
@@ -0,0 +1,195 @@
from .text_normalizer import UYGHUR_LETTERS, clean_input_text
# def uighur_syllabize(text: str):
# # Uyghur Vowels
# vowels = "ئاەئەوۆئۇئۈئىئېاەوۆۇۈىې"
# def split_word(word):
# syllables = []
# current_syllable = ""
# i = 0
# # Helper to check if a character is a vowel
# is_vowel = lambda char: char in vowels
# while i < len(word):
# current_syllable += word[i]
# # If we find a vowel, look ahead to decide where to split
# if is_vowel(word[i]):
# # Check next characters
# remaining = word[i+1:]
# # Rule 1: V-V (e.g., 'ائ') -> Split after first vowel
# if len(remaining) >= 1 and is_vowel(remaining[0]):
# syllables.append(current_syllable)
# current_syllable = ""
# # Rule 2: V-C-V (e.g., 'ba-ra') -> Split after first vowel
# elif len(remaining) >= 2 and not is_vowel(remaining[0]) and is_vowel(remaining[1]):
# syllables.append(current_syllable)
# current_syllable = ""
# # Rule 3: V-C-C-V (e.g., 'mek-tep') -> Split after first consonant
# elif len(remaining) >= 3 and not is_vowel(remaining[0]) and not is_vowel(remaining[1]) and is_vowel(remaining[2]):
# current_syllable += remaining[0]
# syllables.append(current_syllable)
# current_syllable = ""
# i += 1 # Skip the consonant we just added
# # Rule 4: End of word or C-C-C clusters (rare in native words)
# # We keep going until we find a clear boundary or end of word
# i += 1
# if current_syllable:
# syllables.append(current_syllable)
# return syllables
# # Clean text and process word by word
# words = text.split()
# all_syllables = []
# for w in words:
# all_syllables.extend(split_word(w))
# #merge ئ to next syllable. single ئ no any semantic meaning and no it's dedicated sound.
# original_ayllables = [*all_syllables]
# all_syllables.clear()
# handled = True
# for s in original_ayllables:
# if s == "ئ":
# handled = False
# continue
# if not handled:
# s = "ئ" + s
# handled = True
# all_syllables.append(s)
# return all_syllables
# test_words = ['تەكلىپتەۋسىيەلىرىگە', 'تەكلىپ-تەۋسىيەلىرىگە']
# for tw in test_words:
# print(f"{tw} -> {uighur_syllabize(tw.strip())}")
# تەكلىپتەۋسىيەلىرىگە -> ['تەك', 'لىپ', 'تەۋ', 'سى', 'يە', 'لى', 'رى', 'گە']
# تەكلىپ-تەۋسىيەلىرىگە -> ['تەك', 'لىپ-تەۋ', 'سى', 'يە', 'لى', 'رى', 'گە']
def uighur_syllabize(text: str):
# Uyghur Vowels (ASU)
vowels = "ئاەئەوۆئۇئۈئىئېاەوۆۇۈىې"
def is_vowel(char):
return char in vowels
def split_word(word):
syllables = []
i = 0
last_split = 0
while i < len(word):
# Look for vowel patterns to determine boundaries
if is_vowel(word[i]):
rem = word[i+1:]
# Rule: V-V -> Split after first vowel
if len(rem) >= 1 and is_vowel(rem[0]):
syllables.append(word[last_split:i+1])
last_split = i + 1
# Rule: V-C-V -> Split after vowel (e.g., ba-ra)
elif len(rem) >= 2 and not is_vowel(rem[0]) and is_vowel(rem[1]):
syllables.append(word[last_split:i+1])
last_split = i + 1
# Rule: V-C-C-V -> Split between consonants (e.g., mek-tep)
elif len(rem) >= 3 and not is_vowel(rem[0]) and not is_vowel(rem[1]) and is_vowel(rem[2]):
syllables.append(word[last_split:i+2])
last_split = i + 2
i += 1 # Skip first C
# Rule: V-C-C-C-V (VCCCV) -> Split after second consonant (e.g., gert-mek)
# This fixes "gertmek", "eytqan", "partlap", "dostlar"
elif len(rem) >= 4 and not is_vowel(rem[0]) and not is_vowel(rem[1]) and not is_vowel(rem[2]) and is_vowel(rem[3]):
syllables.append(word[last_split:i+3])
last_split = i + 3
i += 2 # Skip first two Cs
i += 1
# Add the remaining part of the word
if last_split < len(word):
syllables.append(word[last_split:])
return syllables
# Process word by word
words = text.split()
final_output = []
for w in words:
raw_syllables = split_word(w)
# Handle Hemze (ئ) re-merging
processed = []
skip_next = False
for j in range(len(raw_syllables)):
if skip_next:
skip_next = False
continue
s = raw_syllables[j]
# If the current syllable is JUST "ئ" or ends in "ئ", merge it with next
if s == "ئ" and j + 1 < len(raw_syllables):
processed.append("ئ" + raw_syllables[j+1])
skip_next = True
else:
processed.append(s)
final_output.extend(processed)
return final_output
# Verification
# test_words = ["گەرتمەك", "ئېيتقان", "دوستلار", "مەكتەپكە", ' كومپېيۇتېر تورى زادى قانداق نەرسىدۇ؟ ']
# for tw in test_words:
# print(f"{tw} -> {uighur_syllabize(tw.strip())}")
# test_words = ['تەكلىپتەۋسىيەلىرىگە', 'تەكلىپ-تەۋسىيەلىرىگە']
# for tw in test_words:
# print(f"{tw} -> {uighur_syllabize(tw.strip())}")
# تەكلىپتەۋسىيەلىرىگە -> ['تەك', 'لىپ', 'تەۋ', 'سى', 'يە', 'لى', 'رى', 'گە']
# تەكلىپ-تەۋسىيەلىرىگە -> ['تەك', 'لىپ-', 'تەۋ', 'سى', 'يە', 'لى', 'رى', 'گە']
text = " ھەر قانداق ئىشتا كىشىلەر بىلەن كېڭىشىش كېرەك. لېكىن كۆڭۈل تارتقان ئىشنى قىلىۋېرىش كېرەك."
text = "\" 15 يىل بۇرۇن مەن بىلەن سەي چۇڭشىن (ئالى بابانىڭ قۇرغۇچىسىدىن بىرى) ئامېرىكىغا بېرىپ 30 نەچچە مەبلەغ سالغۇچى شىركەتلەر بىلەن كۆرۈشكىنىمىزدە ئۇلار بىزنى رەت قىلىپ ئىشىك سىرتىدا قالدۇرغاندى. ھېچكىم بىزگە ۋە كەلگۈسىمىزگە ئىشەنمىگەن ئىدى، ھېچكىممۇ بىزنىڭ ھازىر 300 مىليارد سودىنى تاماملىيالايدىغانلىقىمىزنى ئويلاپ خىيالىغىمۇ كەلتۈرمىگەن ئىدى.\" ئالى بابانىنىڭ ئورگىنى تەرىپىدىن ئىشلەنگەن ئىگىلىك تىكلەش ھۆججەتلىك فىلىمى \" بۇ چۈش ئەمەس\" نىڭ باش قىسمىدا مۇنداق بىر داڭلىق سۆز چىقىدۇ:\" مەن تاغدىن ئۆتكەن ۋاقتىمدا تاغ ماڭا گەپ قىلمىدى، مەن دېڭىزنى كېچىپ ئۆتكىنىمدە دېڭىز ماڭا گەپ قىلمىدى.\" بۇ فىلىمنىڭ قوشۇمچە ئىسمى بولسا \" مايۈن ۋە ئۇنىڭ مەڭگۈلۈك ' ياش ئالى'سى\" بولۇپ، ھەرخىل خەتەر ۋە قىيىنلىقنى بىرمۇ بىر يەڭگەن مايۈن ۋە ئۇنىڭ ئالى بابا قوشۇنىدىكى ھەمكارلاشقۇچىلىرىنىڭ قەيسەر كەچمىشى جانلىق بايان قىلىنغان."
text = "يۆ جيەنتاۋ يېقىنقى بىر مەزگىلدە، خىزمەتداشلىرى بىلەن نۇرغۇن قىيىنچىلىققا ئۇچرىغان شوپۇرغا ياردەم قىلغانلىقىنى، شۇنداقلا يېمەكلىك ۋە سۇ يەتكۈزۈپ بەرگەنلىكىنى، ئەمما ئاياغ سوۋغا قىلىشى تۇنجى قېتىم ئىكەنلىكىنى، بۈگۈنكىسى 20 يىللىق ساقچىلىق جەريانىدا تۇنجى قېتىم شوپۇرغا ئاياغ سوۋغا قىلىشى ئىكەنلىكىنى ئېيتتى."
text = "يۆ جۇڭمىڭ مۇنداق دېدى: 2019- يىلى 12-ئايدا، مەملىكەتلىك خەلق قۇرۇلتىيى دائىمىي كومىتېتىنىڭ 44- قېتىملىق كومىتېت باشلىقلىرى يىغىنى مەملىكەتلىك خەلق قۇرۇلتىيى دائىمىي كومىتېتىنىڭ 2020-يىللىق قانۇن چىقىرىش خىزمىتى پىلانىنى پىرىنسىپ جەھەتتىن ماقۇللىدى، خىزمەت تەرتىپى بويىچە، 13-نۆۋەتلىك مەملىكەتلىك خەلق قۇرۇلتىيى 3-يىغىنىنىڭ روھى ۋە ۋەكىللەرنىڭ تەكلىپ-تەۋسىيەلىرىگە ئاساسەن پىلاننى تەڭشەش كېرەك. بۇ يىل 6-ئاينىڭ 1-كۈنى، 58-قېتىملىق كومىتېت باشلىقلىرى يىغىنى تەڭشەلگەندىن كېيىنكى يىللىق قانۇن چىقىرىش خىزمىتى پىلانىنى قاراپ چىقىپ ماقۇللىدى."
text = "مەن مەكتەپكە باردىم"
# text = normalize_extended_uyghur_characters(text=text)
# print(clean_uyghur(text=text))
def process_text(text: str) -> list[str]:
text = clean_input_text(text=text)
result = []
current_word_chars = []
for char in text:
if char in UYGHUR_LETTERS:
current_word_chars.append(char)
else:
# 遇到非字母字符,先处理缓存的单词
if current_word_chars:
word = ''.join(current_word_chars)
result.extend(uighur_syllabize(word))
current_word_chars.clear()
# 非字母字符直接加入
result.append(char)
# 处理末尾的单词
if current_word_chars:
word = ''.join(current_word_chars)
result.extend(uighur_syllabize(word))
return result
+126
View File
@@ -0,0 +1,126 @@
UYGHUR_LETTERS = {
'ا', 'ە', 'ب', 'پ', 'ت', 'ج', 'چ', 'خ', 'د', 'ر', 'ز', 'ژ', 'س', 'ش', 'غ', 'ف',
'ق', 'ك', 'گ', 'ڭ', 'ل', 'م', 'ن', 'ھ', 'و', 'ۇ', 'ۆ', 'ۈ', 'ۋ', 'ې', 'ى', 'ي',
"ئ"
}
uyghur_symbols = {
"ھەرىپلەر": [
'ا', 'ە', 'ب', 'پ', 'ت', 'ج', 'چ', 'خ', 'د', 'ر', 'ز', 'ژ', 'س', 'ش', 'غ', 'ف',
'ق', 'ك', 'گ', 'ڭ', 'ل', 'م', 'ن', 'ھ', 'و', 'ۇ', 'ۆ', 'ۈ', 'ۋ', 'ې', 'ى', 'ي',
"ئ"
],
"تىنىش_بەلگىلىرى": [
"۔", "،", "؟", "!", "-", "«", "»", "؛", ":", "'", "\"", "]", "[", " ", "", ""
],
"سانلار": [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
],
}
english_symbols = {
"characters": [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
],
"symbols": [
".", "!", "?", "-", "<", ">", "=", "+", "*", "/", "|", "\\", "~", "_", "^", "@", "{", "}", "[", "]", "(", ")", "#", "%", "$", "", "£", "¥", " "
]
}
# Export all symbols:
symbols_list = uyghur_symbols["ھەرىپلەر"] + uyghur_symbols["تىنىش_بەلگىلىرى"] + uyghur_symbols["سانلار"] + english_symbols["characters"] + english_symbols["symbols"]
symbols = {i: i for i in symbols_list}
def normalize_extended_uyghur_characters(text: str) -> str:
characters = [
#["standard", "individual", "beginning", "middle", "end"]
["ئ","","","",""],
["ا","","","",""],
["ە","","","",""],
["ب","","","",""],
["پ","","","",""],
["ت","","","",""],
["ج","","","",""],
["چ","","","",""],
["خ","","","",""],
["د","","","",""],
["ر","","","",""],
["ز","","","",""],
["ژ","","","",""],
["س","","","",""],
["ش","","","",""],
["غ","","","",""],
["ف","","","",""],
["ق","","","",""],
["ك","","","",""],
["گ","","","",""],
["ڭ","","","",""],
["ل","","","",""],
["م","","","",""],
["ن","","","",""],
["ھ","","","",""],
["و","","","",""],
["ۇ","","","",""],
["ۆ","","","",""],
["ۈ","","","",""],
["ۋ","","","",""],
["ې","","","",""],
["ى","","","",""],
["ي","","","",""],
["ۅ","","","",""],
["ۉ","","","",""],
["ح","","","",""],
["ع","","","",""]
]
replacement_table: dict[str, str] = {}
#Create a replacement table.
for char_map in characters:
for char in char_map[1:]:
replacement_table[char] = char_map[0]
replacement_table[''] = "لا" #add some additional exceptional symbols
text = text.replace('ئ', 'ئ')
clean = ""
#Replace the extended characters with their standard unicode ones.
for char in text:
if char in replacement_table:
char = replacement_table[char]
clean += char
return clean
def is_uyghur_char(char: str) -> bool:
"""判断是否是维吾尔语字母"""
return all(c in UYGHUR_LETTERS for c in char)
def is_uyghur_text(text: str) -> bool:
"""清洗后检查是否全为维吾尔语"""
return all(c in symbols for c in text)
def clean_unknown_symbols(text: str) -> str:
return ''.join(c for c in text if c in symbols)
import re
def collapse_spaces(text: str) -> str:
return re.sub(r'\s+', ' ', text)
def clean_english_text(text: str) -> str:
return re.sub(r'[a-zA-Z]', ' ', text)
def clean_chinese_text(text: str) -> str:
return re.sub(r'[\u4e00-\u9fff]', ' ', text)
def clean_http_links(text: str) -> str:
return re.sub(r'https?://[^\s]+', ' ', text)
def clean_input_text(text: str) -> str:
text = collapse_spaces(text)
text = clean_http_links(text)
text = normalize_extended_uyghur_characters(text)
text = clean_unknown_symbols(text)
return text