You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的针对性修复

  1. 匹配模型与语言:如果识别中文,必须使用中文Vosk模型,不能混用英文模型
  2. 核对录音参数:确保AudioRecord的采样率为16kHz、单声道、16bit PCM,这是Vosk的强制要求
  3. 验证模型完整性:检查assets中的模型文件是否完整,解压后大小符合预期(中文小模型约1.2GB)
  4. 测试音频输入:用第三方录音APP测试麦克风是否正常工作,排除硬件或权限问题

内容的提问来源于stack exchange,提问作者Asadullah Hil Galib

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 13:07:50