phamhapa101 commited on
Commit
c1b005c
·
verified ·
1 Parent(s): e634f16

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -67
app.py CHANGED
@@ -1,5 +1,5 @@
1
  #!/usr/bin/env python3
2
- # app.py - Modified for Piper Dataset Creation (Auto-split < 10s)
3
 
4
  import logging
5
  import os
@@ -12,9 +12,9 @@ from pathlib import Path
12
  import gradio as gr
13
  import torch
14
  import torchaudio
15
- from pydub import AudioSegment, silence # Thêm thư viện xử lý audio
16
 
17
- # Import từ file model của bạn
18
  from examples import examples
19
  from model import (
20
  decode,
@@ -31,60 +31,92 @@ def MyPrint(s):
31
  date_time = now.strftime("%Y-%m-%d %H:%M:%S")
32
  print(f"{date_time}: {s}")
33
 
34
- # --- CÁC HÀM XỬ LÝ AUDIO ---
35
 
36
- def split_audio_by_silence(
37
  in_filename: str,
38
  output_dir: str,
39
  base_name: str,
40
- min_silence_len=500,
41
- silence_thresh=-40,
42
- max_len_sec=10
 
43
  ):
44
  """
45
- Cắt audio thành các đoạn nhỏ dựa trên khoảng lặng.
46
- - min_silence_len: Độ dài khoảng lặng tối thiểu để cắt (ms)
47
- - silence_thresh: Ngưỡng âm thanh (dB) để coi khoảng lặng
48
- - max_len_sec: Độ dài tối đa mong muốn (giây)
 
49
  """
50
  try:
51
- # Load audio bằng pydub
52
  sound = AudioSegment.from_file(in_filename)
53
-
54
- # Chuẩn hóa về 16000Hz, 1 kênh (Mono) cho Piper/Sherpa
55
  sound = sound.set_frame_rate(16000).set_channels(1)
56
 
57
- # Cắt dựa trên khoảng lặng
58
- # keep_silence=200: giữ lại 200ms khoảng lặng ở đầu/cuối để giọng đọc tự nhiên hơn
59
- chunks = silence.split_on_silence(
 
 
 
 
 
 
 
 
 
60
  sound,
61
  min_silence_len=min_silence_len,
62
  silence_thresh=silence_thresh,
63
- keep_silence=200
64
  )
65
 
 
 
 
 
 
 
 
 
 
66
  output_files = []
 
67
 
68
- for i, chunk in enumerate(chunks):
69
- duration_sec = len(chunk) / 1000.0
 
 
 
 
 
 
70
 
71
- # Lọc các đoạn quá ngắn (dưới 0.5s) hoặc quá dài (trên max_len_sec)
72
- # Piper tốt nhất với audio 2s - 10s
73
- if duration_sec < 0.5:
 
74
  continue
75
 
76
- # Nếu đoạn quá dài, ta thể bỏ qua hoặc vẫn lưu (tùy chọn).
77
- # đây tôi cảnh báo nhưng vẫn lưu để người dùng lọc sau nếu muốn.
78
- if duration_sec > max_len_sec:
79
- MyPrint(f"⚠️ Warning: Chunk {i} dài {duration_sec}s (> {max_len_sec}s)")
 
 
 
 
 
 
80
 
81
- # Xuất file wav
82
- out_name = f"{base_name}_{i:04d}.wav"
83
  out_path = os.path.join(output_dir, out_name)
84
 
85
- # Export params: 16k, 16bit, mono
86
  chunk.export(out_path, format="wav", parameters=["-ac", "1", "-ar", "16000"])
87
  output_files.append(out_path)
 
88
 
89
  return output_files
90
 
@@ -92,7 +124,7 @@ def split_audio_by_silence(
92
  MyPrint(f"Lỗi khi cắt file {in_filename}: {e}")
93
  return []
94
 
95
- # --- HÀM XỬ LÝ HÀNG LOẠT (BATCH) ---
96
 
97
  def process_batch_files(
98
  language: str,
@@ -108,16 +140,17 @@ def process_batch_files(
108
  return None, "Vui lòng chọn ít nhất một file audio."
109
 
110
  MyPrint(f"Bắt đầu xử lý: {len(uploaded_files)} files gốc.")
111
- MyPrint(f"Model: {repo_id} | Silence Thresh: {silence_thresh}dB | Min Silence: {min_silence_len}ms")
 
 
112
 
113
- # 1. Khởi tạo thư mục tạm
114
  tmp_dir = tempfile.mkdtemp()
115
- wavs_dir = os.path.join(tmp_dir, "wavs") # Folder wavs theo chuẩn Piper
116
  os.makedirs(wavs_dir, exist_ok=True)
117
 
118
  csv_path = os.path.join(tmp_dir, "metadata.csv")
119
 
120
- # 2. Tải model Sherpa
121
  try:
122
  recognizer = get_pretrained_model(
123
  repo_id,
@@ -130,62 +163,57 @@ def process_batch_files(
130
  results_metadata = []
131
  total_chunks = 0
132
 
133
- # 3. Duyệt qua từng file upload
134
  for file_obj in progress.tqdm(uploaded_files, desc="Đang cắt & Nhận dạng..."):
135
  in_path = file_obj.name
136
  base_name = Path(in_path).stem
137
 
138
- # Bước A: Cắt file audio thành các đoạn nhỏ < 10s
139
- chunk_paths = split_audio_by_silence(
140
  in_path,
141
  wavs_dir,
142
  base_name,
143
- min_silence_len=min_silence_len,
144
- silence_thresh=silence_thresh
 
145
  )
146
 
147
- # Bước B: Nhận dạng text cho từng đoạn cắt
148
  for chunk_path in chunk_paths:
149
  try:
150
- # Sherpa nhận dạng
151
  text = decode(recognizer, chunk_path)
152
-
153
- # Làm sạch text cơ bản (trim)
154
  text = text.strip()
155
 
156
- # Chỉ lưu nếu text
157
- if len(text) > 0:
 
158
  wav_filename = os.path.basename(chunk_path)
159
-
160
- # Format chuẩn Piper: filename|transcript
161
  line = f"{wav_filename}|{text}"
162
  results_metadata.append(line)
163
  total_chunks += 1
164
  else:
165
- # Xóa file audio rỗng không text để sạch dataset
166
  os.remove(chunk_path)
167
 
168
  except Exception as e:
169
  MyPrint(f"Lỗi decode {chunk_path}: {e}")
170
 
171
- # 4. Ghi metadata.csv
172
  with open(csv_path, "w", encoding="utf-8") as f:
173
  for line in results_metadata:
174
  f.write(line + "\n")
175
 
176
- MyPrint(f"Hoàn tất! Tổng số mẫu dataset: {total_chunks}")
177
 
178
- # 5. Nén file zip
179
  zip_filename = f"piper_dataset_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
180
  zip_path = os.path.join(tempfile.gettempdir(), zip_filename)
181
-
182
  shutil.make_archive(zip_path.replace('.zip', ''), 'zip', tmp_dir)
183
 
184
  info_text = (
185
  f"✅ Xử lý hoàn tất!\n"
186
  f"- Tổng số câu (segments): {total_chunks}\n"
187
- f"- Format: Piper Dataset (wavs/ + metadata.csv)\n"
188
- f"- File đã được cắt nhỏ chuẩn hóa 16kHz Mono."
189
  )
190
 
191
  return zip_path, info_text
@@ -202,9 +230,9 @@ css = """
202
  .result {display:flex;flex-direction:column}
203
  """
204
 
205
- with gr.Blocks(css=css, title="Auto Piper Dataset Maker") as demo:
206
- gr.Markdown("# ✂️ Auto Piper Dataset Maker")
207
- gr.Markdown("Tự động cắt audio dài thành các đoạn nhỏ (<10s), nhận dạng văn bản tạo dataset chuẩn Piper.")
208
 
209
  with gr.Row():
210
  with gr.Column(scale=1):
@@ -224,31 +252,31 @@ with gr.Blocks(css=css, title="Auto Piper Dataset Maker") as demo:
224
 
225
  gr.Markdown("---")
226
  gr.Markdown("### 2. Cấu hình Cắt Audio")
 
227
  silence_thresh_slider = gr.Slider(
228
- minimum=-60, maximum=-10, value=-40, step=1,
229
  label="Ngưỡng khoảng lặng (dB)",
230
- info="Càng nhỏ càng nhạy (ví dụ -50dB nhạy hơn -30dB). Nếu file ồn hãy tăng lên (-30)."
231
  )
232
  min_silence_slider = gr.Slider(
233
- minimum=100, maximum=2000, value=500, step=100,
234
- label="Độ dài khoảng lặng tối thiểu (ms)",
235
- info="Khoảng lặng phải dài hơn số này mới cắt. Giảm xuống nếu nói quá nhanh."
236
  )
237
 
238
  with gr.Column(scale=2):
239
  gr.Markdown("### 3. Upload & Xử lý")
240
  files_input = gr.File(
241
- label="Upload Audio gốc (File dài cũng được)",
242
  file_count="multiple",
243
  type="filepath"
244
  )
245
 
246
  batch_btn = gr.Button("🚀 Cắt Audio & Tạo Dataset", variant="primary")
247
 
248
- status_output = gr.Textbox(label="Kết quả", lines=5)
249
  file_output = gr.File(label="Download Dataset (.zip)")
250
 
251
- # Ẩn các tham số nâng cao của Sherpa để giao diện gọn gàng
252
  decoding_method_state = gr.State("modified_beam_search")
253
  num_active_paths_state = gr.State(4)
254
 
 
1
  #!/usr/bin/env python3
2
+ # app.py - Modified for Piper Dataset Creation (Smart Split)
3
 
4
  import logging
5
  import os
 
12
  import gradio as gr
13
  import torch
14
  import torchaudio
15
+ from pydub import AudioSegment, silence, effects # Thêm effects để normalize
16
 
17
+ # Import từ file model của bạn (giữ nguyên cấu trúc cũ)
18
  from examples import examples
19
  from model import (
20
  decode,
 
31
  date_time = now.strftime("%Y-%m-%d %H:%M:%S")
32
  print(f"{date_time}: {s}")
33
 
34
+ # --- CÁC HÀM XỬ LÝ AUDIO THÔNG MINH ---
35
 
36
+ def smart_split_audio(
37
  in_filename: str,
38
  output_dir: str,
39
  base_name: str,
40
+ min_silence_len=500, # Độ dài khoảng lặng để xác định là "ngắt câu"
41
+ silence_thresh=-40, # Ngưỡng dB
42
+ max_len_sec=12, # Độ dài tối đa cho phép của 1 đoạn (Piper tốt nhất < 15s)
43
+ keep_silence=300 # Giữ lại bao nhiêu ms khoảng lặng ở đầu/cuối (Padding)
44
  ):
45
  """
46
+ Cắt audio thông minh:
47
+ 1. Normalize audio để ngưỡng dB hoạt động đúng.
48
+ 2. Dùng detect_nonsilent để tìm các đoạn CÓ TIẾNG.
49
+ 3. Gộp các đoạn tiếng gần nhau (cách nhau < min_silence_len) thành 1 câu.
50
+ 4. Thêm padding (keep_silence) để không bị cụt chữ.
51
  """
52
  try:
53
+ # 1. Load Normalize Audio
54
  sound = AudioSegment.from_file(in_filename)
 
 
55
  sound = sound.set_frame_rate(16000).set_channels(1)
56
 
57
+ # Normalize giúp âm lượng đồng đều, tránh việc đoạn nhỏ bị cắt nhầm
58
+ sound = effects.normalize(sound)
59
+
60
+ # Vì đã normalize, ngưỡng silence_thresh nên chỉnh lại tương đối
61
+ # Tuy nhiên để người dùng dễ chỉnh, ta dùng tham số truyền vào nhưng tính toán lại một chút
62
+ # Nếu audio đã normalize, max volume là 0dBFS. Ngưỡng cắt nên khoảng -40 đến -50.
63
+
64
+ MyPrint(f"Đang phân tích audio: {in_filename} | Độ dài: {len(sound)/1000}s")
65
+
66
+ # 2. Phát hiện các đoạn có tiếng (Non-silent chunks)
67
+ # seek_step=10: bước nhảy 10ms giúp xử lý nhanh hơn
68
+ nonsilent_ranges = silence.detect_nonsilent(
69
  sound,
70
  min_silence_len=min_silence_len,
71
  silence_thresh=silence_thresh,
72
+ seek_step=10
73
  )
74
 
75
+ if not nonsilent_ranges:
76
+ MyPrint(f"⚠️ Không tìm thấy giọng nói trong file {in_filename}. Kiểm tra lại ngưỡng dB.")
77
+ return []
78
+
79
+ # 3. Thuật toán Gộp đoạn (Merging Logic)
80
+ # Mục tiêu: Không để các từ bị rời rạc. Nếu khoảng cách giữa 2 từ < min_silence_len, gộp chúng lại.
81
+ # Ở đây detect_nonsilent đã làm việc đó dựa trên min_silence_len.
82
+ # Tuy nhiên, ta cần kiểm tra độ dài tổng để đảm bảo không quá ngắn hoặc quá dài.
83
+
84
  output_files = []
85
+ chunk_count = 0
86
 
87
+ # Xử từng khoảng thời gian tìm được
88
+ for start_i, end_i in nonsilent_ranges:
89
+
90
+ # Tính toán padding để âm thanh nghe "tròn" hơn
91
+ # Lùi điểm đầu lại một chút (start - keep_silence)
92
+ # Kéo điểm cuối ra một chút (end + keep_silence)
93
+ adj_start = max(0, start_i - keep_silence)
94
+ adj_end = min(len(sound), end_i + keep_silence)
95
 
96
+ chunk_duration = (adj_end - adj_start) / 1000.0
97
+
98
+ # Lọc rác: Bỏ qua đoạn quá ngắn (< 0.3s) - thường là tiếng click chuột hoặc noise
99
+ if chunk_duration < 0.3:
100
  continue
101
 
102
+ # Xử đoạn quá dài: Nếu dài hơn max_len_sec, Piper sẽ khó học.
103
+ # Với script đơn giản này, ta chấp nhận lưu, nhưng cảnh báo.
104
+ # (Giải pháp nâng cao là dùng VAD AI để cắt nhỏ hơn, nhưng ở đây dùng pydub cho nhẹ)
105
+ if chunk_duration > max_len_sec:
106
+ MyPrint(f"⚠️ Chunk {chunk_count} hơi dài: {chunk_duration:.1f}s (Khuyên dùng < {max_len_sec}s)")
107
+
108
+ chunk = sound[adj_start:adj_end]
109
+
110
+ # Fade in/out cực nhẹ (10ms) để tránh tiếng "bụp" ở đầu/cuối file
111
+ chunk = chunk.fade_in(10).fade_out(10)
112
 
113
+ # Xuất file
114
+ out_name = f"{base_name}_{chunk_count:04d}.wav"
115
  out_path = os.path.join(output_dir, out_name)
116
 
 
117
  chunk.export(out_path, format="wav", parameters=["-ac", "1", "-ar", "16000"])
118
  output_files.append(out_path)
119
+ chunk_count += 1
120
 
121
  return output_files
122
 
 
124
  MyPrint(f"Lỗi khi cắt file {in_filename}: {e}")
125
  return []
126
 
127
+ # --- HÀM XỬ LÝ BATCH ---
128
 
129
  def process_batch_files(
130
  language: str,
 
140
  return None, "Vui lòng chọn ít nhất một file audio."
141
 
142
  MyPrint(f"Bắt đầu xử lý: {len(uploaded_files)} files gốc.")
143
+ # Lưu ý: threshold càng âm thì càng nhạy (ví dụ -50 nhạy hơn -30).
144
+ # Nhưng vì ta đã normalize audio, nên threshold -40 đến -50 là chuẩn.
145
+ MyPrint(f"Cấu hình cắt: Thresh={silence_thresh}dB | Min Gap={min_silence_len}ms")
146
 
 
147
  tmp_dir = tempfile.mkdtemp()
148
+ wavs_dir = os.path.join(tmp_dir, "wavs")
149
  os.makedirs(wavs_dir, exist_ok=True)
150
 
151
  csv_path = os.path.join(tmp_dir, "metadata.csv")
152
 
153
+ # Load Model Sherpa
154
  try:
155
  recognizer = get_pretrained_model(
156
  repo_id,
 
163
  results_metadata = []
164
  total_chunks = 0
165
 
 
166
  for file_obj in progress.tqdm(uploaded_files, desc="Đang cắt & Nhận dạng..."):
167
  in_path = file_obj.name
168
  base_name = Path(in_path).stem
169
 
170
+ # --- SỬ DỤNG HÀM CẮT MỚI ---
171
+ chunk_paths = smart_split_audio(
172
  in_path,
173
  wavs_dir,
174
  base_name,
175
+ min_silence_len=min_silence_len, # Khoảng lặng tối thiểu để tính là hết câu
176
+ silence_thresh=silence_thresh, # Ngưỡng ồn
177
+ keep_silence=300 # Thêm 300ms đệm đầu/cuối để TRÒN CHỮ
178
  )
179
 
 
180
  for chunk_path in chunk_paths:
181
  try:
 
182
  text = decode(recognizer, chunk_path)
 
 
183
  text = text.strip()
184
 
185
+ # Logic làm sạch text
186
+ # Bỏ qua các đoạn text quá ngắn (thường là halluncination/ảo giác của AI)
187
+ if len(text) > 1:
188
  wav_filename = os.path.basename(chunk_path)
189
+ # Piper format: filename|transcript
 
190
  line = f"{wav_filename}|{text}"
191
  results_metadata.append(line)
192
  total_chunks += 1
193
  else:
194
+ # Xóa file rác (âm thanh tiếng thở, click chuột mà AI không dịch ra chữ)
195
  os.remove(chunk_path)
196
 
197
  except Exception as e:
198
  MyPrint(f"Lỗi decode {chunk_path}: {e}")
199
 
200
+ # Ghi file metadata.csv
201
  with open(csv_path, "w", encoding="utf-8") as f:
202
  for line in results_metadata:
203
  f.write(line + "\n")
204
 
205
+ MyPrint(f"Hoàn tất! Tổng số mẫu: {total_chunks}")
206
 
207
+ # Nén zip
208
  zip_filename = f"piper_dataset_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
209
  zip_path = os.path.join(tempfile.gettempdir(), zip_filename)
 
210
  shutil.make_archive(zip_path.replace('.zip', ''), 'zip', tmp_dir)
211
 
212
  info_text = (
213
  f"✅ Xử lý hoàn tất!\n"
214
  f"- Tổng số câu (segments): {total_chunks}\n"
215
+ f"- File đã được Normalize và thêm Padding (đệm) để tròn chữ.\n"
216
+ f"- Tải file .zip bên dưới để train Piper."
217
  )
218
 
219
  return zip_path, info_text
 
230
  .result {display:flex;flex-direction:column}
231
  """
232
 
233
+ with gr.Blocks(css=css, title="Auto Piper Dataset Maker (Smart Split)") as demo:
234
+ gr.Markdown("# ✂️ Auto Piper Dataset Maker (Smart Split)")
235
+ gr.Markdown("Tự động cắt audio, chuẩn hóa âm lượng thêm vùng đệm để **không bị mất chữ**.")
236
 
237
  with gr.Row():
238
  with gr.Column(scale=1):
 
252
 
253
  gr.Markdown("---")
254
  gr.Markdown("### 2. Cấu hình Cắt Audio")
255
+ gr.Markdown("*(Đã bật chế độ Normalize và Smart Padding)*")
256
  silence_thresh_slider = gr.Slider(
257
+ minimum=-60, maximum=-20, value=-45, step=1,
258
  label="Ngưỡng khoảng lặng (dB)",
259
+ info="Mặc định -45dB. Nếu file bị cắt vụn quá nhiều câu ngắn, hãy giảm xuống -50 hoặc -55."
260
  )
261
  min_silence_slider = gr.Slider(
262
+ minimum=200, maximum=2000, value=700, step=100,
263
+ label="Độ dài ngắt câu (ms)",
264
+ info="Khoảng lặng phải dài hơn số này mới được tính là hết câu. Tăng lên (800-1000) để câu dài hơn."
265
  )
266
 
267
  with gr.Column(scale=2):
268
  gr.Markdown("### 3. Upload & Xử lý")
269
  files_input = gr.File(
270
+ label="Upload Audio gốc",
271
  file_count="multiple",
272
  type="filepath"
273
  )
274
 
275
  batch_btn = gr.Button("🚀 Cắt Audio & Tạo Dataset", variant="primary")
276
 
277
+ status_output = gr.Textbox(label="Log Kết quả", lines=5)
278
  file_output = gr.File(label="Download Dataset (.zip)")
279
 
 
280
  decoding_method_state = gr.State("modified_beam_search")
281
  num_active_paths_state = gr.State(4)
282