|
|
|
@ -15,7 +15,6 @@ import com.yunqiinnovation.user_portrait.UserPortrait |
|
|
|
import com.yunqiinnovation.location_service.LocationService |
|
|
|
import android.util.Log |
|
|
|
import kotlinx.coroutines.* |
|
|
|
import kotlinx.coroutines.channels.Channel |
|
|
|
import java.util.Collections |
|
|
|
import kotlin.coroutines.CoroutineContext |
|
|
|
import com.yunqiinnovation.ble_service.BleService |
|
|
|
@ -117,7 +116,7 @@ object AgentService : CoroutineScope { |
|
|
|
|
|
|
|
// 空闲检测相关 |
|
|
|
private var idleCheckJob: Job? = null |
|
|
|
private val maxIdleSeconds = 10 // 最大空闲秒数(保持原有设置) |
|
|
|
private val maxIdleSeconds = 10 // 最大空闲秒数 |
|
|
|
|
|
|
|
// 打断防抖相关 |
|
|
|
private var lastInterruptTime = 0L |
|
|
|
@ -193,31 +192,27 @@ object AgentService : CoroutineScope { |
|
|
|
} |
|
|
|
// 初始化Azure ASR |
|
|
|
azureAsrHelper = AzureAsrHelper(context).apply { |
|
|
|
// 使用AgentService的协程作用域,确保生命周期管理 |
|
|
|
launch(Dispatchers.IO) { |
|
|
|
try { |
|
|
|
//获取语音识别的语种 |
|
|
|
val supportedLanguagesConfig = config["supportedLanguages"] as? List<String> |
|
|
|
val supportedLanguages = supportedLanguagesConfig?.toTypedArray() |
|
|
|
?: arrayOf("zh-CN", "en-US") // 默认支持多语言 |
|
|
|
|
|
|
|
val asrInitSuccess = initialize( |
|
|
|
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", |
|
|
|
region = config["azureSpeechRegion"]?.toString() ?: "", |
|
|
|
supportedLanguages = supportedLanguages, |
|
|
|
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL |
|
|
|
else AzureAsrHelper.AudioSourceType.MICROPHONE |
|
|
|
) |
|
|
|
// 使用协程处理异步初始化 |
|
|
|
CoroutineScope(Dispatchers.IO).launch { |
|
|
|
//获取语音识别的语种 |
|
|
|
val supportedLanguagesConfig = config["supportedLanguages"] as? List<String> |
|
|
|
val supportedLanguages = supportedLanguagesConfig?.toTypedArray() |
|
|
|
?: arrayOf("zh-CN", "en-US") // 默认支持多语言 |
|
|
|
|
|
|
|
val asrInitSuccess = initialize( |
|
|
|
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", |
|
|
|
region = config["azureSpeechRegion"]?.toString() ?: "", |
|
|
|
supportedLanguages = supportedLanguages, |
|
|
|
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL |
|
|
|
else AzureAsrHelper.AudioSourceType.MICROPHONE |
|
|
|
) |
|
|
|
|
|
|
|
if (asrInitSuccess) { |
|
|
|
withContext(Dispatchers.Main) { |
|
|
|
recognizeCallback() |
|
|
|
} |
|
|
|
} else { |
|
|
|
Log.w(TAG, "Azure ASR初始化失败") |
|
|
|
if (asrInitSuccess) { |
|
|
|
withContext(Dispatchers.Main) { |
|
|
|
recognizeCallback() |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "Azure ASR初始化异常", e) |
|
|
|
} else { |
|
|
|
Log.w(TAG, "Azure ASR初始化失败") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
@ -249,71 +244,35 @@ object AgentService : CoroutineScope { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 释放所有资源 - 增强版本 |
|
|
|
* 释放所有资源 |
|
|
|
*/ |
|
|
|
fun dispose() { |
|
|
|
try { |
|
|
|
Log.i(TAG, "开始释放AgentService资源...") |
|
|
|
|
|
|
|
// 1. 停止所有活动 |
|
|
|
stopRecognition() |
|
|
|
stopTts() |
|
|
|
stopAiStream() |
|
|
|
stopIdleCheck() |
|
|
|
|
|
|
|
// 2. 停止事件处理器 |
|
|
|
eventProcessorJob?.cancel() |
|
|
|
eventProcessorJob = null |
|
|
|
eventQueue.close() |
|
|
|
|
|
|
|
// 3. 释放音频播放器 |
|
|
|
audioPlayer?.release() |
|
|
|
audioPlayer = null |
|
|
|
|
|
|
|
// 4. 释放Azure ASR(确保完全清理) |
|
|
|
azureAsrHelper?.let { helper -> |
|
|
|
try { |
|
|
|
helper.dispose() |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "释放Azure ASR异常", e) |
|
|
|
} |
|
|
|
} |
|
|
|
azureAsrHelper = null |
|
|
|
azureAsrHelper?.dispose() |
|
|
|
|
|
|
|
// 5. 释放ChatAPI服务 |
|
|
|
// 释放ChatAPI服务 |
|
|
|
if (::chatApiService.isInitialized) { |
|
|
|
try { |
|
|
|
chatApiService.cancelCurrentStream() |
|
|
|
chatApiService.dispose() |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "释放ChatAPI服务异常", e) |
|
|
|
} |
|
|
|
chatApiService.cancelCurrentStream() |
|
|
|
chatApiService.dispose() |
|
|
|
} |
|
|
|
|
|
|
|
// 6. 释放TTS服务 |
|
|
|
ttsService?.let { tts -> |
|
|
|
try { |
|
|
|
tts.release() |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "释放TTS服务异常", e) |
|
|
|
} |
|
|
|
} |
|
|
|
// 释放TTS服务 |
|
|
|
ttsService?.release() |
|
|
|
ttsService = null |
|
|
|
|
|
|
|
// 7. 取消所有协程并清理监听器 |
|
|
|
job.cancel() |
|
|
|
clearListeners() |
|
|
|
|
|
|
|
// 8. 重置状态 |
|
|
|
_isInitialized.set(false) |
|
|
|
_isRecognitionActive.set(false) |
|
|
|
_hasSpeechDetected.set(false) |
|
|
|
_isTtsSpeaking.set(false) |
|
|
|
_isAiStreaming.set(false) |
|
|
|
|
|
|
|
Log.i(TAG, "AgentService资源释放完成") |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "释放资源异常: ${e.message}", e) |
|
|
|
Log.e(TAG, "释放资源异常: ${e.message}") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -456,31 +415,19 @@ object AgentService : CoroutineScope { |
|
|
|
stopIdleCheck() // 先停止现有的检查 |
|
|
|
if (!isRecognitionActive) return |
|
|
|
|
|
|
|
// BLE模式下使用稍长的超时时间,但不影响正常使用 |
|
|
|
val timeoutSeconds = if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { |
|
|
|
maxIdleSeconds + 3 // BLE模式额外增加3秒,避免过度延长 |
|
|
|
} else { |
|
|
|
maxIdleSeconds |
|
|
|
} |
|
|
|
|
|
|
|
idleCheckJob = launch { |
|
|
|
try { |
|
|
|
Log.d(TAG, "启动空闲检测,超时时间: ${timeoutSeconds}秒,音频源: $audioSourceType") |
|
|
|
delay(timeoutSeconds * 1000L) |
|
|
|
delay(maxIdleSeconds * 1000L) |
|
|
|
|
|
|
|
if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { |
|
|
|
Log.i(TAG, "空闲超时,停止识别 - 超时时间: ${timeoutSeconds}秒") |
|
|
|
stopRecognition() |
|
|
|
sendEvent( |
|
|
|
"auto_stop", |
|
|
|
mapOf("reason" to "idle_timeout", "seconds" to timeoutSeconds) |
|
|
|
mapOf("reason" to "idle_timeout", "seconds" to maxIdleSeconds) |
|
|
|
) |
|
|
|
} else { |
|
|
|
Log.d(TAG, "空闲检测结束,但条件不满足停止要求") |
|
|
|
} |
|
|
|
} catch (e: CancellationException) { |
|
|
|
// 协程被取消,正常行为 |
|
|
|
Log.d(TAG, "空闲检测被取消") |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "空闲检测异常: ${e.message}") |
|
|
|
} |
|
|
|
@ -513,12 +460,26 @@ object AgentService : CoroutineScope { |
|
|
|
Log.d(TAG, "服务未初始化") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
Log.i(TAG, "开始语音识别 - 模式: ${if (isExternalActive) "BLE外部音频" else "设备麦克风"}") |
|
|
|
|
|
|
|
// 先停止当前识别 |
|
|
|
Log.d("TAG", "开始语音识别startRecognition?") |
|
|
|
stopRecognition() |
|
|
|
|
|
|
|
BleService.openEncoder() |
|
|
|
// if (isRecognitionActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE){ |
|
|
|
// Log.d(TAG, "开始语音识别stopRecognition") |
|
|
|
// // BleService.closeCodec() |
|
|
|
// stopRecognition() |
|
|
|
|
|
|
|
// } |
|
|
|
// if((isExternalActive&&audioSourceType==AzureAsrHelper.AudioSourceType.MICROPHONE)||!isRecognitionActive) |
|
|
|
// { |
|
|
|
// Log.d(TAG, "开始语音识别BleService.openEncoder") |
|
|
|
// BleService.openEncoder() |
|
|
|
|
|
|
|
// } |
|
|
|
// if(isRecognitionActive) |
|
|
|
// { |
|
|
|
// Log.d(TAG, "开始语音识别azureAsrHelper?.startContinuousRecognition(audioSourceType)") |
|
|
|
// return true |
|
|
|
// } |
|
|
|
// 检查录音权限 |
|
|
|
if (!isExternalActive && !checkRecordAudioPermission()) { |
|
|
|
Log.e(TAG, "需要录音权限") |
|
|
|
@ -526,45 +487,22 @@ object AgentService : CoroutineScope { |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
// 设置音频源类型 |
|
|
|
audioSourceType = if (isExternalActive) { |
|
|
|
AzureAsrHelper.AudioSourceType.EXTERNAL |
|
|
|
} else { |
|
|
|
AzureAsrHelper.AudioSourceType.MICROPHONE |
|
|
|
} |
|
|
|
|
|
|
|
Log.i(TAG, "音频源类型设置为: $audioSourceType") |
|
|
|
_isRecognitionActive.set(true) |
|
|
|
_hasSpeechDetected.set(false) |
|
|
|
|
|
|
|
try { |
|
|
|
// 启动Azure ASR |
|
|
|
val asrStarted = azureAsrHelper?.startContinuousRecognition(audioSourceType) ?: false |
|
|
|
if (!asrStarted) { |
|
|
|
Log.e(TAG, "Azure ASR启动失败") |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
// 如果是BLE模式,在ASR启动后再打开编码器 |
|
|
|
if (isExternalActive) { |
|
|
|
Log.i(TAG, "BLE模式:启动编码器") |
|
|
|
// 使用协程延迟而不是阻塞主线程 |
|
|
|
launch { |
|
|
|
delay(50) // 给ASR一些启动时间 |
|
|
|
val encoderOpened = BleService.openEncoder() |
|
|
|
if (!encoderOpened) { |
|
|
|
Log.w(TAG, "BLE编码器启动失败,但继续尝试识别") |
|
|
|
} |
|
|
|
} |
|
|
|
audioSourceType = if (isExternalActive) { |
|
|
|
AzureAsrHelper.AudioSourceType.EXTERNAL |
|
|
|
} else { |
|
|
|
AzureAsrHelper.AudioSourceType.MICROPHONE |
|
|
|
} |
|
|
|
Log.d("TAG", "azureAsrHelper?.startContinuousRecognition") |
|
|
|
azureAsrHelper?.startContinuousRecognition(audioSourceType) |
|
|
|
|
|
|
|
_isRecognitionActive.set(true) |
|
|
|
_hasSpeechDetected.set(false) |
|
|
|
|
|
|
|
Log.i(TAG, "语音识别启动成功") |
|
|
|
return true |
|
|
|
|
|
|
|
} catch (e: Exception) { |
|
|
|
_isRecognitionActive.set(false) |
|
|
|
Log.e(TAG, "启动语音识别失败: ${e.message}", e) |
|
|
|
Log.e(TAG, "启动语音识别失败: ${e.message}") |
|
|
|
sendEvent( |
|
|
|
"error", mapOf( |
|
|
|
"code" to "RECOGNITION_START_ERROR", |
|
|
|
@ -587,17 +525,13 @@ object AgentService : CoroutineScope { |
|
|
|
_hasSpeechDetected.set(true) |
|
|
|
|
|
|
|
if (!previousHasSpeech) { |
|
|
|
Log.i(TAG, "首次检测到语音输入: $recognizing (音频源: $audioSourceType)") |
|
|
|
restartIdleCheck() |
|
|
|
} |
|
|
|
|
|
|
|
// 实时发送识别结果,确保UI及时更新 |
|
|
|
Log.v(TAG, "实时识别: $recognizing") |
|
|
|
sendEvent( |
|
|
|
"recognizing", mapOf( |
|
|
|
"text" to recognizing, |
|
|
|
"language" to detectedLanguage, |
|
|
|
"audioSource" to audioSourceType.toString() |
|
|
|
"language" to detectedLanguage |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
@ -670,42 +604,28 @@ object AgentService : CoroutineScope { |
|
|
|
override fun onCanceled(reason: String, errorDetails: String) { |
|
|
|
_isRecognitionActive.set(false) |
|
|
|
stopIdleCheck() |
|
|
|
Log.w(TAG, "语音识别被取消: $reason, 详情: $errorDetails") |
|
|
|
|
|
|
|
// 如果是BLE模式,关闭编码器 |
|
|
|
if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { |
|
|
|
BleService.closeCodec() |
|
|
|
} |
|
|
|
|
|
|
|
Log.d(TAG, "recognition_canceled:") |
|
|
|
BleService.closeCodec() |
|
|
|
sendEvent( |
|
|
|
"recognition_canceled", mapOf( |
|
|
|
"reason" to reason, |
|
|
|
"details" to errorDetails |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
// 重新设置回调,但不自动重启识别 |
|
|
|
recognizeCallback() |
|
|
|
} |
|
|
|
|
|
|
|
override fun onError(error: String) { |
|
|
|
_isRecognitionActive.set(false) |
|
|
|
stopIdleCheck() |
|
|
|
Log.e(TAG, "语音识别错误: $error") |
|
|
|
|
|
|
|
// 如果是BLE模式,关闭编码器 |
|
|
|
if (audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL) { |
|
|
|
BleService.closeCodec() |
|
|
|
} |
|
|
|
|
|
|
|
BleService.closeCodec() |
|
|
|
Log.d(TAG, "语音识别错误: $error") |
|
|
|
sendEvent( |
|
|
|
"error", mapOf( |
|
|
|
"code" to "RECOGNITION_ERROR", |
|
|
|
"message" to error |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
// 重新设置回调,但不自动重启识别 |
|
|
|
recognizeCallback() |
|
|
|
} |
|
|
|
}) |
|
|
|
@ -715,68 +635,10 @@ object AgentService : CoroutineScope { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 音频数据推送统计 |
|
|
|
private var audioDataCount = 0 |
|
|
|
private var lastAudioDataTime = 0L |
|
|
|
|
|
|
|
// 性能监控 |
|
|
|
private var lastMemoryCheckTime = 0L |
|
|
|
private val MEMORY_CHECK_INTERVAL = 30000L // 30秒检查一次内存 |
|
|
|
|
|
|
|
fun pushAudioData(data: ByteArray) { |
|
|
|
val currentTime = System.currentTimeMillis() |
|
|
|
audioDataCount++ |
|
|
|
|
|
|
|
// 每100个数据包或每5秒输出一次统计 |
|
|
|
if (audioDataCount % 100 == 0 || (currentTime - lastAudioDataTime) > 5000) { |
|
|
|
Log.i(TAG, "音频数据统计: 已推送${audioDataCount}包, 当前包大小: ${data.size} bytes, 音频源: $audioSourceType") |
|
|
|
lastAudioDataTime = currentTime |
|
|
|
|
|
|
|
// 定期检查内存使用情况 |
|
|
|
checkMemoryUsage(currentTime) |
|
|
|
} |
|
|
|
|
|
|
|
// 添加调试日志,但保持原有逻辑的兼容性 |
|
|
|
if (!isRecognitionActive) { |
|
|
|
Log.w(TAG, "语音识别未激活,但仍尝试推送音频数据: ${data.size} bytes") |
|
|
|
} |
|
|
|
|
|
|
|
if (audioSourceType != AzureAsrHelper.AudioSourceType.EXTERNAL) { |
|
|
|
Log.w(TAG, "音频源类型不匹配: $audioSourceType,推送音频数据: ${data.size} bytes") |
|
|
|
} |
|
|
|
|
|
|
|
// 保持原有逻辑,确保兼容性 |
|
|
|
azureAsrHelper?.audioStream?.saveAudioDataTo(data) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 检查内存使用情况 |
|
|
|
*/ |
|
|
|
private fun checkMemoryUsage(currentTime: Long) { |
|
|
|
if (currentTime - lastMemoryCheckTime > MEMORY_CHECK_INTERVAL) { |
|
|
|
lastMemoryCheckTime = currentTime |
|
|
|
|
|
|
|
val runtime = Runtime.getRuntime() |
|
|
|
val usedMemory = runtime.totalMemory() - runtime.freeMemory() |
|
|
|
val maxMemory = runtime.maxMemory() |
|
|
|
val memoryUsagePercent = (usedMemory * 100 / maxMemory).toInt() |
|
|
|
|
|
|
|
Log.i(TAG, "内存使用情况: ${usedMemory / 1024 / 1024}MB / ${maxMemory / 1024 / 1024}MB (${memoryUsagePercent}%)") |
|
|
|
|
|
|
|
// 如果内存使用超过80%,发出警告 |
|
|
|
if (memoryUsagePercent > 80) { |
|
|
|
Log.w(TAG, "内存使用率过高: ${memoryUsagePercent}%,建议检查内存泄漏") |
|
|
|
|
|
|
|
// 发送内存警告事件 |
|
|
|
sendEvent("memory_warning", mapOf( |
|
|
|
"usedMemoryMB" to (usedMemory / 1024 / 1024), |
|
|
|
"maxMemoryMB" to (maxMemory / 1024 / 1024), |
|
|
|
"usagePercent" to memoryUsagePercent |
|
|
|
)) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 停止语音识别 |
|
|
|
*/ |
|
|
|
@ -1374,53 +1236,19 @@ object AgentService : CoroutineScope { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 事件发送优化:使用单一协程处理事件队列 |
|
|
|
private val eventQueue = Channel<Pair<String, Map<String, Any>>>(capacity = Channel.UNLIMITED) |
|
|
|
private var eventProcessorJob: Job? = null |
|
|
|
|
|
|
|
init { |
|
|
|
// 启动事件处理协程 |
|
|
|
startEventProcessor() |
|
|
|
} |
|
|
|
|
|
|
|
private fun startEventProcessor() { |
|
|
|
eventProcessorJob = launch(Dispatchers.Main) { |
|
|
|
for ((eventName, data) in eventQueue) { |
|
|
|
try { |
|
|
|
// 创建监听器快照,避免并发修改 |
|
|
|
val currentListeners = synchronized(listeners) { listeners.toList() } |
|
|
|
|
|
|
|
currentListeners.forEach { listener -> |
|
|
|
try { |
|
|
|
listener.onEvent(eventName, data) |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "向监听器发送事件失败: $eventName", e) |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "处理事件失败: $eventName", e) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 发送事件 - 优化版本,使用事件队列避免主线程阻塞 |
|
|
|
* 发送事件 |
|
|
|
*/ |
|
|
|
private fun sendEvent(eventName: String, data: Map<String, Any>) { |
|
|
|
try { |
|
|
|
// 对于高频事件,进行限流处理 |
|
|
|
if (eventName == "recognizing" && eventQueue.trySend(Pair(eventName, data)).isFailure) { |
|
|
|
Log.v(TAG, "识别事件队列已满,跳过当前事件") |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// 非阻塞发送到事件队列 |
|
|
|
if (!eventQueue.trySend(Pair(eventName, data)).isSuccess) { |
|
|
|
Log.w(TAG, "事件队列已满,丢弃事件: $eventName") |
|
|
|
// 切换到主线程执行监听器回调,避免 UI 更新问题 |
|
|
|
launch(Dispatchers.Main) { |
|
|
|
listeners.forEach { listener -> |
|
|
|
try { |
|
|
|
listener.onEvent(eventName, data) |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "向监听器发送事件失败", e) |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
Log.e(TAG, "发送事件异常: $eventName", e) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|