#!/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") @dataclass 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 @dataclass 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(" size: res.critical_errors.append("Thiếu hoặc hỏng chunk 'fmt '") res.is_critical = True return res fmt_size = struct.unpack(" size: res.critical_errors.append("Không tìm thấy chunk 'data'") res.is_critical = True return res header_data_size = struct.unpack(" 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(" 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()