Initial commit: FunASR Speech Recognition Toolkit
Update API Documentation / build-api-docs (push) Has been cancelled

Add complete FunASR codebase including models, runtime, and documentation.
This commit is contained in:
freedakgmail
2026-07-09 22:38:58 +08:00
commit 6116b1f3c6
3683 changed files with 990984 additions and 0 deletions
@@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""DynamicStreamingVAD — 动态阈值流式 VAD 封装。
在 fsmn-vad 基础上,根据当前语音段的累积时长动态调整静音切分阈值:
短句等待更长静音(避免切碎),长句快速切分(避免堆积)。
支持流式(逐帧喂入)和非流式(一次性处理完整音频)两种调用方式。
Usage (流式):
from funasr import AutoModel
from funasr.models.fsmn_vad_streaming.dynamic_vad import DynamicStreamingVAD
vad_model = AutoModel(model="fsmn-vad", device="cuda:0")
vad = DynamicStreamingVAD(vad_model)
for audio_chunk in audio_stream:
segments = vad.feed(audio_chunk)
for seg in segments:
print(f"Speech: {seg[0]}-{seg[1]}ms")
# 结束时
final_segments = vad.finalize()
Usage (非流式):
segments = vad.process(full_audio_tensor)
for seg in segments:
print(f"Speech: {seg[0]}-{seg[1]}ms")
"""
from typing import List, Optional, Tuple
import torch
import numpy as np
# 默认动态阈值配置:(累积时长上限ms, 静音阈值ms)
DEFAULT_SILENCE_SCHEDULE = [
(5000, 2000),
(10000, 1500),
(15000, 1000),
(30000, 800),
(45000, 400),
(float('inf'), 100),
]
class DynamicStreamingVAD:
"""动态阈值流式 VAD。
在 fsmn-vad 的流式推理基础上,根据当前语音段已累积的时长
动态调整静音切分阈值,实现「短句不切碎、长句快切分」。
Args:
vad_model: FunASR AutoModel 加载的 fsmn-vad 模型实例。
chunk_size_ms: 每次喂入 VAD 的 chunk 大小(毫秒),默认 60。
speech_noise_thres: 语音/噪声判别阈值,默认 0.5。
speech_to_sil_thres_ms: 语音转静音的基础时间(毫秒),默认 150。
silence_schedule: 动态阈值配置表,格式为
[(累积时长上限ms, 对应的静音阈值ms), ...]。
当累积时长 <= 上限时,使用对应的静音阈值。
默认值适合实时对话场景。设为 None 禁用动态调整(使用固定阈值)。
sample_rate: 采样率,默认 16000。
Example:
# 自定义阈值:更激进的切分
vad = DynamicStreamingVAD(
vad_model,
silence_schedule=[
(3000, 1500),
(8000, 800),
(15000, 400),
(float('inf'), 200),
],
)
"""
def __init__(
self,
vad_model,
chunk_size_ms: int = 60,
speech_noise_thres: float = 0.5,
speech_to_sil_thres_ms: int = 150,
silence_schedule: Optional[List[Tuple[float, int]]] = None,
sample_rate: int = 16000,
):
self.model = vad_model
self.chunk_size_ms = chunk_size_ms
self.speech_noise_thres = speech_noise_thres
self.speech_to_sil_thres_ms = speech_to_sil_thres_ms
self.silence_schedule = silence_schedule if silence_schedule is not None else DEFAULT_SILENCE_SCHEDULE
self.sample_rate = sample_rate
self.cache = {}
self.confirmed_segments: List[List[int]] = []
self.current_speech_start: Optional[int] = None
self.accumulated_since_cut_ms: int = 0
def _get_silence_threshold(self) -> int:
"""根据当前累积时长,从 schedule 中查询静音阈值。"""
for limit_ms, silence_ms in self.silence_schedule:
if self.accumulated_since_cut_ms <= limit_ms:
return silence_ms
return self.silence_schedule[-1][1]
def _apply_dynamic_threshold(self):
"""将动态阈值应用到 VAD 内部 cache。"""
if "stats" not in self.cache:
return
stats = self.cache["stats"]
stats.speech_noise_thres = self.speech_noise_thres
desired_silence_ms = self._get_silence_threshold()
stats.max_end_sil_frame_cnt_thresh = max(desired_silence_ms - self.speech_to_sil_thres_ms, 0)
def feed(self, audio_chunk: torch.Tensor, is_final: bool = False) -> List[List[int]]:
"""喂入一段音频,返回新确认的语音段。
Args:
audio_chunk: 音频数据(float32 tensor16kHz)。
可以是任意长度,内部按 chunk_size_ms 处理。
is_final: 是否为最后一段音频。设为 True 时会强制结束当前语音段。
Returns:
新确认的语音段列表,每段为 [start_ms, end_ms]。
仅在检测到语音结束时返回非空列表。
"""
if audio_chunk.dim() > 1:
audio_chunk = audio_chunk.squeeze()
chunk_samples = len(audio_chunk)
self.accumulated_since_cut_ms += int(chunk_samples * 1000 / self.sample_rate)
self._apply_dynamic_threshold()
res = self.model.generate(
input=[audio_chunk], cache=self.cache,
is_final=is_final, chunk_size=self.chunk_size_ms,
)
signals = res[0].get("value", [])
new_confirmed = []
for sig in signals:
if sig[0] >= 0 and sig[1] == -1:
self.current_speech_start = sig[0]
elif sig[0] == -1 and sig[1] >= 0:
start = self.current_speech_start if self.current_speech_start is not None else 0
seg = [start, sig[1]]
self.confirmed_segments.append(seg)
new_confirmed.append(seg)
self.current_speech_start = None
self.accumulated_since_cut_ms = 0
elif sig[0] >= 0 and sig[1] >= 0:
self.confirmed_segments.append(sig)
new_confirmed.append(sig)
self.current_speech_start = None
self.accumulated_since_cut_ms = 0
return new_confirmed
def finalize(self) -> List[List[int]]:
"""结束流式处理,返回最后可能未结束的语音段。
调用此方法后,VAD 状态会被重置。
如果当前有正在进行的语音段,会被强制结束。
Returns:
最后确认的语音段列表。
"""
# Feed empty with is_final=True to flush
empty = torch.zeros(int(self.sample_rate * 0.01), dtype=torch.float32)
return self.feed(empty, is_final=True)
def process(self, audio: torch.Tensor) -> List[List[int]]:
"""非流式接口:一次性处理完整音频,返回所有语音段。
Args:
audio: 完整音频(float32 tensor16kHz)。
Returns:
所有检测到的语音段 [[start_ms, end_ms], ...]。
"""
self.reset()
if isinstance(audio, np.ndarray):
audio = torch.from_numpy(audio).float()
if audio.dim() > 1:
audio = audio.squeeze()
# 分 chunk 喂入
chunk_samples = int(self.sample_rate * self.chunk_size_ms / 1000)
total = len(audio)
all_segments = []
for i in range(0, total, chunk_samples):
chunk = audio[i:i + chunk_samples]
is_last = (i + chunk_samples >= total)
segs = self.feed(chunk, is_final=is_last)
all_segments.extend(segs)
return all_segments
@property
def is_speaking(self) -> bool:
"""当前是否在语音状态中。"""
return self.current_speech_start is not None
@property
def current_duration_ms(self) -> int:
"""当前段已累积的时长(毫秒)。"""
return self.accumulated_since_cut_ms
@property
def current_threshold_ms(self) -> int:
"""当前使用的静音阈值(毫秒)。"""
return self._get_silence_threshold()
def reset(self):
"""重置所有状态,开始新一轮检测。"""
self.cache = {}
self.confirmed_segments = []
self.current_speech_start = None
self.accumulated_since_cut_ms = 0