基于 Web Speech API 的 React 语音唤醒与转写 Hook 实践
·
基于 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,
};
}
更多推荐


所有评论(0)