feat(speech): 添加语音识别和合成服务的自动重连机制
- 集成网络状态监听功能,网络恢复时自动重连语音服务 - 实现 ASR 和 TTS 服务的断线重连逻辑,提升连接稳定性 - 添加异常处理和重试机制,防止连接失败导致的服务中断 - 优化网络检测逻辑,统一使用 ConnectivityManager 管理网络状态 - 添加定时重连任务,支持延迟重连和立即重连两种模式 - 完善资源清理机制,确保关闭时正确释放所有连接和回调
这个提交包含在:
父节点
ec83fe0c2d
当前提交
edcd01d146
@ -7,11 +7,13 @@ import android.content.Intent
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import com.blankj.utilcode.util.Utils
|
||||||
import com.nova.brain.glass.BuildConfig
|
import com.nova.brain.glass.BuildConfig
|
||||||
import com.nova.brain.glass.R
|
import com.nova.brain.glass.R
|
||||||
import com.nova.brain.glass.model.RecognizeAction
|
import com.nova.brain.glass.model.RecognizeAction
|
||||||
@ -55,10 +57,15 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
private var isMicRunning = false
|
private var isMicRunning = false
|
||||||
private var isTtsConnected = false
|
private var isTtsConnected = false
|
||||||
private var isAsrConnecting = false
|
private var isAsrConnecting = false
|
||||||
|
private var isTtsConnecting = false
|
||||||
private var pendingStartMic = 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 WAKE_RESPONSE = "在呢,您请说"
|
||||||
private const val LISTENING_TIMEOUT_MS = 30_000L
|
private const val LISTENING_TIMEOUT_MS = 30_000L
|
||||||
|
private const val RECONNECT_DELAY_MS = 3_000L
|
||||||
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
private var listeningDialog: AlertDialog? = null
|
private var listeningDialog: AlertDialog? = null
|
||||||
@ -80,6 +87,27 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
mainHandler.postDelayed(this, 1000L)
|
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 = ""
|
private var currentPartial = ""
|
||||||
@ -103,6 +131,7 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
var onDirectChat: ((text: String) -> Unit)? = null
|
var onDirectChat: ((text: String) -> Unit)? = null
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
|
isClosed = false
|
||||||
val cfg = OnlineSpeechSdkConfig(
|
val cfg = OnlineSpeechSdkConfig(
|
||||||
domain = DOMAIN,
|
domain = DOMAIN,
|
||||||
ak = AK,
|
ak = AK,
|
||||||
@ -129,20 +158,42 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
// 注册离线关键词 Nova Nova,GlassSdk 触发后启动 ASR
|
// 注册离线关键词 Nova Nova,GlassSdk 触发后启动 ASR
|
||||||
OfflineCmdServiceHelper.registerAsrWakeWord()
|
OfflineCmdServiceHelper.registerAsrWakeWord()
|
||||||
OfflineCmdServiceHelper.addOnLineListener(this)
|
OfflineCmdServiceHelper.addOnLineListener(this)
|
||||||
|
registerNetworkCallback()
|
||||||
|
|
||||||
// 自动建立 ASR / TTS 连接
|
// 自动建立 ASR / TTS 连接
|
||||||
asrConnect()
|
asrConnect()
|
||||||
tts?.connect()
|
ttsConnect()
|
||||||
|
|
||||||
Log.d(TAG, "AsrHelper init done")
|
Log.d(TAG, "AsrHelper init done")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun asrConnect() {
|
private fun asrConnect() {
|
||||||
if (isConnected || isAsrConnecting) return
|
if (!shouldReconnectAsr()) return
|
||||||
isAsrConnecting = true
|
isAsrConnecting = true
|
||||||
asr?.connect()
|
runCatching { asr?.connect() }
|
||||||
|
.onSuccess {
|
||||||
Log.d(TAG, "ASR connect() called")
|
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() {
|
private fun asrStartMic() {
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
@ -173,6 +224,64 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
mainHandler.removeCallbacks(listeningTimeoutRunnable)
|
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() {
|
private fun showListeningDialog() {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
listeningDialog?.dismiss()
|
listeningDialog?.dismiss()
|
||||||
@ -254,12 +363,12 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isNetworkAvailable(): Boolean {
|
private fun isNetworkAvailable(): Boolean {
|
||||||
val activity = runCatching { AppManager.getInstance().getActivity() }.getOrNull()
|
val manager = connectivityManager
|
||||||
val context: Context = activity ?: return false
|
?: getContext().getSystemService(ConnectivityManager::class.java)
|
||||||
val connectivityManager =
|
?: return false
|
||||||
context.getSystemService(ConnectivityManager::class.java) ?: return false
|
connectivityManager = manager
|
||||||
val network = connectivityManager.activeNetwork ?: return false
|
val network = manager.activeNetwork ?: return false
|
||||||
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
val capabilities = manager.getNetworkCapabilities(network) ?: return false
|
||||||
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,6 +377,7 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
override fun onOpen() {
|
override fun onOpen() {
|
||||||
isConnected = true
|
isConnected = true
|
||||||
isAsrConnecting = false
|
isAsrConnecting = false
|
||||||
|
mainHandler.removeCallbacks(asrReconnectRunnable)
|
||||||
Log.d(TAG, "ASR websocket open")
|
Log.d(TAG, "ASR websocket open")
|
||||||
if (pendingStartMic) {
|
if (pendingStartMic) {
|
||||||
asrStartMic()
|
asrStartMic()
|
||||||
@ -329,6 +439,7 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
isAsrConnecting = false
|
isAsrConnecting = false
|
||||||
isMicRunning = false
|
isMicRunning = false
|
||||||
dismissListeningDialog()
|
dismissListeningDialog()
|
||||||
|
scheduleAsrReconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosed(code: Int, reason: String) {
|
override fun onClosed(code: Int, reason: String) {
|
||||||
@ -337,6 +448,7 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
isMicRunning = false
|
isMicRunning = false
|
||||||
dismissListeningDialog()
|
dismissListeningDialog()
|
||||||
Log.d(TAG, "ASR closed code=$code reason=$reason")
|
Log.d(TAG, "ASR closed code=$code reason=$reason")
|
||||||
|
scheduleAsrReconnect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -345,6 +457,8 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
ttsClient.setListener(object : TtsClient.Listener {
|
ttsClient.setListener(object : TtsClient.Listener {
|
||||||
override fun onOpen() {
|
override fun onOpen() {
|
||||||
isTtsConnected = true
|
isTtsConnected = true
|
||||||
|
isTtsConnecting = false
|
||||||
|
mainHandler.removeCallbacks(ttsReconnectRunnable)
|
||||||
Log.d(TAG, "TTS websocket open")
|
Log.d(TAG, "TTS websocket open")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,13 +468,18 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(code: Int, message: String) {
|
override fun onError(code: Int, message: String) {
|
||||||
|
isTtsConnected = false
|
||||||
|
isTtsConnecting = false
|
||||||
Log.e(TAG, "TTS error code=$code msg=$message, fallback to mic")
|
Log.e(TAG, "TTS error code=$code msg=$message, fallback to mic")
|
||||||
|
scheduleTtsReconnect()
|
||||||
asrStartMic()
|
asrStartMic()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosed(code: Int, reason: String) {
|
override fun onClosed(code: Int, reason: String) {
|
||||||
isTtsConnected = false
|
isTtsConnected = false
|
||||||
|
isTtsConnecting = false
|
||||||
Log.d(TAG, "TTS closed code=$code reason=$reason")
|
Log.d(TAG, "TTS closed code=$code reason=$reason")
|
||||||
|
scheduleTtsReconnect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -415,14 +534,18 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun close() {
|
fun close() {
|
||||||
|
isClosed = true
|
||||||
OfflineCmdServiceHelper.removeOnLineListener(this)
|
OfflineCmdServiceHelper.removeOnLineListener(this)
|
||||||
mainHandler.removeCallbacks(networkCheckRunnable)
|
mainHandler.removeCallbacks(networkCheckRunnable)
|
||||||
|
mainHandler.removeCallbacks(asrReconnectRunnable)
|
||||||
|
mainHandler.removeCallbacks(ttsReconnectRunnable)
|
||||||
dismissNoNetworkDialog()
|
dismissNoNetworkDialog()
|
||||||
if (isMicRunning) {
|
if (isMicRunning) {
|
||||||
runCatching { asr?.stopAsrWithMic() }
|
runCatching { asr?.stopAsrWithMic() }
|
||||||
isMicRunning = false
|
isMicRunning = false
|
||||||
}
|
}
|
||||||
dismissListeningDialog()
|
dismissListeningDialog()
|
||||||
|
unregisterNetworkCallback()
|
||||||
asr?.close()
|
asr?.close()
|
||||||
tts?.close()
|
tts?.close()
|
||||||
sdk?.close()
|
sdk?.close()
|
||||||
@ -430,7 +553,9 @@ object AsrHelper : OfflineCmdListener {
|
|||||||
tts = null
|
tts = null
|
||||||
sdk = null
|
sdk = null
|
||||||
isConnected = false
|
isConnected = false
|
||||||
|
isAsrConnecting = false
|
||||||
isTtsConnected = false
|
isTtsConnected = false
|
||||||
|
isTtsConnecting = false
|
||||||
Log.d(TAG, "AsrHelper closed")
|
Log.d(TAG, "AsrHelper closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户