音视频有效语音检测silero-vad

132 阅读5分钟

本文主要讲诉如何对音视频中的有效音频进行检测

VAD(语音活动检测)的核心作用是区分语音与非语音信号,核心原理是基于信号特征分析,主要应用于需要精准处理语音的场景

一、为何要进行 VAD 检测

  • 减少无效数据处理,降低系统计算量和存储成本,提升效率。
  • 避免非语音噪声干扰,提高语音相关任务的准确性(如识别、编码)。
  • 实现语音交互的触发与中断控制,优化用户体验。

二、VAD 检测的主要应用场景

  • 语音通信:通话降噪、回声消除、半双工通信切换(如对讲机模式)。
  • 语音识别 / 助手:唤醒词触发、语音指令截取、转文字时过滤静音段。
  • 音频处理:录音自动启停、音频编辑时分割语音片段、视频字幕生成预处理。
  • 安防监控:通过语音活动触发录像或报警,减少无效监控数据。

三、VAD 检测的核心原理

  • 提取信号特征:分析音频的能量、过零率、频谱特征等关键指标。
  • 设定判断阈值:基于特征建立语音与非语音的区分标准(如能量高于阈值且过零率在特定范围)。
  • 动态决策:结合时间连续性等规则,避免单帧误判,最终输出 “语音” 或 “非语音” 的判定结果

功能概述

本文业务主要是用于语音识别,音视频文件中有效语音时长大于5分钟为合法文件。detectAudioEffective 方法是 NotRealTimeValidate 类中的核心方法,用于对音视频文件进行非实时语音检测,验证音频的有效时长是否满足业务要求。该方法能够检测音频中包含有效语音的片段时长,并判断是否达到最低标准(默认100秒)。

一、准备步骤

1.安装依赖

npm install @ricky0123/vad-web onnxruntime-web
  1. ffmpeg.wasm的初始化,参考之前的一篇文章 juejin.cn/post/750487…

二、实现步骤解析

  1. 初始化阶段
const actionId = file?.name || uuidv4();
const startTime = performance.now();
this.emit(VALIDATE_MP4_EVENT.VALIDATE_START);
const sampleRate = 16000;
  • 创建唯一标识符用于日志追踪
  • 记录开始时间用于性能统计
  • 发送验证开始事件通知
  • 设置音频采样率16000Hz
  1. 元数据提取与进度控制
const metadataProgress = this.mockProgress(0, this.actionProportion.firstExtract, (progress) => {
  this.emit(VALIDATE_MP4_EVENT.VALIDATE_PROGRESS, progress);
});
metadataProgress.start();

const startTimeStr = this.secondsToTimeFormat(extractStart);
const durationStr = this.secondsToTimeFormat(extractDuration);
const tryBuffer = await this.ffmpegUtil.extractWav(file, {
  startTimeStr,
  durationStr,
  metaCallback: getMetadata,
});
  • 使用 mockProgress 创建进度控制器
  • 调用 ffmpegUtil.extractWav 提取前5分钟音频数据
  • 通过 metaCallback 回调获取文件元数据
  1. 总时长验证
const validDurationInfo = this.checkTotalDuration(currentMetaData);
if (!validDurationInfo.valid) {
  return validDurationInfo;
}

  • 调用 checkTotalDuration 验证文件总时长是否满足要求(≥5分钟)
  • 不满足则直接返回错误信息
  1. 首次语音有效性检测
const tryAudio = new Float32Array(tryBuffer);
const validEffectiveInfo = await this.detectSpeechSegments(tryAudio, sampleRate);
effectiveDuration += validEffectiveInfo.fragmentDuration;

  • 将音频数据转换为 Float32Array 格式
  • 调用 detectSpeechSegments 检测有效语音片段
  • 累计计算有效时长
  1. 循环检测后续音频片段
