Skip to main content

flexaudio_vad/
segmenter.rs

1//! セグメント化状態機械(ONNX 非依存)。
2//!
3//! silero-VAD 原典 `utils_vad.py::get_speech_timestamps` の判定ロジックを再現する。
4//! frame ごとの発話確率を逐次食わせると、min_speech / min_silence / max_speech / pad /
5//! neg_threshold を適用したセグメント(開始/終了サンプル位置)を確定順に返す。
6//!
7//! streaming ([`crate::Vad::process`]) と batch ([`crate::get_speech_timestamps`]) が
8//! この同じロジックを使う。サンプル位置は frame index でなく絶対サンプル位置(累積)。
9
10use crate::config::VadConfig;
11
12/// 確定した発話セグメント(パディング適用後・絶対サンプル位置)。
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct Segment {
15    /// 発話開始サンプル位置(パディング適用後)。
16    pub start_sample: u64,
17    /// 発話終了サンプル位置(パディング適用後・排他的。この位置の手前まで)。
18    pub end_sample: u64,
19}
20
21impl Segment {
22    /// 開始位置を ms に変換(指定サンプルレート基準)。
23    pub fn start_ms(&self, sample_rate: u32) -> u64 {
24        (self.start_sample * 1000) / u64::from(sample_rate.max(1))
25    }
26
27    /// 終了位置を ms に変換(指定サンプルレート基準)。
28    pub fn end_ms(&self, sample_rate: u32) -> u64 {
29        (self.end_sample * 1000) / u64::from(sample_rate.max(1))
30    }
31
32    /// セグメント長(サンプル数)。
33    pub fn len_samples(&self) -> u64 {
34        self.end_sample.saturating_sub(self.start_sample)
35    }
36}
37
38/// セグメント化状態機械。
39///
40/// frame index ではなくフレーム末尾の絶対サンプル位置で進む。1 frame が
41/// `frame_size` サンプル (16k=512) で、`feed` ごとに位置が `frame_size` 進む。
42#[derive(Debug, Clone)]
43pub struct Segmenter {
44    threshold: f32,
45    neg_threshold: f32,
46    min_speech_samples: u64,
47    min_silence_samples: u64,
48    speech_pad_samples: u64,
49    max_speech_samples: u64, // 0 = 無制限
50    frame_size: u64,
51
52    /// 発話中フラグ。
53    triggered: bool,
54    /// 現在の発話の開始サンプル位置(生・未パディング)。
55    current_start: u64,
56    /// 無音が始まった位置。0 = 無音区間なし(silero と同じ番兵)。
57    temp_end: u64,
58    /// 次に feed するフレーム末尾の絶対サンプル位置(これまで feed したサンプル総数)。
59    next_pos: u64,
60    /// 直前に確定(pad 適用後)したセグメント終端。pad 重なりクランプ用。0 = 未確定。
61    prev_end: u64,
62}
63
64impl Segmenter {
65    /// 設定からセグメンタを構築する。
66    pub fn new(config: &VadConfig) -> Self {
67        Segmenter {
68            threshold: config.threshold,
69            neg_threshold: config.resolved_neg_threshold(),
70            min_speech_samples: config.ms_to_samples(config.min_speech_ms),
71            min_silence_samples: config.ms_to_samples(config.min_silence_ms),
72            speech_pad_samples: config.ms_to_samples(config.speech_pad_ms),
73            max_speech_samples: config.ms_to_samples(config.max_speech_ms),
74            frame_size: config.frame_size() as u64,
75            triggered: false,
76            current_start: 0,
77            temp_end: 0,
78            next_pos: 0,
79            prev_end: 0,
80        }
81    }
82
83    /// 状態を初期化する。
84    pub fn reset(&mut self) {
85        self.triggered = false;
86        self.current_start = 0;
87        self.temp_end = 0;
88        self.next_pos = 0;
89        self.prev_end = 0;
90    }
91
92    /// 1 フレーム分の発話確率を食わせ、確定したセグメントがあれば返す。
93    ///
94    /// `prob` はそのフレーム [next_pos, next_pos + frame_size) の発話確率。内部位置は
95    /// `frame_size` 進む。silero `get_speech_timestamps` のループ本体に対応する。
96    pub fn feed(&mut self, prob: f32, out: &mut Vec<Segment>) {
97        // このフレームが占める区間 [frame_start, frame_end)。
98        let frame_start = self.next_pos;
99        let frame_end = frame_start + self.frame_size;
100        self.next_pos = frame_end;
101
102        // 発話あり (prob >= threshold)。
103        if prob >= self.threshold {
104            // 無音カウンタをリセット(silero: temp_end = 0)。
105            if self.temp_end != 0 {
106                self.temp_end = 0;
107            }
108            if !self.triggered {
109                self.triggered = true;
110                // silero は frame index * window。ここでは frame 先頭の絶対サンプル位置。
111                self.current_start = frame_start;
112            }
113            // triggered 中は silero と同じく毎フレーム max_speech を見る。
114            self.check_max_speech(frame_end, out);
115            return;
116        }
117
118        // 無音側 (prob < neg_threshold) かつ発話中。
119        if prob < self.neg_threshold && self.triggered {
120            if self.temp_end == 0 {
121                self.temp_end = frame_start;
122            }
123            // 無音が min_silence に達したら発話終了を確定。終端は無音開始位置 (temp_end)。
124            if frame_start.saturating_sub(self.temp_end) >= self.min_silence_samples {
125                self.finalize_segment(self.current_start, self.temp_end, out);
126                self.triggered = false;
127                self.temp_end = 0;
128                return;
129            }
130            // まだ min_silence 未満。発話継続のまま max_speech 評価へ落ちる。
131        }
132        // threshold > prob >= neg_threshold のグレーゾーンは silero と同じく状態を
133        // 変えず継続する(triggered のまま無音カウントも始めない)。
134
135        // triggered 継続中の無音/グレーフレームでの max_speech 強制分割。
136        self.check_max_speech(frame_end, out);
137    }
138
139    /// triggered 中に発話長が max_speech を超えていれば強制分割する。
140    ///
141    /// silero: 直近の無音 (temp_end) があればそこで切って続行、無ければ現フレーム末尾で切る。
142    fn check_max_speech(&mut self, frame_end: u64, out: &mut Vec<Segment>) {
143        if !self.triggered || self.max_speech_samples == 0 {
144            return;
145        }
146        let speech_len = frame_end.saturating_sub(self.current_start);
147        if speech_len <= self.max_speech_samples {
148            return;
149        }
150        if self.temp_end != 0 {
151            // 直近の無音位置で分割し、その位置から次発話を継続。
152            let split = self.temp_end;
153            self.finalize_segment(self.current_start, split, out);
154            self.current_start = split;
155            self.temp_end = 0;
156        } else {
157            // 無音が無いまま長すぎ → 現フレーム末尾で分割し継続。
158            self.finalize_segment(self.current_start, frame_end, out);
159            self.current_start = frame_end;
160        }
161    }
162
163    /// 入力終端に達したときに呼ぶ。発話中なら現在位置までを最終セグメントとして確定する。
164    /// silero は末尾の未確定発話を `audio_length` まで採用する。
165    pub fn flush(&mut self, out: &mut Vec<Segment>) {
166        if self.triggered {
167            self.finalize_segment(self.current_start, self.next_pos, out);
168            self.triggered = false;
169            self.temp_end = 0;
170        }
171    }
172
173    /// 生(未パディング)の発話区間を min_speech で篩い、pad を適用して `out` に積む。
174    fn finalize_segment(&mut self, raw_start: u64, raw_end: u64, out: &mut Vec<Segment>) {
175        // min_speech 未満は捨てる(silero: (end - start) < min_speech_samples)。
176        if raw_end.saturating_sub(raw_start) < self.min_speech_samples {
177            return;
178        }
179
180        // speech_pad: 開始を手前へ、終了を後ろへ広げる。
181        let mut start = raw_start.saturating_sub(self.speech_pad_samples);
182        let end = raw_end + self.speech_pad_samples;
183
184        // 直前セグメントと pad が重ならないようクランプする。silero は隣接セグメント間の
185        // 隙間を pad*2 と比べて中点で割るが、ここでは prev_end を侵さないよう開始を
186        // クランプする(等価で保守的)。
187        if self.prev_end != 0 && start < self.prev_end {
188            start = self.prev_end;
189        }
190
191        self.prev_end = end;
192        out.push(Segment {
193            start_sample: start,
194            end_sample: end,
195        });
196    }
197}