Jetpack Compose语音转文字遇ERROR_NO_MATCH,求无弹窗本地实现方案
解决Jetpack Compose中语音转文字的ERROR_NO_MATCH问题(无Google API、无系统弹窗方案)
一、ERROR_NO_MATCH的核心排查方向
- 语言配置不匹配:离线识别模型的语言设置和实际发音语言不一致,比如用英文模型识别中文
- 音频参数不兼容:录音的采样率、声道数、编码格式和识别模型要求不匹配
- 模型加载异常:离线模型文件损坏、未正确放置到assets目录,或初始化过程出错
- 音频输入问题:麦克风权限未授予、环境噪音过大、麦克风硬件故障
二、无Google API的现代离线实现方案
放弃依赖Google服务且会弹出系统弹窗的SpeechRecognizer,改用开源离线语音识别库Vosk(轻量、易集成、支持多语言)。
1. 项目集成配置
在Module级别的build.gradle中添加依赖:
implementation 'com.alphacephei:vosk-android:0.3.45'
在AndroidManifest.xml中添加必要权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 加载本地模型需加 -->
2. 重构RealSpeechToText实现(无系统弹窗)
替换原有基于Google SpeechRecognizer的逻辑,直接控制录音和离线识别:
class RealSpeechToText(private val context: Context) : SpeechToText { private var model: Model? = null private var recognizer: SpeechRecognizer? = null private var audioRecorder: AudioRecord? = null private var isListening = false private var resultCallback: ((String) -> Unit)? = null private var errorCallback: ((String) -> Unit)? = null init { // 子线程加载模型,避免阻塞主线程 CoroutineScope(Dispatchers.IO).launch { try { // 把Vosk中文小模型放在assets/vosk-model-small-zh-cn-0.22目录下 model = Model(context, "vosk-model-small-zh-cn-0.22") recognizer = SpeechRecognizer(model).apply { setListener(object : RecognitionListener { override fun onResult(result: RecognitionResult?) { result?.finalResult?.let { resultCallback?.invoke(it) } } override fun onPartialResult(partialResult: PartialResult?) {} override fun onError(error: Exception?) { errorCallback?.invoke(error?.message ?: "未知错误") } override fun onTimeout() {} }) } } catch (e: Exception) { errorCallback?.invoke("模型加载失败: ${e.message}") } } } override fun startListening(language: String, resultCallback: (String) -> Unit, errorCallback: (String) -> Unit) { if (isListening || recognizer == null) return this.resultCallback = resultCallback this.errorCallback = errorCallback isListening = true // 初始化符合Vosk要求的录音参数:16kHz采样率、单声道、16bit PCM val sampleRate = 16000 val audioFormat = AudioFormat.ENCODING_PCM_16BIT val channelConfig = AudioFormat.CHANNEL_IN_MONO val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat) try { audioRecorder = AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channelConfig, audioFormat, bufferSize) audioRecorder?.startRecording() // 启动录音识别线程 CoroutineScope(Dispatchers.IO).launch { val buffer = ByteArray(bufferSize) while (isListening && audioRecorder != null) { val readBytes = audioRecorder?.read(buffer, 0, buffer.size) ?: 0 if (readBytes > 0) { recognizer?.acceptWaveForm(buffer, readBytes) } } } } catch (e: Exception) { errorCallback("录音启动失败: ${e.message}") stopListening() } } override fun stopListening() { isListening = false audioRecorder?.stop() audioRecorder?.release() audioRecorder = null recognizer?.cancel() } override fun destroy() { stopListening() recognizer?.close() model?.close() } }
3. 调整AppViewModel的逻辑
完善权限检查和生命周期管理:
class AppViewModel : ViewModel() { private val _speechResult = MutableStateFlow("") val speechResult = _speechResult.asStateFlow() private val _error = MutableStateFlow("") val error = _error.asStateFlow() private lateinit var speechToText: SpeechToText fun initSpeechToText(context: Context) { speechToText = RealSpeechToText(context) } fun startListening(language: String = "zh-CN") { viewModelScope.launch { val hasPermission = ContextCompat.checkSelfPermission( context, Manifest.permission.RECORD_AUDIO ) == PackageManager.PERMISSION_GRANTED if (!hasPermission) { _error.emit("请授予录音权限") return@launch } speechToText.startListening( language = language, resultCallback = { result -> viewModelScope.launch { _speechResult.emit(result) } }, errorCallback = { errorMsg -> viewModelScope.launch { _error.emit(errorMsg) } } ) } } fun stopListening() { speechToText.stopListening() } override fun onCleared() { super.onCleared() speechToText.destroy() } }
4. Compose UI组件优化
处理权限请求和状态展示:
@Composable fun SpeechToTextScreen(viewModel: AppViewModel = viewModel()) { val speechResult by viewModel.speechResult.collectAsState() val error by viewModel.error.collectAsState() val context = LocalContext.current val permissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO) LaunchedEffect(Unit) { viewModel.initSpeechToText(context) } Column( modifier = Modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { if (error.isNotEmpty()) { Text(text = error, color = Color.Red, modifier = Modifier.padding(bottom = 16.dp)) } Text(text = "识别结果: $speechResult", modifier = Modifier.padding(bottom = 32.dp)) Button( onClick = { if (permissionState.hasPermission) viewModel.startListening() else permissionState.launchPermissionRequest() }, modifier = Modifier.padding(bottom = 16.dp) ) { Text("开始录音") } Button(onClick = { viewModel.stopListening() }) { Text("停止录音") } } }
三、ERROR_NO_MATCH的针对性修复
- 匹配模型与语言:如果识别中文,必须使用中文Vosk模型,不能混用英文模型
- 核对录音参数:确保AudioRecord的采样率为16kHz、单声道、16bit PCM,这是Vosk的强制要求
- 验证模型完整性:检查assets中的模型文件是否完整,解压后大小符合预期(中文小模型约1.2GB)
- 测试音频输入:用第三方录音APP测试麦克风是否正常工作,排除硬件或权限问题
内容的提问来源于stack exchange,提问作者Asadullah Hil Galib
相关产品推荐
相关产品推荐

