基于 Web Speech API 的 React 语音唤醒与转写 Hook 实践

本文介绍如何用 React + Web Speech API 实现“语音唤醒词+持续转写”功能,并详细解析 useVoiceWakeup 自定义 Hook 的设计与用法。

背景与目标

在智能助手、语音交互等场景中,常见需求是:

  • 用户说出唤醒词(如“你好小助手”)后,系统自动进入持续语音转写模式
  • 支持静默超时自动回到唤醒监听,避免误触发和资源浪费
  • 能灵活配置唤醒词、语言、静默时长等参数

useVoiceWakeup 就是为此场景设计的 React Hook,基于浏览器原生 Web Speech API 实现,兼容 Chrome、Edge、部分国产浏览器。


Hook 核心功能

  • 唤醒词监听:只要检测到用户说出任一唤醒词(可自定义),自动切换到转写模式
  • 持续转写:唤醒后持续识别用户语音,实时追加到 transcript
  • 静默超时:用户长时间不说话,自动回到唤醒监听,节省资源
  • 错误处理:自动重启监听,支持 debug 日志
  • API 简洁:只需一行代码即可集成

代码结构与关键实现

1. 参数与返回值

const {
  transcript, // 当前累计转写文本
  isAwake, // 是否已被唤醒
  error, // 错误信息
  reset, // 重置状态(回到唤醒模式)
  stop, // 停止监听
} = useVoiceWakeup({
  lang: 'zh-CN',
  wakeWords: ['你好小助手', '小助手'],
  silenceTimeoutMs: 10000, // 10秒静默超时
  debug: true,
});

2. 唤醒监听与转写切换

  • 唤醒模式continuous=false,每次只识别一句,检测到唤醒词后切换
  • 转写模式continuous=true,持续识别,静默超时后自动回到唤醒
  • 状态切换:内部自动管理,无需手动干预

3. 静默超时与自动重启

  • 每次有新语音输入时重置静默计时器
  • 超时后自动 stop + 回到唤醒监听
  • 监听 onend/onerror,保证健壮性

4. 关键实现片段

// 1. 唤醒监听
const startWakeupMode = () => {
  const recognition = createRecognition(false); // 单句监听
  recognition.onresult = (event) => {
    const result = ...;
    if (wakeWords.some(word => result.includes(word))) {
      setIsAwake(true);
      recognition.onend = startTranscribeMode;
      recognition.stop();
    }
  };
  recognition.start();
};

// 2. 持续转写
const startTranscribeMode = () => {
  const recognition = createRecognition(true); // 持续识别
  recognition.onresult = (event) => {
    ... // 追加转写内容
    startSilenceTimer();
  };
  recognition.start();
};

// 3. 静默超时
const startSilenceTimer = () => {
  silenceTimer.current = setTimeout(() => {
    setIsAwake(false);
    stop();
    startWakeupMode();
  }, silenceTimeoutMs);
};

在这里插入图片描述


实际应用场景

  • 智能语音助手(如网页端“小爱同学”、“小度”)
  • 语音表单、语音笔记、语音命令输入
  • 需要“先唤醒再转写”安全机制的场景

注意事项与扩展

  • 需在支持 Web Speech API 的浏览器下使用(推荐 Chrome)
  • 唤醒词建议用中文短语,避免误触发
  • 可结合 TTS(语音合成)实现完整语音交互闭环
  • 可扩展支持多语言、更多唤醒词、UI 反馈等

总结

useVoiceWakeup 让 React 项目轻松拥有“唤醒词+转写”能力,适合各类语音交互场景。只需一行代码,即可获得健壮的语音监听体验。

源码如下:

import { useEffect, useRef, useState } from 'react';

declare global {
  interface Window {
    SpeechRecognition: new () => SpeechRecognition;
    webkitSpeechRecognition: new () => SpeechRecognition;
  }
}

interface SpeechRecognition extends EventTarget {
  lang: string;
  continuous: boolean;
  interimResults: boolean;
  start(): void;
  stop(): void;
  onresult: ((event: SpeechRecognitionEvent) => void) | null;
  onerror: ((event: SpeechRecognitionErrorEvent) => void) | null;
  onstart: (() => void) | null;
  onend: (() => void) | null;
}

interface SpeechRecognitionEvent {
  resultIndex: number;
  results: SpeechRecognitionResultList;
}

interface SpeechRecognitionResultList {
  length: number;
  [index: number]: SpeechRecognitionResult;
}

interface SpeechRecognitionResult {
  isFinal: boolean;
  [index: number]: SpeechRecognitionAlternative;
}

