Download test.py from bonelag/ccvoice: direct link, hf CLI and curl.
- Browser
- Download file 32.5 kB
-
https://huggingface.co/bonelag/ccvoice/resolve/main/test.py
- Command line
-
hf download hf://bonelag/ccvoice/test.py
-
curl -L -o test.py https://huggingface.co/bonelag/ccvoice/resolve/main/test.py
32.5 kB
| #!/usr/bin/env python3 | |
| """ | |
| Enterprise-Grade Audio Dataset Quality Audit & Verification Suite for TTS | |
| ========================================================================== | |
| Được thiết kế chuyên biệt cho việc kiểm tra chất lượng dữ liệu huấn luyện TTS | |
| (VITS, FastSpeech 2, Piper, Matcha-TTS, StyleTTS 2...). Đảm bảo 0% rủi ro lỗi âm thanh | |
| trước khi đưa vào đào tạo mô hình. | |
| Các tầng kiểm tra chất lượng: | |
| 1. Container & Cấu trúc file (Hardware/Framework Specs): | |
| - Chuẩn RIFF / WAVE container | |
| - Audio format: 1 (PCM tuyến tính, không nén) | |
| - Kênh âm thanh: Mono (1 channel) | |
| - Tần số lấy mẫu: 24,000 Hz (hoặc chỉ định qua --sr) | |
| - Độ sâu bit: 16-bit signed integer PCM | |
| - Kiểm tra toàn vẹn chunk (fmt, data) & bắt lỗi lệch kích thước streaming CapCut | |
| 2. Chất lượng Tín hiệu & m học (Acoustic & Signal Integrity): | |
| - Phát hiện âm thanh chết / câm hoàn toàn (Dead silence: Peak < 50 hoặc RMS < -65 dBFS) | |
| - Phát hiện vỡ âm / xén biên độ (Severe digital clipping: samples >= 32767) | |
| - Phát hiện lệch một chiều DC Offset (|DC Offset| > 1%) gây méo lọc mel 0Hz | |
| - Phát hiện thời lượng bất thường: Quá ngắn (< 0.3s) hoặc quá dài (> 15.0s gây OOM GPU) | |
| 3. Tính Nhất quán Văn bản & m thanh (Text-Audio Alignment): | |
| - Đối chiếu 1-to-1 giữa file audio và metadata.csv (tìm file thiếu, file thừa) | |
| - Phát hiện text rỗng hoặc chứa ký tự điều khiển lạ | |
| - Phân tích tốc độ đọc Words-Per-Second (WPS): | |
| * WPS > 8.5: Cảnh báo file bị cắt cụt (audio ngắn bất thường so với text) | |
| * WPS < 0.6: Cảnh báo audio bị kéo dài khoảng lặng bất thường | |
| 4. Tự động hóa & Khắc phục: | |
| - Tùy chọn --fix: Sửa lỗi header CapCut tức thì (lossless, không chạm payload PCM) | |
| - Tùy chọn --sync-metadata: Đồng bộ metadata.csv vào các thư mục con còn thiếu | |
| - Xuất báo cáo JSON / Log chi tiết và danh sách file lỗi | |
| - Exit code chuẩn CI/CD: 0 = PASS, 1 = Có lỗi CRITICAL (chặn pipeline train) | |
| """ | |
| import argparse | |
| from concurrent.futures import ThreadPoolExecutor | |
| from dataclasses import asdict, dataclass, field | |
| import json | |
| import math | |
| import os | |
| from pathlib import Path | |
| import struct | |
| import sys | |
| import time | |
| from typing import Any, Dict, List, Optional, Set, Tuple | |
| import numpy as np | |
| # Đảm bảo in UTF-8 không lỗi trên Windows | |
| if hasattr(sys.stdout, "reconfigure"): | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| if hasattr(sys.stderr, "reconfigure"): | |
| sys.stderr.reconfigure(encoding="utf-8") | |
| class AuditConfig: | |
| target_sr: int = 24000 | |
| target_channels: int = 1 | |
| target_bit_depth: int = 16 | |
| min_duration: float = 0.30 # Giây (dưới mức này dễ gây sụp aligner / STFT frame) | |
| max_duration: float = 15.0 # Giây (trên mức này dễ gây CUDA OOM khi train theo batch) | |
| min_peak_amplitude: int = 50 # Biên độ đỉnh tối thiểu (trên thang 32767) | |
| min_rms_dbfs: float = -65.0 # Ngưỡng RMS dBFS tối thiểu | |
| max_clipped_samples: int = 20 # Số mẫu đạt biên tối đa chấp nhận được | |
| max_dc_offset_ratio: float = 0.01 # Độ lệch DC tối đa (1% full scale) | |
| min_wps: float = 0.6 # Tốc độ đọc tối thiểu (từ/giây) cho câu > 2 từ | |
| max_wps: float = 8.5 # Tốc độ đọc tối đa (từ/giây) | |
| workers: int = 16 # Số luồng quét song song | |
| class FileAuditResult: | |
| path: str | |
| filename: str | |
| stem: str | |
| size_bytes: int = 0 | |
| valid: bool = False | |
| is_critical: bool = False | |
| is_warning: bool = False | |
| critical_errors: List[str] = field(default_factory=list) | |
| warnings: List[str] = field(default_factory=list) | |
| can_fix_header: bool = False | |
| sample_rate: int = 0 | |
| channels: int = 0 | |
| bit_depth: int = 0 | |
| duration: float = 0.0 | |
| n_samples: int = 0 | |
| peak_amp: int = 0 | |
| rms_dbfs: float = -100.0 | |
| clipped_count: int = 0 | |
| dc_offset_ratio: float = 0.0 | |
| wps: Optional[float] = None | |
| text: str = "" | |
| def find_master_metadata(base_dir: Path) -> Optional[Path]: | |
| """Tìm file metadata.csv chuẩn làm mốc đối chiếu toàn hệ thống.""" | |
| candidates = [ | |
| base_dir / "metadata.csv", | |
| base_dir.parent / "capi" / "metadata.csv", | |
| Path("E:/VieTTS/capi/metadata.csv"), | |
| base_dir / "ReviewNew" / "metadata.csv", | |
| ] | |
| for sub in base_dir.iterdir(): | |
| if sub.is_dir(): | |
| candidates.append(sub / "metadata.csv") | |
| for cand in candidates: | |
| if cand.is_file() and cand.stat().st_size > 0: | |
| return cand.resolve() | |
| return None | |
| def load_metadata_map(meta_path: Path) -> Tuple[Dict[str, str], List[str]]: | |
| """Đọc mapping stem -> text từ metadata.csv.""" | |
| text_map: Dict[str, str] = {} | |
| ordered_stems: List[str] = [] | |
| with open(meta_path, "r", encoding="utf-8") as f: | |
| for line_num, line in enumerate(f, 1): | |
| line = line.strip() | |
| if not line or line.startswith("#"): | |
| continue | |
| parts = line.split("|") | |
| if len(parts) >= 2: | |
| file_rel = parts[0].strip() | |
| text = parts[1].strip() | |
| stem = Path(file_rel).stem | |
| text_map[stem] = text | |
| ordered_stems.append(stem) | |
| return text_map, ordered_stems | |
| def audit_single_wav(file_path: Path, expected_text: Optional[str], cfg: AuditConfig) -> FileAuditResult: | |
| """ | |
| Kiểm tra chuyên sâu 1 file WAV theo tiêu chuẩn doanh nghiệp. | |
| Sử dụng giải mã trực tiếp raw PCM + NumPy vectorization cho tốc độ cực cao. | |
| """ | |
| res = FileAuditResult( | |
| path=str(file_path), | |
| filename=file_path.name, | |
| stem=file_path.stem, | |
| text=expected_text or "", | |
| ) | |
| if not file_path.is_file(): | |
| res.critical_errors.append("File không tồn tại trên ổ đĩa") | |
| res.is_critical = True | |
| return res | |
| size = file_path.stat().st_size | |
| res.size_bytes = size | |
| if size < 44: | |
| res.critical_errors.append(f"File quá nhỏ hoặc rỗng ({size} bytes)") | |
| res.is_critical = True | |
| return res | |
| try: | |
| raw = file_path.read_bytes() | |
| except Exception as e: | |
| res.critical_errors.append(f"Không thể đọc file: {e}") | |
| res.is_critical = True | |
| return res | |
| # 1. Kiểm tra RIFF container | |
| if raw[:4] != b"RIFF" or raw[8:12] != b"WAVE": | |
| res.critical_errors.append("Không phải định dạng RIFF/WAVE hợp lệ") | |
| res.is_critical = True | |
| return res | |
| riff_size = struct.unpack("<I", raw[4:8])[0] | |
| expected_riff = size - 8 | |
| # 2. Tìm và phân tích chunk 'fmt ' | |
| fmt_idx = raw.find(b"fmt ") | |
| if fmt_idx == -1 or fmt_idx + 24 > size: | |
| res.critical_errors.append("Thiếu hoặc hỏng chunk 'fmt '") | |
| res.is_critical = True | |
| return res | |
| fmt_size = struct.unpack("<I", raw[fmt_idx + 4 : fmt_idx + 8])[0] | |
| if fmt_size < 16: | |
| res.critical_errors.append(f"fmt chunk quá ngắn ({fmt_size} bytes)") | |
| res.is_critical = True | |
| return res | |
| audio_format, channels, sample_rate, byte_rate, block_align, bits_per_sample = struct.unpack( | |
| "<HHIIHH", raw[fmt_idx + 8 : fmt_idx + 24] | |
| ) | |
| res.sample_rate = sample_rate | |
| res.channels = channels | |
| res.bit_depth = bits_per_sample | |
| if audio_format != 1: | |
| res.critical_errors.append(f"Định dạng âm thanh không phải PCM chuẩn (Format Code: {audio_format})") | |
| res.is_critical = True | |
| if channels != cfg.target_channels: | |
| res.critical_errors.append(f"Sai số kênh: {channels} kênh (Yêu cầu: {cfg.target_channels} Mono)") | |
| res.is_critical = True | |
| if sample_rate != cfg.target_sr: | |
| res.critical_errors.append(f"Sai tần số lấy mẫu: {sample_rate} Hz (Yêu cầu: {cfg.target_sr} Hz)") | |
| res.is_critical = True | |
| if bits_per_sample != cfg.target_bit_depth: | |
| res.critical_errors.append(f"Sai độ sâu mẫu: {bits_per_sample}-bit (Yêu cầu: {cfg.target_bit_depth}-bit)") | |
| res.is_critical = True | |
| # 3. Tìm và phân tích chunk 'data' | |
| data_idx = raw.find(b"data") | |
| if data_idx == -1 or data_idx + 8 > size: | |
| res.critical_errors.append("Không tìm thấy chunk 'data'") | |
| res.is_critical = True | |
| return res | |
| header_data_size = struct.unpack("<I", raw[data_idx + 4 : data_idx + 8])[0] | |
| pcm_offset = data_idx + 8 | |
| actual_data_size = size - pcm_offset | |
| # Kiểm tra sai lệch kích thước header do lỗi streaming CapCut | |
| has_header_mismatch = (riff_size != expected_riff) or (header_data_size != actual_data_size) | |
| if has_header_mismatch: | |
| res.can_fix_header = True | |
| res.warnings.append( | |
| f"Lệch kích thước Header CapCut (Header: {header_data_size} bytes, File thực tế: {actual_data_size} bytes)" | |
| ) | |
| res.is_warning = True | |
| # 4. Trích xuất và phân tích sóng âm PCM | |
| pcm_bytes = raw[pcm_offset:] | |
| if len(pcm_bytes) % 2 != 0: | |
| # Nếu lẻ byte, bỏ byte cuối cùng | |
| pcm_bytes = pcm_bytes[: len(pcm_bytes) - (len(pcm_bytes) % 2)] | |
| if len(pcm_bytes) == 0: | |
| res.critical_errors.append("Dữ liệu PCM rỗng (0 bytes âm thanh)") | |
| res.is_critical = True | |
| return res | |
| pcm = np.frombuffer(pcm_bytes, dtype=np.int16) | |
| res.n_samples = len(pcm) | |
| duration = len(pcm) / sample_rate if sample_rate > 0 else 0.0 | |
| res.duration = duration | |
| # 5. Kiểm tra thời lượng | |
| if duration < cfg.min_duration: | |
| res.warnings.append(f"Thời lượng quá ngắn: {duration:.3f}s (< {cfg.min_duration}s)") | |
| res.is_warning = True | |
| elif duration > cfg.max_duration: | |
| res.warnings.append(f"Thời lượng quá dài: {duration:.3f}s (> {cfg.max_duration}s - dễ gây OOM GPU)") | |
| res.is_warning = True | |
| # 6. Kiểm tra tín hiệu âm học (Acoustic analysis) | |
| peak_val = int(np.abs(pcm).max()) if len(pcm) > 0 else 0 | |
| res.peak_amp = peak_val | |
| # Dead silence (im lặng hoàn toàn) | |
| if peak_val < cfg.min_peak_amplitude: | |
| res.critical_errors.append(f"m thanh chết / Câm hoàn toàn (Peak: {peak_val} < {cfg.min_peak_amplitude})") | |
| res.is_critical = True | |
| # RMS energy (dBFS) | |
| rms_val = float(np.sqrt(np.mean(pcm.astype(np.float64) ** 2))) if len(pcm) > 0 else 0.0 | |
| if rms_val > 0.0: | |
| res.rms_dbfs = float(20.0 * math.log10(rms_val / 32768.0)) | |
| else: | |
| res.rms_dbfs = -100.0 | |
| if res.rms_dbfs < cfg.min_rms_dbfs: | |
| res.critical_errors.append(f"Năng lượng âm thanh quá yếu (RMS: {res.rms_dbfs:.1f} dBFS < {cfg.min_rms_dbfs} dBFS)") | |
| res.is_critical = True | |
| # Digital Clipping (xén biên độ đỉnh 16-bit: ±32767) | |
| clip_count = int((np.abs(pcm) >= 32767).sum()) | |
| res.clipped_count = clip_count | |
| if clip_count > cfg.max_clipped_samples: | |
| res.warnings.append(f"Xén biên độ đỉnh (Clipping): {clip_count} mẫu chạm ngưỡng 32767") | |
| res.is_warning = True | |
| # DC Offset | |
| mean_val = float(np.mean(pcm)) if len(pcm) > 0 else 0.0 | |
| dc_ratio = abs(mean_val) / 32768.0 | |
| res.dc_offset_ratio = dc_ratio | |
| if dc_ratio > cfg.max_dc_offset_ratio: | |
| res.warnings.append(f"Lệch DC Offset: {dc_ratio * 100:.2f}% (> {cfg.max_dc_offset_ratio * 100}%)") | |
| res.is_warning = True | |
| # 7. Đối chiếu tốc độ đọc (Words Per Second) nếu có text | |
| if expected_text is not None and duration > 0.0: | |
| clean_text = expected_text.strip() | |
| if not clean_text: | |
| res.critical_errors.append("Văn bản trong metadata bị rỗng hoàn toàn") | |
| res.is_critical = True | |
| else: | |
| words = clean_text.split() | |
| word_count = len(words) | |
| wps = word_count / duration | |
| res.wps = round(wps, 2) | |
| if wps > cfg.max_wps: | |
| res.warnings.append(f"Tốc độ đọc bất thường (Nhanh): {wps:.2f} từ/s (> {cfg.max_wps}) - Nguy cơ bị cắt audio") | |
| res.is_warning = True | |
| elif wps < cfg.min_wps and word_count >= 3: | |
| res.warnings.append(f"Tốc độ đọc bất thường (Chậm): {wps:.2f} từ/s (< {cfg.min_wps}) - Nguy cơ khoảng lặng thừa") | |
| res.is_warning = True | |
| # Tổng kết tính hợp lệ | |
| res.valid = (len(res.critical_errors) == 0) and not res.can_fix_header | |
| return res | |
| def fix_wav_header(file_path: Path) -> bool: | |
| """Sửa lỗi header WAV bị sai kích thước do streaming (lossless in-place).""" | |
| try: | |
| raw = bytearray(file_path.read_bytes()) | |
| size = len(raw) | |
| if size < 44 or raw[:4] != b"RIFF" or raw[8:12] != b"WAVE": | |
| return False | |
| data_idx = raw.find(b"data") | |
| if data_idx == -1 or data_idx + 8 > size: | |
| return False | |
| actual_riff = size - 8 | |
| actual_data = size - (data_idx + 8) | |
| struct.pack_into("<I", raw, 4, actual_riff) | |
| struct.pack_into("<I", raw, data_idx + 4, actual_data) | |
| file_path.write_bytes(raw) | |
| return True | |
| except Exception: | |
| return False | |
| def audit_dataset_dir( | |
| dir_path: Path, | |
| text_map: Dict[str, str], | |
| expected_stems: List[str], | |
| cfg: AuditConfig, | |
| ) -> Dict[str, Any]: | |
| """Kiểm tra toàn diện 1 thư mục dataset voice bằng đa luồng song song.""" | |
| wav_files = sorted(dir_path.glob("*.wav")) | |
| file_map = {f.stem: f for f in wav_files} | |
| expected_set = set(expected_stems) | |
| # 1. Đối chiếu số lượng | |
| missing_stems = [stem for stem in expected_stems if stem not in file_map] | |
| extra_files = [f.name for f in wav_files if f.stem not in expected_set] | |
| # 2. Chạy đa luồng kiểm tra file WAV | |
| results: List[FileAuditResult] = [] | |
| tasks = [(f, text_map.get(f.stem)) for f in wav_files] | |
| with ThreadPoolExecutor(max_workers=cfg.workers) as executor: | |
| futures = [executor.submit(audit_single_wav, path, text, cfg) for path, text in tasks] | |
| for fut in futures: | |
| results.append(fut.result()) | |
| # 3. Phân loại kết quả | |
| critical_files: List[FileAuditResult] = [] | |
| warning_files: List[FileAuditResult] = [] | |
| bad_header_files: List[FileAuditResult] = [] | |
| valid_count = 0 | |
| total_duration = 0.0 | |
| sample_rates: Set[int] = set() | |
| channels_set: Set[int] = set() | |
| bit_depths_set: Set[int] = set() | |
| durations: List[float] = [] | |
| wps_list: List[float] = [] | |
| silence_count = 0 | |
| clipping_count = 0 | |
| dc_offset_count = 0 | |
| short_count = 0 | |
| long_count = 0 | |
| wps_outlier_count = 0 | |
| for r in results: | |
| durations.append(r.duration) | |
| total_duration += r.duration | |
| if r.sample_rate: | |
| sample_rates.add(r.sample_rate) | |
| if r.channels: | |
| channels_set.add(r.channels) | |
| if r.bit_depth: | |
| bit_depths_set.add(r.bit_depth) | |
| if r.wps is not None: | |
| wps_list.append(r.wps) | |
| if r.is_critical: | |
| critical_files.append(r) | |
| if r.can_fix_header: | |
| bad_header_files.append(r) | |
| if r.is_warning and not r.is_critical: | |
| warning_files.append(r) | |
| if r.valid: | |
| valid_count += 1 | |
| # Đếm chi tiết các loại cảnh báo | |
| for err in r.critical_errors: | |
| if "Câm hoàn toàn" in err or "năng lượng âm thanh quá yếu" in err.lower(): | |
| silence_count += 1 | |
| for w in r.warnings: | |
| if "Clipping" in w: | |
| clipping_count += 1 | |
| elif "DC Offset" in w: | |
| dc_offset_count += 1 | |
| elif "quá ngắn" in w: | |
| short_count += 1 | |
| elif "quá dài" in w: | |
| long_count += 1 | |
| elif "Tốc độ đọc" in w: | |
| wps_outlier_count += 1 | |
| # Đánh giá trạng thái | |
| if missing_stems or critical_files: | |
| status = "FAIL (Lỗi Nghiêm Trọng)" | |
| elif bad_header_files: | |
| status = "WARN (Sai Header)" | |
| elif warning_files: | |
| status = "WARN (Cảnh Báo Tín Hiệu)" | |
| else: | |
| status = "PASS (Hoàn Hảo 100%)" | |
| dur_arr = np.array(durations) if durations else np.array([0.0]) | |
| wps_arr = np.array(wps_list) if wps_list else np.array([0.0]) | |
| return { | |
| "name": dir_path.name, | |
| "path": dir_path, | |
| "total_files": len(wav_files), | |
| "expected_count": len(expected_stems), | |
| "valid_count": valid_count, | |
| "missing_count": len(missing_stems), | |
| "missing_samples": missing_stems[:5], | |
| "all_missing": missing_stems, | |
| "extra_count": len(extra_files), | |
| "critical_count": len(critical_files), | |
| "critical_files": critical_files, | |
| "warning_count": len(warning_files), | |
| "warning_files": warning_files, | |
| "bad_header_count": len(bad_header_files), | |
| "bad_header_files": bad_header_files, | |
| "silence_count": silence_count, | |
| "clipping_count": clipping_count, | |
| "dc_offset_count": dc_offset_count, | |
| "short_count": short_count, | |
| "long_count": long_count, | |
| "wps_outlier_count": wps_outlier_count, | |
| "total_duration": total_duration, | |
| "duration_min": float(dur_arr.min()), | |
| "duration_max": float(dur_arr.max()), | |
| "duration_mean": float(dur_arr.mean()), | |
| "duration_p50": float(np.percentile(dur_arr, 50)), | |
| "duration_p95": float(np.percentile(dur_arr, 95)), | |
| "wps_mean": float(wps_arr.mean()) if len(wps_arr) else 0.0, | |
| "sample_rates": sorted(list(sample_rates)), | |
| "channels": sorted(list(channels_set)), | |
| "bit_depths": sorted(list(bit_depths_set)), | |
| "status": status, | |
| "has_metadata": (dir_path / "metadata.csv").is_file(), | |
| } | |
| def format_duration(seconds: float) -> str: | |
| """Đổi số giây thành chuỗi HH:MM:SS.""" | |
| h = int(seconds // 3600) | |
| m = int((seconds % 3600) // 60) | |
| s = int(seconds % 60) | |
| return f"{h:02d}h {m:02d}m {s:02d}s" | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Enterprise-Grade Dataset Verification Suite for TTS (Kiểm tra chất lượng dữ liệu huấn luyện TTS)", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| ) | |
| parser.add_argument( | |
| "--dir", | |
| "-d", | |
| default=".", | |
| help="Thư mục chứa các dataset voice (Mặc định: thư mục hiện tại)", | |
| ) | |
| parser.add_argument( | |
| "--metadata", | |
| "-m", | |
| default=None, | |
| help="Đường dẫn file metadata.csv chuẩn (Mặc định: tự tìm trong thư mục)", | |
| ) | |
| parser.add_argument( | |
| "--sr", | |
| type=int, | |
| default=24000, | |
| help="Tần số lấy mẫu mục tiêu bắt buộc (Mặc định: 24000 Hz)", | |
| ) | |
| parser.add_argument( | |
| "--min-dur", | |
| type=float, | |
| default=0.30, | |
| help="Thời lượng tối thiểu cho 1 file audio (Mặc định: 0.30s)", | |
| ) | |
| parser.add_argument( | |
| "--max-dur", | |
| type=float, | |
| default=15.0, | |
| help="Thời lượng tối đa cho 1 file audio tránh OOM (Mặc định: 15.0s)", | |
| ) | |
| parser.add_argument( | |
| "--workers", | |
| "-w", | |
| type=int, | |
| default=16, | |
| help="Số luồng CPU quét song song (Mặc định: 16)", | |
| ) | |
| parser.add_argument( | |
| "--fix", | |
| action="store_true", | |
| help="Tự động sửa lỗi header WAV bị sai lệch kích thước streaming (lossless in-place)", | |
| ) | |
| parser.add_argument( | |
| "--sync-metadata", | |
| action="store_true", | |
| help="Tự động sao chép metadata.csv vào các thư mục dataset còn thiếu", | |
| ) | |
| parser.add_argument( | |
| "--json-report", | |
| default=None, | |
| help="Đường dẫn xuất file báo cáo JSON chi tiết cho CI/CD", | |
| ) | |
| parser.add_argument( | |
| "--log-report", | |
| default=None, | |
| help="Đường dẫn xuất file log ghi chi tiết danh sách tất cả file có vấn đề", | |
| ) | |
| parser.add_argument( | |
| "--bad-list", | |
| default=None, | |
| help="Đường dẫn xuất file text chứa danh sách đường dẫn các file lỗi (để tiện lọc bỏ hoặc gen lại)", | |
| ) | |
| args = parser.parse_args() | |
| cfg = AuditConfig( | |
| target_sr=args.sr, | |
| min_duration=args.min_dur, | |
| max_duration=args.max_dur, | |
| workers=args.workers, | |
| ) | |
| base_dir = Path(args.dir).resolve() | |
| if not base_dir.exists(): | |
| print(f"[LỖI CRITICAL] Thư mục '{base_dir}' không tồn tại!") | |
| sys.exit(1) | |
| # 1. Tìm metadata chuẩn | |
| meta_path = Path(args.metadata).resolve() if args.metadata else find_master_metadata(base_dir) | |
| if not meta_path or not meta_path.is_file(): | |
| print(f"[LỖI CRITICAL] Không tìm thấy metadata.csv chuẩn để đối chiếu!") | |
| sys.exit(1) | |
| text_map, expected_stems = load_metadata_map(meta_path) | |
| total_expected = len(expected_stems) | |
| # 2. Tìm danh sách thư mục dataset | |
| dataset_dirs: List[Path] = [] | |
| if list(base_dir.glob("*.wav")): | |
| dataset_dirs = [base_dir] | |
| else: | |
| for child in sorted(base_dir.iterdir()): | |
| if child.is_dir() and not child.name.startswith("."): | |
| # Chỉ nhận thư mục có chứa ít nhất 1 file .wav | |
| if any(child.glob("*.wav")): | |
| dataset_dirs.append(child) | |
| if not dataset_dirs: | |
| print(f"[THÔNG BÁO] Không tìm thấy file WAV nào trong '{base_dir}'.") | |
| sys.exit(0) | |
| print("=" * 118) | |
| print("KIỂM TRA TOÀN DIỆN CHẤT LƯỢNG DATASET TTS (ENTERPRISE-GRADE VERIFICATION SUITE)") | |
| print("=" * 118) | |
| print(f"• Thư mục quét : {base_dir}") | |
| print(f"• File metadata : {meta_path} ({total_expected:,} câu chuẩn)") | |
| print(f"• Số dataset voice : {len(dataset_dirs)} thư mục | Số luồng quét: {cfg.workers}") | |
| print(f"• Tiêu chuẩn mẫu : {cfg.target_sr} Hz | Mono (1 ch) | 16-bit PCM | Dur: [{cfg.min_duration}s - {cfg.max_duration}s]") | |
| print("-" * 118) | |
| t0 = time.time() | |
| results: List[Dict[str, Any]] = [] | |
| for d in dataset_dirs: | |
| t_d0 = time.time() | |
| res = audit_dataset_dir(d, text_map, expected_stems, cfg) | |
| results.append(res) | |
| scan_elapsed = time.time() - t0 | |
| # 3. In bảng tổng kết | |
| header_fmt = "{:<16} | {:>6} | {:>6} | {:>6} | {:>10} | {:>13} | {:>10} | {:<22}" | |
| print(header_fmt.format("Tên Dataset", "Tổng", "Thiếu", "Lỗi Cực", "Sai Header", "Thời lượng", "Định dạng", "Trạng thái")) | |
| print("-" * 118) | |
| grand_total = 0 | |
| grand_missing = 0 | |
| grand_critical = 0 | |
| grand_bad_headers = 0 | |
| grand_duration = 0.0 | |
| all_bad_file_paths: List[str] = [] | |
| for r in results: | |
| grand_total += r["total_files"] | |
| grand_missing += r["missing_count"] | |
| grand_critical += r["critical_count"] | |
| grand_bad_headers += r["bad_header_count"] | |
| grand_duration += r["total_duration"] | |
| sr_str = ",".join(str(s) for s in r["sample_rates"]) if r["sample_rates"] else "-" | |
| sr_fmt = f"{sr_str}Hz" if sr_str != "-" else "N/A" | |
| print(header_fmt.format( | |
| r["name"][:16], | |
| f"{r['total_files']:,}", | |
| f"{r['missing_count']:,}", | |
| f"{r['critical_count']:,}", | |
| f"{r['bad_header_count']:,}", | |
| format_duration(r["total_duration"]), | |
| sr_fmt, | |
| r["status"], | |
| )) | |
| for cf in r["critical_files"]: | |
| all_bad_file_paths.append(cf.path) | |
| print("-" * 118) | |
| print(f"TỔNG CỘNG: {len(results)} dataset | {grand_total:,} file audio | {grand_duration/3600:.2f} giờ âm thanh ({format_duration(grand_duration)})") | |
| print(f"Quét hoàn tất trong {scan_elapsed:.2f}s ({grand_total/scan_elapsed:,.0f} files/s)") | |
| print(f"Chỉ số lỗi: {grand_critical} lỗi CRITICAL | {grand_missing} file thiếu | {grand_bad_headers} file sai header") | |
| print("=" * 118) | |
| # 4. In bảng phân tích tín hiệu âm học sâu (Signal & Acoustic Metrics) | |
| print("\nBẢNG PHÂN TÍCH CHỈ SỐ TÍN HIỆU ÂM HỌC & ĐẶC TRƯNG TTS:") | |
| print("-" * 118) | |
| sig_fmt = "{:<16} | {:>8} | {:>8} | {:>8} | {:>8} | {:>8} | {:>8} | {:>10} | {:>8}" | |
| print(sig_fmt.format("Tên Dataset", "Câm (<50)", "Clip(>20)", "DC(>1%)", "< 0.30s", "> 15.0s", "WPS Dị", "Min/Max Dur", "Avg WPS")) | |
| print("-" * 118) | |
| for r in results: | |
| print(sig_fmt.format( | |
| r["name"][:16], | |
| r["silence_count"], | |
| r["clipping_count"], | |
| r["dc_offset_count"], | |
| r["short_count"], | |
| r["long_count"], | |
| r["wps_outlier_count"], | |
| f"{r['duration_min']:.2f}/{r['duration_max']:.1f}s", | |
| f"{r['wps_mean']:.2f} wps", | |
| )) | |
| print("-" * 118) | |
| # 5. Cảnh báo chi tiết | |
| has_critical = any(r["missing_count"] > 0 or r["critical_count"] > 0 for r in results) | |
| has_warnings = any(r["warning_count"] > 0 or r["bad_header_count"] > 0 for r in results) | |
| if has_critical or has_warnings: | |
| print("\nCHI TIẾT VẤN ĐỀ PHÁT HIỆN:") | |
| for r in results: | |
| if r["missing_count"] > 0: | |
| samples = ", ".join(r["missing_samples"]) | |
| print(f" [!] [{r['name']}] THIẾU {r['missing_count']} file so với metadata! (Ví dụ: {samples})") | |
| if r["critical_count"] > 0: | |
| print(f" [CRITICAL] [{r['name']}] Có {r['critical_count']} file lỗi nghiêm trọng:") | |
| for cf in r["critical_files"][:5]: | |
| print(f" - {cf.filename}: {'; '.join(cf.critical_errors)}") | |
| if r["bad_header_count"] > 0: | |
| print(f" [WARN] [{r['name']}] Có {r['bad_header_count']} file bị CapCut ghi sai kích thước Header (PCM nguyên vẹn, sửa được bằng --fix).") | |
| if r["short_count"] > 0: | |
| print(f" [WARN] [{r['name']}] Có {r['short_count']} file ngắn hơn {cfg.min_duration}s (Ví dụ các từ đơn ngắn: 'Ấp.', 'Át.').") | |
| if r["clipping_count"] > 0: | |
| print(f" [WARN] [{r['name']}] Có {r['clipping_count']} file bị xén biên độ đỉnh (>20 mẫu chạm 32767).") | |
| # 6. Tự động sửa header nếu có cờ --fix | |
| if args.fix and grand_bad_headers > 0: | |
| print("\n--- BẮT ĐẦU TỰ ĐỘNG SỬA HEADER WAV (LOSSLESS) ---") | |
| fixed_count = 0 | |
| for r in results: | |
| if r["bad_header_count"] > 0: | |
| print(f"Đang sửa header {r['bad_header_count']} file trong '{r['name']}'...", end=" ", flush=True) | |
| ds_fixed = 0 | |
| for item in r["bad_header_files"]: | |
| fpath = Path(item.path) | |
| if fix_wav_header(fpath): | |
| ds_fixed += 1 | |
| fixed_count += ds_fixed | |
| print(f"Xong ({ds_fixed}/{r['bad_header_count']} file).") | |
| print(f"\n[Hoàn tất] Đã sửa thành công {fixed_count} file. Hãy chạy lại test.py để kiểm tra lại!") | |
| # 7. Đồng bộ metadata.csv nếu có cờ --sync-metadata | |
| if args.sync_metadata: | |
| meta_content = meta_path.read_bytes() | |
| synced = 0 | |
| for r in results: | |
| target_meta = Path(r["path"]) / "metadata.csv" | |
| if not target_meta.exists(): | |
| target_meta.write_bytes(meta_content) | |
| synced += 1 | |
| print(f"Đã sao chép metadata.csv vào '{r['name']}'.") | |
| if synced > 0: | |
| print(f"[Đồng bộ] Đã sao chép metadata.csv vào {synced} thư mục dataset.") | |
| # 8. Xuất báo cáo JSON / Log / Bad List | |
| if args.json_report: | |
| report_path = Path(args.json_report).resolve() | |
| json_data = { | |
| "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), | |
| "base_dir": str(base_dir), | |
| "metadata_file": str(meta_path), | |
| "grand_total_files": grand_total, | |
| "grand_duration_hours": round(grand_duration / 3600, 3), | |
| "grand_critical_errors": grand_critical, | |
| "grand_bad_headers": grand_bad_headers, | |
| "scan_elapsed_seconds": round(scan_elapsed, 2), | |
| "datasets": [ | |
| { | |
| "name": r["name"], | |
| "total_files": r["total_files"], | |
| "valid_count": r["valid_count"], | |
| "missing_count": r["missing_count"], | |
| "critical_count": r["critical_count"], | |
| "warning_count": r["warning_count"], | |
| "bad_header_count": r["bad_header_count"], | |
| "silence_count": r["silence_count"], | |
| "clipping_count": r["clipping_count"], | |
| "dc_offset_count": r["dc_offset_count"], | |
| "short_count": r["short_count"], | |
| "long_count": r["long_count"], | |
| "wps_outlier_count": r["wps_outlier_count"], | |
| "duration_seconds": round(r["total_duration"], 2), | |
| "duration_stats": { | |
| "min": round(r["duration_min"], 3), | |
| "max": round(r["duration_max"], 3), | |
| "mean": round(r["duration_mean"], 3), | |
| "p50": round(r["duration_p50"], 3), | |
| "p95": round(r["duration_p95"], 3), | |
| }, | |
| "status": r["status"], | |
| "critical_files": [asdict(cf) for cf in r["critical_files"]], | |
| } | |
| for r in results | |
| ], | |
| } | |
| report_path.write_text(json.dumps(json_data, ensure_ascii=False, indent=2), encoding="utf-8") | |
| print(f"\n[Báo cáo JSON] Đã xuất file: {report_path}") | |
| if args.log_report: | |
| log_path = Path(args.log_report).resolve() | |
| with open(log_path, "w", encoding="utf-8") as lf: | |
| lf.write(f"=== BÁO CÁO CHI TIẾT LỖI DATASET TTS ({time.strftime('%Y-%m-%d %H:%M:%S')}) ===\n\n") | |
| for r in results: | |
| lf.write(f"[{r['name']}] Trạng thái: {r['status']}\n") | |
| if r["missing_count"]: | |
| lf.write(f" * Thiếu {r['missing_count']} file:\n") | |
| for m in r["all_missing"]: | |
| lf.write(f" - {m}.wav\n") | |
| if r["critical_count"]: | |
| lf.write(f" * {r['critical_count']} file lỗi CRITICAL:\n") | |
| for cf in r["critical_files"]: | |
| lf.write(f" - {cf.filename}: {'; '.join(cf.critical_errors)}\n") | |
| if r["warning_count"]: | |
| lf.write(f" * {r['warning_count']} file có cảnh báo WARNING:\n") | |
| for wf in r["warning_files"]: | |
| lf.write(f" - {wf.filename}: {'; '.join(wf.warnings)}\n") | |
| lf.write("\n") | |
| print(f"[Báo cáo Log] Đã xuất file: {log_path}") | |
| if args.bad_list: | |
| bad_path = Path(args.bad_list).resolve() | |
| with open(bad_path, "w", encoding="utf-8") as bf: | |
| for p in all_bad_file_paths: | |
| bf.write(f"{p}\n") | |
| print(f"[Danh sách File Lỗi] Đã xuất {len(all_bad_file_paths)} đường dẫn vào: {bad_path}") | |
| if not args.fix and grand_bad_headers > 0: | |
| print("\n[MẸO] Để tự động sửa sạch 100% các file bị sai header, chạy lệnh:") | |
| print(f" python test.py --fix") | |
| # CI/CD Fail-Safe Exit Code: nếu có bất kỳ lỗi CRITICAL nào, thoát với code 1 | |
| if has_critical: | |
| print("\n[KẾT LUẬN] PHÁT HIỆN LỖI CRITICAL TRONG DATASET! CHẶN TIẾN TRÌNH TRAIN.") | |
| sys.exit(1) | |
| else: | |
| print("\n[KẾT LUẬN] TOÀN BỘ DATASET ĐẠT TIÊU CHUẨN KỸ THUẬT DOANH NGHIỆP CHO HUẤN LUYỆN TTS.") | |
| sys.exit(0) | |
| if __name__ == "__main__": | |
| main() | |