From edcd01d146dffdb618249014a2ea9c89e971e9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=8B=A4=E6=B0=91?= Date: Mon, 20 Apr 2026 10:52:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(speech):=20=E6=B7=BB=E5=8A=A0=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E8=AF=86=E5=88=AB=E5=92=8C=E5=90=88=E6=88=90=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=9A=84=E8=87=AA=E5=8A=A8=E9=87=8D=E8=BF=9E=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成网络状态监听功能,网络恢复时自动重连语音服务 - 实现 ASR 和 TTS 服务的断线重连逻辑,提升连接稳定性 - 添加异常处理和重试机制,防止连接失败导致的服务中断 - 优化网络检测逻辑,统一使用 ConnectivityManager 管理网络状态 - 添加定时重连任务,支持延迟重连和立即重连两种模式 - 完善资源清理机制,确保关闭时正确释放所有连接和回调 --- .../com/nova/brain/glass/helper/AsrHelper.kt | 145 ++++++++++++++++-- 1 file changed, 135 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt b/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt index 8064f14..3ecaa08 100644 --- a/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt +++ b/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt @@ -7,11 +7,13 @@ import android.content.Intent import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.net.ConnectivityManager +import android.net.Network import android.net.NetworkCapabilities import android.os.Handler import android.os.Looper import android.util.Log import android.view.LayoutInflater +import com.blankj.utilcode.util.Utils import com.nova.brain.glass.BuildConfig import com.nova.brain.glass.R import com.nova.brain.glass.model.RecognizeAction @@ -55,10 +57,15 @@ object AsrHelper : OfflineCmdListener { private var isMicRunning = false private var isTtsConnected = false private var isAsrConnecting = false + private var isTtsConnecting = false private var pendingStartMic = false + private var isClosed = false + private var connectivityManager: ConnectivityManager? = null + private var networkCallbackRegistered = false private const val WAKE_RESPONSE = "在呢,您请说" private const val LISTENING_TIMEOUT_MS = 30_000L + private const val RECONNECT_DELAY_MS = 3_000L private val mainHandler = Handler(Looper.getMainLooper()) private var listeningDialog: AlertDialog? = null @@ -80,6 +87,27 @@ object AsrHelper : OfflineCmdListener { mainHandler.postDelayed(this, 1000L) } } + private val asrReconnectRunnable = Runnable { + if (shouldReconnectAsr()) { + asrConnect() + } + } + private val ttsReconnectRunnable = Runnable { + if (shouldReconnectTts()) { + ttsConnect() + } + } + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Log.d(TAG, "Network available, reconnect speech channels if needed") + reconnectAllIfNeeded(immediate = true) + dismissNoNetworkDialog() + } + + override fun onLost(network: Network) { + Log.d(TAG, "Network lost") + } + } // 拼接每次识别会话中的中间结果 private var currentPartial = "" @@ -103,6 +131,7 @@ object AsrHelper : OfflineCmdListener { var onDirectChat: ((text: String) -> Unit)? = null fun init() { + isClosed = false val cfg = OnlineSpeechSdkConfig( domain = DOMAIN, ak = AK, @@ -129,19 +158,41 @@ object AsrHelper : OfflineCmdListener { // 注册离线关键词 Nova Nova,GlassSdk 触发后启动 ASR OfflineCmdServiceHelper.registerAsrWakeWord() OfflineCmdServiceHelper.addOnLineListener(this) + registerNetworkCallback() // 自动建立 ASR / TTS 连接 asrConnect() - tts?.connect() + ttsConnect() Log.d(TAG, "AsrHelper init done") } private fun asrConnect() { - if (isConnected || isAsrConnecting) return + if (!shouldReconnectAsr()) return isAsrConnecting = true - asr?.connect() - Log.d(TAG, "ASR connect() called") + runCatching { asr?.connect() } + .onSuccess { + Log.d(TAG, "ASR connect() called") + } + .onFailure { + isAsrConnecting = false + Log.e(TAG, "ASR connect failed: ${it.message}") + scheduleAsrReconnect() + } + } + + private fun ttsConnect() { + if (!shouldReconnectTts()) return + isTtsConnecting = true + runCatching { tts?.connect() } + .onSuccess { + Log.d(TAG, "TTS connect() called") + } + .onFailure { + isTtsConnecting = false + Log.e(TAG, "TTS connect failed: ${it.message}") + scheduleTtsReconnect() + } } private fun asrStartMic() { @@ -173,6 +224,64 @@ object AsrHelper : OfflineCmdListener { mainHandler.removeCallbacks(listeningTimeoutRunnable) } + private fun scheduleAsrReconnect(delayMs: Long = RECONNECT_DELAY_MS) { + if (!shouldReconnectAsr()) return + mainHandler.removeCallbacks(asrReconnectRunnable) + mainHandler.postDelayed(asrReconnectRunnable, delayMs) + Log.d(TAG, "ASR reconnect scheduled in ${delayMs}ms") + } + + private fun scheduleTtsReconnect(delayMs: Long = RECONNECT_DELAY_MS) { + if (!shouldReconnectTts()) return + mainHandler.removeCallbacks(ttsReconnectRunnable) + mainHandler.postDelayed(ttsReconnectRunnable, delayMs) + Log.d(TAG, "TTS reconnect scheduled in ${delayMs}ms") + } + + private fun reconnectAllIfNeeded(immediate: Boolean = false) { + if (!isNetworkAvailable()) return + val delayMs = if (immediate) 0L else RECONNECT_DELAY_MS + if (!isConnected) { + scheduleAsrReconnect(delayMs) + } + if (!isTtsConnected) { + scheduleTtsReconnect(delayMs) + } + } + + private fun shouldReconnectAsr(): Boolean { + return !isClosed && !isConnected && !isAsrConnecting && isNetworkAvailable() + } + + private fun shouldReconnectTts(): Boolean { + return !isClosed && !isTtsConnected && !isTtsConnecting && isNetworkAvailable() + } + + private fun getContext(): Context = Utils.getApp() + + private fun registerNetworkCallback() { + if (networkCallbackRegistered) return + val manager = getContext().getSystemService(ConnectivityManager::class.java) ?: return + connectivityManager = manager + runCatching { + manager.registerDefaultNetworkCallback(networkCallback) + networkCallbackRegistered = true + }.onFailure { + Log.e(TAG, "register network callback failed: ${it.message}") + } + } + + private fun unregisterNetworkCallback() { + if (!networkCallbackRegistered) return + runCatching { + connectivityManager?.unregisterNetworkCallback(networkCallback) + }.onFailure { + Log.w(TAG, "unregister network callback failed: ${it.message}") + } + networkCallbackRegistered = false + connectivityManager = null + } + private fun showListeningDialog() { mainHandler.post { listeningDialog?.dismiss() @@ -254,12 +363,12 @@ object AsrHelper : OfflineCmdListener { } private fun isNetworkAvailable(): Boolean { - val activity = runCatching { AppManager.getInstance().getActivity() }.getOrNull() - val context: Context = activity ?: return false - val connectivityManager = - context.getSystemService(ConnectivityManager::class.java) ?: return false - val network = connectivityManager.activeNetwork ?: return false - val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + val manager = connectivityManager + ?: getContext().getSystemService(ConnectivityManager::class.java) + ?: return false + connectivityManager = manager + val network = manager.activeNetwork ?: return false + val capabilities = manager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) } @@ -268,6 +377,7 @@ object AsrHelper : OfflineCmdListener { override fun onOpen() { isConnected = true isAsrConnecting = false + mainHandler.removeCallbacks(asrReconnectRunnable) Log.d(TAG, "ASR websocket open") if (pendingStartMic) { asrStartMic() @@ -329,6 +439,7 @@ object AsrHelper : OfflineCmdListener { isAsrConnecting = false isMicRunning = false dismissListeningDialog() + scheduleAsrReconnect() } override fun onClosed(code: Int, reason: String) { @@ -337,6 +448,7 @@ object AsrHelper : OfflineCmdListener { isMicRunning = false dismissListeningDialog() Log.d(TAG, "ASR closed code=$code reason=$reason") + scheduleAsrReconnect() } }) } @@ -345,6 +457,8 @@ object AsrHelper : OfflineCmdListener { ttsClient.setListener(object : TtsClient.Listener { override fun onOpen() { isTtsConnected = true + isTtsConnecting = false + mainHandler.removeCallbacks(ttsReconnectRunnable) Log.d(TAG, "TTS websocket open") } @@ -354,13 +468,18 @@ object AsrHelper : OfflineCmdListener { } override fun onError(code: Int, message: String) { + isTtsConnected = false + isTtsConnecting = false Log.e(TAG, "TTS error code=$code msg=$message, fallback to mic") + scheduleTtsReconnect() asrStartMic() } override fun onClosed(code: Int, reason: String) { isTtsConnected = false + isTtsConnecting = false Log.d(TAG, "TTS closed code=$code reason=$reason") + scheduleTtsReconnect() } }) } @@ -415,14 +534,18 @@ object AsrHelper : OfflineCmdListener { } fun close() { + isClosed = true OfflineCmdServiceHelper.removeOnLineListener(this) mainHandler.removeCallbacks(networkCheckRunnable) + mainHandler.removeCallbacks(asrReconnectRunnable) + mainHandler.removeCallbacks(ttsReconnectRunnable) dismissNoNetworkDialog() if (isMicRunning) { runCatching { asr?.stopAsrWithMic() } isMicRunning = false } dismissListeningDialog() + unregisterNetworkCallback() asr?.close() tts?.close() sdk?.close() @@ -430,7 +553,9 @@ object AsrHelper : OfflineCmdListener { tts = null sdk = null isConnected = false + isAsrConnecting = false isTtsConnected = false + isTtsConnecting = false Log.d(TAG, "AsrHelper closed") } }