interface SpeechRecognitionAlternative {
  transcript: string;
  confidence: number;
}

interface SpeechRecognitionErrorEvent {
  error: string;
}

type UseVoiceWakeupOptions = {
  lang?: string;
  wakeWords?: string[]; // 唤醒词列表
  debug?: boolean;
  silenceTimeoutMs?: number; // 静默超时时间
};

type UseVoiceWakeupResult = {
  transcript: string;
  isAwake: boolean;
  error: string | null;
  reset: () => void;
  stop: () => void;
};

export function useVoiceWakeup(options?: UseVoiceWakeupOptions): UseVoiceWakeupResult {
  const { lang = 'zh-CN', wakeWords = ['你好小助手'], debug = false, silenceTimeoutMs = 10000 } = options || {};

  const [transcript, setTranscript] = useState('');
  const [isAwake, setIsAwake] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const recognitionRef = useRef<SpeechRecognition | null>(null);
  const isListeningRef = useRef(false);
  const silenceTimer = useRef<NodeJS.Timeout | null>(null);

  const log = (...args: any[]) => {
    if (debug) console.log('[useVoiceWakeup]', ...args);
  };

  const createRecognition = (continuous = false): SpeechRecognition | null => {
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SpeechRecognition) {
      setError('当前浏览器不支持 SpeechRecognition');
      return null;
    }

    const recognition = new SpeechRecognition();
    recognition.lang = lang;
    recognition.interimResults = false;
    recognition.continuous = continuous;
    return recognition;
  };

  const stop = () => {
    if (recognitionRef.current) {
      recognitionRef.current.stop();
      isListeningRef.current = false;
    }
    if (silenceTimer.current) {
      clearTimeout(silenceTimer.current);
    }
  };

  const reset = () => {
    stop();
    setTranscript('');
    setIsAwake(false);
    setError(null);
    startWakeupMode(); // 回到唤醒模式
  };

  const startSilenceTimer = () => {
    if (silenceTimer.current) clearTimeout(silenceTimer.current);
    silenceTimer.current = setTimeout(() => {
      log('🕓 静默超时,重回唤醒模式');
      setIsAwake(false);
      stop();
      startWakeupMode();
    }, silenceTimeoutMs);
  };

  const startWakeupMode = () => {
    const recognition = createRecognition(false); // 单句监听
    if (!recognition) return;

    recognition.onresult = (event: SpeechRecognitionEvent) => {
      const result = Array.from(event.results)
        .map((res) => res[0].transcript)
        .join('');
      log('唤醒检测识别结果:', result);

      const matched = wakeWords.some((word) => result.includes(word));
      if (matched) {
        log('✅ 唤醒词匹配成功');
        setIsAwake(true);

        recognition.onend = () => {
          log('唤醒识别结束,切换转写模式');
          startTranscribeMode();
        };
        recognition.stop();
      }
    };

    recognition.onerror = (e: SpeechRecognitionErrorEvent) => {
      log('❌ 唤醒识别错误:', e.error);
      setError(e.error);
    };

    recognition.onend = () => {
      if (!isAwake) {
        isListeningRef.current = false;
        setTimeout(() => startWakeupMode(), 300); // 自动重启
      }
    };

    recognitionRef.current = recognition;
    recognition.start();
    isListeningRef.current = true;
    log('🎤 开始唤醒监听');
  };

  const startTranscribeMode = () => {
    const recognition = createRecognition(true); // 持续识别
    if (!recognition) return;

    recognition.onresult = (event: SpeechRecognitionEvent) => {
      let result = '';
      for (let i = event.resultIndex; i < event.results.length; ++i) {
        const res = event.results[i];
        if (res.isFinal) {
          result += res[0].transcript;
        }
      }

      if (result.trim()) {
        startSilenceTimer(); // 重置静默计时器
        setTranscript((prev) => {
          const updated = prev + result;
          log('📌 转写中:', updated);
          return updated;
        });
      }
    };

    recognition.onerror = (e: SpeechRecognitionErrorEvent) => {
      log('❌ 转写识别错误:', e.error);
      setError(e.error);
    };

    recognition.onend = () => {
      isListeningRef.current = false;
      if (isAwake) {
        log('🔁 转写监听结束,尝试重启');
        setTimeout(() => startTranscribeMode(), 300);
      }
    };

    recognitionRef.current = recognition;
    recognition.start();
    isListeningRef.current = true;
    log('🎙️ 开始语音转写');
    startSilenceTimer();
  };

  useEffect(() => {
    startWakeupMode();
    return () => {
      stop();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return {
    transcript,
    isAwake,
    error,
    reset,
    stop,
  };
}

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