ccvoice / test.py
bonelag's picture
Upload folder using huggingface_hub
94d7a4f verified
Raw History Blame Contribute Delete
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")
@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("<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()