File size: 4,374 Bytes
cb82b68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""
Audio Preprocessing Pipeline
Tối ưu hóa chất lượng audio trước khi feed vào Wav2Vec2.

Bao gồm:
    1. Noise reduction (spectral gating)
    2. VAD trimming (cắt silence đầu/cuối)
    3. Volume normalization
"""
import io
import numpy as np
import librosa
import soundfile as sf

# Optional: noisereduce
try:
    import noisereduce as nr
    _NR_AVAILABLE = True
except ImportError:
    _NR_AVAILABLE = False
    print("WARNING: noisereduce not installed. Audio preprocessing will skip noise reduction.")


def preprocess_audio(
    audio_path: str,
    target_sr: int = 16000,
    noise_reduce: bool = True,
    vad_trim: bool = True,
    normalize: bool = True,
    noise_sample_duration: float = 0.25,
    vad_top_db: int = 38,
) -> np.ndarray:
    """
    Full preprocessing pipeline cho file audio.

    Args:
        audio_path: Đường dẫn file audio gốc.
        target_sr: Sample rate mục tiêu (Wav2Vec2 cần 16kHz).
        noise_reduce: Áp dụng spectral gating noise reduction.
        vad_trim: Cắt silence đầu/cuối bằng energy-based VAD.
        normalize: Normalize volume về [-1, 1].
        noise_sample_duration: Số giây đầu tiên dùng làm noise profile.
        vad_top_db: Ngưỡng dB cho VAD trimming (cao = aggressive hơn).

    Returns:
        numpy array audio đã xử lý ở target_sr.
    """
    # 1. Load audio
    y, sr = librosa.load(audio_path, sr=target_sr, mono=True)

    if len(y) == 0:
        return y

    # 2. Noise Reduction (spectral gating)
    # Chỉ khử nhiễu khi đoạn đầu thực sự là khoảng lặng (tránh coi tiếng nói là nhiễu)
    if noise_reduce and _NR_AVAILABLE and len(y) > int(noise_sample_duration * target_sr):
        _head = y[: int(noise_sample_duration * target_sr)]
        _rms_head = float(np.sqrt(np.mean(_head ** 2)))
        _rms_all = float(np.sqrt(np.mean(y ** 2))) or 1e-9
        if _rms_head / _rms_all > 0.35:
            noise_reduce = False
    if noise_reduce and _NR_AVAILABLE and len(y) > int(noise_sample_duration * target_sr):
        try:
            # Lấy noise profile từ đoạn đầu (thường là silence trước khi nói)
            noise_sample_len = int(noise_sample_duration * target_sr)
            noise_clip = y[:noise_sample_len]
            y = nr.reduce_noise(
                y=y,
                y_noise=noise_clip,
                sr=target_sr,
                prop_decrease=0.5,       # giảm nhẹ để không xoá âm cuối yếu (ð, θ, s)
                n_fft=2048,
                win_length=2048,
                hop_length=512,
            )
        except Exception as e:
            print(f"Noise reduction failed (skipping): {e}")

    # 3. VAD Trimming — cắt silence đầu/cuối
    if vad_trim and len(y) > 0:
        try:
            y_trimmed, _ = librosa.effects.trim(
                y,
                top_db=vad_top_db,
                frame_length=2048,
                hop_length=512,
            )
            # Chỉ dùng nếu kết quả còn > 0.3 giây (tránh cắt hết)
            if len(y_trimmed) > int(0.3 * target_sr):
                # Chừa đệm 150ms hai đầu để không cắt phụ âm yếu
                _pad = int(0.15 * target_sr)
                _start = max(0, int(_[0]) - _pad)
                _end = min(len(y), int(_[1]) + _pad)
                y_trimmed = y[_start:_end]
                y = y_trimmed
        except Exception as e:
            print(f"VAD trimming failed (skipping): {e}")

    # 4. Volume Normalization
    if normalize and len(y) > 0:
        peak = np.max(np.abs(y))
        if peak > 0:
            y = y / peak * 0.95  # Normalize to 95% peak to avoid clipping

    # 5. Đệm 200ms silence hai đầu: CTC hay bỏ sót âm sát biên
    if len(y) > 0:
        _sil = np.zeros(int(0.2 * target_sr), dtype=y.dtype)
        y = np.concatenate([_sil, y, _sil])

    return y


def preprocess_and_save(
    audio_path: str,
    output_path: str | None = None,
    **kwargs,
) -> str:
    """
    Preprocess audio và lưu ra file mới.
    Nếu output_path=None, ghi đè file gốc.

    Returns:
        Path tới file đã xử lý.
    """
    y = preprocess_audio(audio_path, **kwargs)
    out = output_path or audio_path
    sf.write(out, y, 16000, subtype="PCM_16")
    return out