while (extractStart < duration) {
  const actualDuration = Math.min(extractDuration, duration - extractStart);
  const nextStartTimeStr = this.secondsToTimeFormat(extractStart);
  const nextDurationStr = this.secondsToTimeFormat(actualDuration);

  const fragmentBuffer = await this.ffmpegUtil.extractWav(file, {
    startTimeStr: nextStartTimeStr,
    durationStr: nextDurationStr,
  });
  
  const fragmentAudio = new Float32Array(fragmentBuffer);
  const fragmentEffectiveInfo = await this.detectSpeechSegments(fragmentAudio, sampleRate, effectiveDuration);
  effectiveDuration = fragmentEffectiveInfo.fragmentDuration;
  
  if (fragmentEffectiveInfo.valid) {
    break;
  }
  extractStart += actualDuration;
}

  • 循环提取后续5分钟音频片段
  • 每段都进行语音有效性检测
  • 累计有效时长,满足100秒要求时提前退出
  1. 最终结果判定
if (effectiveDuration <= MEDIA_AUDIO_EFFECTIVE_DURATION * 1000) {
  return {
    valid: false,
    errorInfo: [
      {
        errorMessage: `音频有效时长不足,当前时长(${formatDurationTime({ time: effectiveDuration, isChinese: true })})`,
        type: 'error',
      },
    ],
  };
}

return { valid: true, errorInfo: [] };

  • 判断累计有效时长是否≥100秒
  • 满足要求返回验证通过,否则返回具体错误信息

三、以上需要使用到的部分方法

  1. 提取音视频中的语音
/**
 * 提取适合silero-vad检测的音频数据 (16kHz 单声道)
 * @param {string | File | Blob} file 输入文件
 * @param {object} options 提取选项
 * @returns {Promise<ArrayBuffer>} 返回音频数据的ArrayBuffer
 */
async extractWav(file: string | File | Blob, options: ExtractAudioOptions): Promise<ArrayBuffer> {
  // 1. 加载检查
  if (!this.isLoaded) await this.load();
  
  // 2. 文件处理
  const { startTimeStr, durationStr } = options;
  const { inputName, outputName } = this.generateUniqueFileName(file, '.raw');
  await this.ffmpeg.writeFile(inputName, await fetchFile(file));
  
  // 3. 音频转换
  await this.ffmpeg.exec([
    '-i', inputName,
    '-ss', startTimeStr,     // 开始时间
    '-t', durationStr,       // 持续时间
    '-vn',                   // 移除视频流
    '-acodec', 'pcm_f32le',  // 指定音频编解码器为32位浮点格式
    '-ar', '16000',          // 设置采样率为16kHz
    '-ac', '1',              // 设置为单声道
    '-f', 'f32le',           // 输出格式为32位浮点小端格式
    outputName
  ]);
  
  // 4. 结果返回
  const data = await this.ffmpeg.readFile(outputName);
  return data.buffer;
}
  1. vad检测
/**
 * 检测音频中的有效语音片段(简化版)
 * @param float32Audio 32位浮点格式的音频数据
 * @param sampleRate 采样率
 * @returns 检测结果
 */
async function detectSpeechSegmentsDemo(float32Audio: Float32Array, sampleRate: number) {
  // 1. 初始化VAD模型
  const myvad = await NonRealTimeVAD.new({
    modelURL: '/path/to/model.onnx'
  });
  
  let effectiveDuration = 0;
  
  // 2. 运行VAD检测
  for await (const segment of myvad.run(float32Audio, sampleRate)) {
    // 3. 累计有效语音时长
    effectiveDuration += segment.end - segment.start;
    
    // 4. 达到要求时长则停止检测
    if (effectiveDuration > 100000) { // 100秒
      break;
    }
  }
  
  // 5. 返回结果
  return {
    valid: effectiveDuration > 100000,
    fragmentDuration: effectiveDuration
  };
}

  1. 非实时的检测类(改造为支持V5模型)
import * as ortInstance from 'onnxruntime-web';

import { log } from '@/utils/vad/v5/logging';

import { defaultModelFetcher } from './default-model-fetcher';
import {
  defaultFrameProcessorOptions,
  FrameProcessor,
  FrameProcessorEvent,
  FrameProcessorInterface,
  FrameProcessorOptions,
  validateOptions,
} from './frame-processor';
import { Message } from './messages';
import { ModelFetcher, OrtModule, OrtOptions, SileroV5 } from './models';
import { Resampler } from './resampler';

