File size: 6,412 Bytes
7df3ebb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# -*- coding: utf-8 -*-

import copy
from typing import List
from typing import Optional

import numpy as np
import torch
from transformers.audio_utils import spectrogram
from transformers.audio_utils import window_function
from transformers.models.whisper.feature_extraction_whisper import WhisperFeatureExtractor


class MiniCPMAAudioProcessor(WhisperFeatureExtractor):
    """
    在 WhisperFeatureExtractor 基础上:
    - 支持 dynamic_log_norm(原版 max-8dB,可调 dynamic_range_db)
    - 或固定对数下限 log_floor_db(例如 -10dB)
        - 这是由于我们需要做 streaming 方案,在这个方案下,没办法进行动态设置
        - 这个 可以在中途修改,通过 set_dynamic_log_norm
    两条路径(torch / numpy)保持一致的裁剪与缩放顺序:
        log10 -> (动态/固定下限裁剪) -> (+4)/4
    """

    def __init__(
        self,
        *args,
        dynamic_log_norm: bool = True,
        dynamic_range_db: float = 8.0,
        log_floor_db: float = -10.0,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.dynamic_log_norm = bool(dynamic_log_norm)
        self.dynamic_range_db = float(dynamic_range_db)
        self.log_floor_db = float(log_floor_db)

    def set_spac_log_norm(
        self,
        dynamic_range_db: Optional[float] = None,
        log_floor_db: Optional[float] = None,
        *,
        inplace: bool = True,
    ) -> "MiniCPMAAudioProcessor":
        """
        热更新动态/固定下限策略。

        Args:
            enabled: True=使用动态阈值(max - dynamic_range_db),False=使用固定下限 log_floor_db。
                    传 None 表示保持不变。
            dynamic_range_db: 动态范围(dB),仅在 enabled=True 时生效。None 表示保持不变。
            log_floor_db: 固定对数下限(dB,通常<=0),仅在 enabled=False 时使用。None 表示保持不变。
            inplace: True 直接修改当前实例;False 返回一个浅拷贝并在其上修改。

        Returns:
            self 或新的实例(当 inplace=False 时)。
        """

        target = self if inplace else copy.copy(self)

        if dynamic_range_db is not None:
            val = float(dynamic_range_db)
            if val < 0:
                raise ValueError("dynamic_range_db must be >= 0.")
            target.dynamic_log_norm = True  # 给出显式提示:设置了该值就走动态模式
            target.dynamic_range_db = val

        if log_floor_db is not None:
            val = float(log_floor_db)
            # 一般 log10(mel) 最大不超过 ~0dB,floor 设为<=0更合理;这里做宽松校验
            if val > 0:
                raise ValueError("log_floor_db should be <= 0 (log10 scale).")
            target.dynamic_log_norm = False  # 设置了该值就走固定下限模式
            target.log_floor_db = val

        return target

    # ------------ NumPy 实现:覆写 ----------
    def _np_extract_fbank_features(self, waveform_batch: np.ndarray, device: str) -> np.ndarray:
        """
        与上游一致的 NumPy 版本,但将 max-8dB 替换为可配置的 动态/固定 下限裁剪。
        """
        if device != "cpu":
            raise ValueError(
                f"Got device `{device}` for feature extraction, but feature extraction on CUDA accelerator "
                "devices requires torch. Set device='cpu' or install torch."
            )

        log_spec_batch: List[np.ndarray] = []
        for waveform in waveform_batch:
            # 生成 log10 Mel
            log_spec = spectrogram(
                waveform,
                window_function(self.n_fft, "hann"),
                frame_length=self.n_fft,
                hop_length=self.hop_length,
                power=2.0,
                dither=self.dither,
                mel_filters=self.mel_filters,
                log_mel="log10",
            )
            # 与上游保持一致:去掉最后一帧
            log_spec = log_spec[:, :-1]

            # 动态/固定裁剪
            if self.dynamic_log_norm:
                threshold = log_spec.max() - self.dynamic_range_db
                log_spec = np.maximum(log_spec, threshold)
            else:
                log_spec = np.maximum(log_spec, self.log_floor_db)

            # 与 Whisper 一致的线性缩放
            log_spec = (log_spec + 4.0) / 4.0

            log_spec_batch.append(log_spec)

        return np.array(log_spec_batch)

    # ------------ PyTorch 实现:覆写 ----------
    def _torch_extract_fbank_features(self, waveform: np.ndarray, device: str = "cpu") -> np.ndarray:
        if torch is None:
            raise RuntimeError("PyTorch 未安装,无法在 GPU 上计算 STFT。")

        waveform = torch.from_numpy(waveform).to(device, torch.float32)
        window = torch.hann_window(self.n_fft, device=device)

        if self.dither != 0.0:
            waveform = waveform + self.dither * torch.randn_like(waveform)

        stft = torch.stft(waveform, n_fft=self.n_fft, hop_length=self.hop_length, window=window, return_complex=True)
        magnitudes = stft[..., :-1].abs() ** 2

        mel_filters = torch.from_numpy(self.mel_filters).to(device, torch.float32)  # [n_mels, 1+n_fft//2]
        mel_spec = mel_filters.T @ magnitudes  # [..., n_mels, T]

        log_spec = torch.clamp(mel_spec, min=1e-10).log10()  # ≤ 0

        if self.dynamic_log_norm:
            if waveform.dim() == 2:
                max_val_t = log_spec.max(dim=2, keepdim=True)[0]  # over T
                max_val_bt = max_val_t.max(dim=1, keepdim=True)[0]  # over mel
                threshold = max_val_bt - self.dynamic_range_db
                log_spec = torch.maximum(log_spec, threshold)
            else:
                threshold = log_spec.max() - self.dynamic_range_db
                log_spec = torch.maximum(log_spec, threshold)
        else:
            floor_tensor = torch.tensor(self.log_floor_db, dtype=log_spec.dtype, device=log_spec.device)
            log_spec = torch.maximum(log_spec, floor_tensor)

        log_spec = (log_spec + 4.0) / 4.0

        if device != "cpu":
            log_spec = log_spec.detach().cpu()
        return log_spec.numpy()

    def process(self, *args, **kwargs):
        """Alias of __call__ for convenience."""
        return self.__call__(*args, **kwargs)