通用语音输入模块
在知与行 App 中,新建计划、新建感悟、神秘人聊天三个场景都需要文字输入。当前均依赖键盘打字,体验割裂。
by 于尘
·
知与行
React Native
Agent
通用语音输入模块
记录日期:2026-04-11
1. 背景与目标
在知与行 App 中,新建计划、新建感悟、神秘人聊天三个场景都需要文字输入。当前均依赖键盘打字,体验割裂。
目标:实现一个通用的语音输入转文字模块,三个场景共用同一底层,复用 UI 和交互,一处优化三处生效。
2. 设计方案
2.1 技术选型
| 方案 | 优点 | 缺点 | 推荐 |
|---|---|---|---|
| iOS Speech Framework | 系统级集成,精度高,无需网络 | 仅 iOS | iOS 主方案 |
| Android SpeechRecognizer | 系统级集成 | 仅 Android | Android 主方案 |
| Web Speech API | 跨平台,零依赖 | 依赖浏览器支持,精度一般 | Web 主方案 |
| 第三方 API(MiniMax/DeepSeek) | 精度高,跨平台 | 有延迟,需积分,耗流量 | 降级兜底 |
结论:优先用平台原生 SDK,Web 用 Speech API,失败时降级到服务端 API。
2.2 模块结构
src/
├── lib/
│ └── voice-input/ # 通用语音输入模块
│ ├── index.ts # 统一导出
│ ├── VoiceInputEngine.ts # 核心引擎(平台分发)
│ ├── VoiceInputEngine.ios.ts
│ ├── VoiceInputEngine.android.ts
│ ├── VoiceInputEngine.web.ts
│ ├── VoiceInputEngine.fallback.ts # 服务端兜底
│ └── types.ts
├── hooks/
│ └── useVoiceInput.ts # React Hook,封装状态和交互
└── components/
└── VoiceInputButton.tsx # 可复用的麦克风按钮组件
2.3 核心接口
// types.ts
export type VoiceInputState = 'idle' | 'listening' | 'processing' | 'error' | 'disabled'
export interface VoiceInputResult {
text: string
confidence: number // 0-1
isFinal: boolean
error?: string
}
export interface VoiceInputOptions {
language?: string // 默认 'zh-CN'
interim?: boolean // 是否返回临时结果,默认 true
onResult?: (result: VoiceInputResult) => void
onError?: (error: string) => void
onEnd?: () => void
}
export interface VoiceInputEngine {
state: VoiceInputState
start(options: VoiceInputOptions): void
stop(): void
isSupported(): boolean
}
// VoiceInputEngine.ts — 平台分发
import { Platform } from 'react-native'
import { VoiceInputEngineIos } from './VoiceInputEngine.ios'
import { VoiceInputEngineAndroid } from './VoiceInputEngine.android'
import { VoiceInputEngineWeb } from './VoiceInputEngine.web'
import { VoiceInputEngineFallback } from './VoiceInputEngine.fallback'
export function createVoiceInputEngine(): VoiceInputEngine {
if (Platform.OS === 'ios') return new VoiceInputEngineIos()
if (Platform.OS === 'android') return new VoiceInputEngineAndroid()
if (typeof window !== 'undefined' && 'webkitSpeechRecognition' in window) return new VoiceInputEngineWeb()
return new VoiceInputEngineFallback()
}
2.4 Hook 封装
// useVoiceInput.ts
import { useRef, useState, useCallback, useEffect } from 'react'
import { Alert, Platform } from 'react-native'
import { createVoiceInputEngine, type VoiceInputResult, type VoiceInputOptions } from '../lib/voice-input'
export function useVoiceInput(onTranscription: (text: string) => void) {
const engineRef = useRef<ReturnType<typeof createVoiceInputEngine> | null>(null)
const [state, setState] = useState<VoiceInputState>('idle')
useEffect(() => {
engineRef.current = createVoiceInputEngine()
return () => engineRef.current?.stop()
}, [])
const start = useCallback(() => {
if (!engineRef.current) return
if (!engineRef.current.isSupported()) {
Alert.alert('不支持语音', '当前设备不支持语音输入')
return
}
engineRef.current.start({
language: 'zh-CN',
interim: true,
onResult: (result: VoiceInputResult) => {
const text = result.text.trim()
if (text) onTranscription(text)
},
onError: (error: string) => {
setState('error')
Alert.alert('语音识别失败', error)
},
onEnd: () => setState('idle'),
})
setState('listening')
}, [onTranscription])
const stop = useCallback(() => {
engineRef.current?.stop()
setState('idle')
}, [])
return { state, start, stop, isListening: state === 'listening' }
}
2.5 UI 组件
// VoiceInputButton.tsx
import React from 'react'
import { TouchableOpacity, StyleSheet, Animated } from 'react-native'
import { Ionicons } from '@expo/vector-icons'
interface VoiceInputButtonProps {
isListening: boolean
disabled?: boolean
onPressIn: () => void // 按下开始录音
onPressOut: () => void // 松开停止录音
}
export function VoiceInputButton({ isListening, disabled, onPressIn, onPressOut }: VoiceInputButtonProps) {
const pulseAnim = React.useRef(new Animated.Value(1)).current
React.useEffect(() => {
if (isListening) {
const anim = Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, { toValue: 1.15, duration: 600, useNativeDriver: true }),
Animated.timing(pulseAnim, { toValue: 1, duration: 600, useNativeDriver: true }),
])
)
anim.start()
return () => anim.stop()
} else {
pulseAnim.setValue(1)
}
}, [isListening])
return (
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
<TouchableOpacity
onPressIn={onPressIn}
onPressOut={onPressOut}
disabled={disabled}
style={[
styles.btn,
isListening && styles.btnListening,
disabled && styles.btnDisabled,
]}
accessibilityLabel="语音输入"
>
<Ionicons
name={isListening ? 'mic' : 'mic-outline'}
size={20}
color={isListening ? '#fff' : disabled ? '#9CA3AF' : '#4F46E5'}
/>
</TouchableOpacity>
</Animated.View>
)
}
const styles = StyleSheet.create({
btn: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#EEF2FF',
alignItems: 'center',
justifyContent: 'center',
},
btnListening: {
backgroundColor: '#EF4444',
},
btnDisabled: {
opacity: 0.5,
},
})
2.6 接入场景
新建计划页面(new-plan)
在计划输入框右侧添加 VoiceInputButton,onTranscription 将文字追加到 TextInput:
const { state, start, stop, isListening } = useVoiceInput((text) => {
setPlanTitle(prev => prev + text)
})
<View style={styles.inputRow}>
<TextInput value={planTitle} onChangeText={setPlanTitle} ... />
<VoiceInputButton
isListening={isListening}
onPressIn={start}
onPressOut={stop}
/>
</View>
新建感悟页面(edit-insight)
同上,onTranscription 追加到感悟内容 TextInput。
神秘人聊天(agent-chat)
聊天输入栏底部添加 VoiceInputButton,onTranscription 将文字填入输入框,支持发送。
2.7 权限处理
// VoiceInputEngine.ios.ts
import { Platform, PermissionsAndroid } from 'react-native'
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions'
export async function requestMicrophonePermission(): Promise<boolean> {
if (Platform.OS === 'ios') {
const result = await request(PERMISSIONS.IOS.SPEECH_RECOGNITION)
return result === RESULTS.GRANTED
}
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
)
return granted === PermissionsAndroid.RESULTS.GRANTED
}
return false
}
3. 实现计划
| 阶段 | 内容 | 优先级 |
|---|---|---|
| Phase 1 | 核心引擎 + types + 平台分发 | P0 |
| Phase 2 | iOS/Android/Web 引擎实现 | P0 |
| Phase 3 | useVoiceInput Hook | P0 |
| Phase 4 | VoiceInputButton 组件 | P1 |
| Phase 5 | 接入新建计划页面 | P1 |
| Phase 6 | 接入新建感悟页面 | P1 |
| Phase 7 | 接入神秘人聊天页面 | P2 |
| Phase 8 | 降级兜底:服务端 API | P2 |
4. 小结
通用语音输入模块通过平台分发引擎 + React Hook 封装 + 可复用按钮组件三层结构,实现"一处开发、三处复用"。
核心设计要点:
- 引擎抽象层隔离平台差异,上层无需感知具体实现
useVoiceInputHook 封装状态管理,每个接入场景 < 10 行代码- 按住说话(press-in)、松开停止(press-out)的交互符合移动端直觉
- Web 端零依赖,Native 端零额外依赖(利用系统 Speech Framework)