interface NonRealTimeVADSpeechData {
  audio: Float32Array;
  start: number;
  end: number;
}

export interface NonRealTimeVADOptions extends FrameProcessorOptions, OrtOptions {
  modelURL: string;
  modelFetcher: (path: string) => Promise<ArrayBuffer>;
}

export const defaultNonRealTimeVADOptions: NonRealTimeVADOptions = {
  ...defaultFrameProcessorOptions,
  modelURL: '',
  modelFetcher: defaultModelFetcher,
};

export class NonRealTimeVAD {
  frameSamples: number = 512;

  static async new(options: Partial<NonRealTimeVADOptions> = {}) {
    const fullOptions = {
      ...defaultNonRealTimeVADOptions,
      ...options,
    };
    validateOptions(fullOptions);

    if (fullOptions.ortConfig !== undefined) {
      fullOptions.ortConfig(ortInstance);
    }
    const modelFetcher = () => fullOptions.modelFetcher(fullOptions.modelURL);
    const model = await SileroV5.new(ortInstance, modelFetcher);

    const frameProcessor = new FrameProcessor(
      model.process,
      model.reset_state,
      {
        positiveSpeechThreshold: fullOptions.positiveSpeechThreshold,
        negativeSpeechThreshold: fullOptions.negativeSpeechThreshold,
        redemptionMs: fullOptions.redemptionMs,
        preSpeechPadMs: fullOptions.preSpeechPadMs,
        minSpeechMs: fullOptions.minSpeechMs,
        submitUserSpeechOnPause: fullOptions.submitUserSpeechOnPause,
      },
      1536 / 16,
    );
    frameProcessor.resume();

    const vad = new this(modelFetcher, ortInstance, fullOptions, frameProcessor);
    return vad;
  }

  constructor(
    public modelFetcher: ModelFetcher,
    public ort: OrtModule,
    public options: NonRealTimeVADOptions,
    public frameProcessor: FrameProcessorInterface,
  ) {
    log.debug('non-real-time-vad constructor');
  }

  async *run(inputAudio: Float32Array, sampleRate: number): AsyncGenerator<NonRealTimeVADSpeechData> {
    const resamplerOptions = {
      nativeSampleRate: sampleRate,
      targetSampleRate: 16000,
      targetFrameSize: this.frameSamples,
    };
    const resampler = new Resampler(resamplerOptions);
    let start = 0;
    let end = 0;
    let frameIndex = 0;

    for await (const frame of resampler.stream(inputAudio)) {
      const messageContainer: FrameProcessorEvent[] = [];

      await this.frameProcessor.process(frame, (event) => {
        messageContainer.push(event);
      });

      for (const event of messageContainer) {
        switch (event.msg) {
          case Message.SpeechStart:
            start = (frameIndex * this.frameSamples) / 16;
            break;

          case Message.SpeechEnd:
            end = ((frameIndex + 1) * this.frameSamples) / 16;
            yield { audio: event.audio, start, end };
            break;

          default:
            break;
        }
      }
      frameIndex++;
    }

    const messageContainer: FrameProcessorEvent[] = [];
    this.frameProcessor.endSegment((event) => {
      messageContainer.push(event);
    });

    for (const event of messageContainer) {
      switch (event.msg) {
        case Message.SpeechEnd:
          yield {
            audio: event.audio,
            start,
            end: (frameIndex * this.frameSamples) / 16,
          };
          break;
        default:
          break;
      }
    }
  }
}

原创不易,动动小手,一键三连噢!

完整代码见如下git仓库:

document-helper/juejin/vad-validate at master · yc-lm/document-helper

参考文档

  1. ffmpeg.wasm官网Usage | ffmpeg.wasm
  2. selero-vad snakers4/silero-vad: Silero VAD: pre-trained enterprise-grade Voice Activity Detector
  3. 测试 VAD test site