Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
lxm 1 year ago
parent
commit
f7c27c09d2
  1. 81
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  2. 3
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  3. 203
      local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

81
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -2,6 +2,8 @@ package com.yunqiinnovation.agent_service
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import org.json.JSONArray
import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper
@ -38,6 +40,7 @@ import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date
/**
* 代理服务事件监听接口
*/
@ -771,6 +774,9 @@ object AgentService : CoroutineScope {
// 通知ChatAPI服务终止当前流式请求
chatApiService.cancelCurrentStream()
//结束等待音
audioPlayer?.stopAudio()
} catch (e: Exception) {
Log.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常
@ -873,7 +879,7 @@ object AgentService : CoroutineScope {
// 设置状态为正在流式输出
_isAiStreaming.set(true)
Log.d(TAG, "processWithChatApiInternal")
audioPlayer?.playAudio(R.raw.await, true, 0.3f)
audioPlayer?.playAudio(R.raw.await, true, 0.3f,true)
// 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder()
var aiMetadata: String = ""
@ -970,8 +976,6 @@ object AgentService : CoroutineScope {
responseBuilder.append(token)
if (speakResponse && reply && broadcast && token.isNotEmpty()) {
ttsService?.speakStream(token)
// 在开始流式TTS时立即停止气泡音
audioPlayer?.stopAudio()
}
if (reply && token.isNotEmpty()) {
// 发送流式回复token
@ -1584,6 +1588,11 @@ object AgentService : CoroutineScope {
private var mediaPlayer: MediaPlayer? = null
private var isInitialized = false
private val playbackLock = Object() // 添加同步锁
// 新增:超时相关属性
private var timeoutHandler: Handler? = null
private var timeoutRunnable: Runnable? = null
private var timeoutSeconds: Long = 30000L // 默认10秒超时
/**
* 安全检查 MediaPlayer 是否正在播放
@ -1601,17 +1610,21 @@ object AgentService : CoroutineScope {
}
/**
* 播放音频资源
* 播放音频资源(带超时控制)
* @param resId 资源ID
* @param isLooping 是否循环播放
* @param volume 音量大小,范围0.0-1.0,默认1.0
* @param enableTimeout 是否启用超时自动停止,默认false
*/
fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f) {
fun playAudio(resId: Int, isLooping: Boolean = false, volume: Float = 1.0f, enableTimeout: Boolean = false) {
synchronized(playbackLock) {
try {
Log.d(TAG, "playAudio${isLooping}")
Log.d(TAG, "playAudio isLooping=$isLooping, enableTimeout=$enableTimeout")
// 强制停止并释放之前的资源
forceStop()
// 停止之前的超时定时器
stopTimeout()
// 创建播放器并设置资源
mediaPlayer = MediaPlayer().apply {
@ -1627,11 +1640,14 @@ object AgentService : CoroutineScope {
if (!isLooping) {
release()
}
// 播放完成时停止超时定时器
stopTimeout()
}
setOnErrorListener { _, what, extra ->
Log.e(TAG, "MediaPlayer 错误: what=$what, extra=$extra")
forceRelease()
stopTimeout()
true // 返回true表示错误已处理
}
@ -1640,9 +1656,15 @@ object AgentService : CoroutineScope {
Log.d(TAG, "开始播放音频资源: $resId, 循环: $isLooping")
}
isInitialized = true
// 如果启用超时且是循环播放,启动超时定时器
if (enableTimeout && isLooping) {
startTimeout()
}
} catch (e: Exception) {
Log.e(TAG, "播放音频资源异常: ${e.message}", e)
forceRelease()
stopTimeout()
}
}
}
@ -1654,6 +1676,9 @@ object AgentService : CoroutineScope {
synchronized(playbackLock) {
try {
Log.d(TAG, "stopAudio")
// 停止超时定时器
stopTimeout()
mediaPlayer?.apply {
if (isMediaPlayerPlaying()) {
stop()
@ -1672,6 +1697,42 @@ object AgentService : CoroutineScope {
}
}
/**
* 启动超时定时器
*/
private fun startTimeout() {
stopTimeout() // 先停止之前的定时器
timeoutHandler = Handler(Looper.getMainLooper())
timeoutRunnable = Runnable {
Log.d(TAG, "音频播放超时,自动停止 - 超时时间: ${timeoutSeconds}ms")
stopAudio()
}
timeoutHandler?.postDelayed(timeoutRunnable!!, timeoutSeconds)
Log.d(TAG, "启动音频播放超时定时器,超时时间: ${timeoutSeconds}ms")
}
/**
* 停止超时定时器
*/
private fun stopTimeout() {
timeoutRunnable?.let { runnable ->
timeoutHandler?.removeCallbacks(runnable)
}
timeoutHandler = null
timeoutRunnable = null
}
/**
* 设置超时时间
* @param seconds 超时秒数,最小2秒
*/
fun setTimeoutSeconds(seconds: Long) {
timeoutSeconds = maxOf(2000L, seconds * 1000L) // 最小2秒,转换为毫秒
Log.d(TAG, "设置音频播放超时时间: ${timeoutSeconds}ms")
}
/**
* 强制停止播放(不释放资源)
*/
@ -1703,6 +1764,9 @@ object AgentService : CoroutineScope {
fun release() {
synchronized(playbackLock) {
try {
// 停止超时定时器
stopTimeout()
mediaPlayer?.apply {
// 安全停止播放
if (isMediaPlayerPlaying()) {
@ -1733,6 +1797,7 @@ object AgentService : CoroutineScope {
// 确保变量被清理
mediaPlayer = null
isInitialized = false
stopTimeout()
}
}
}
@ -1743,6 +1808,9 @@ object AgentService : CoroutineScope {
private fun forceRelease() {
synchronized(playbackLock) {
try {
// 停止超时定时器
stopTimeout()
mediaPlayer?.apply {
try {
reset()
@ -1760,6 +1828,7 @@ object AgentService : CoroutineScope {
} finally {
mediaPlayer = null
isInitialized = false
stopTimeout()
}
}
}

3
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -486,7 +486,7 @@ audioStream.saveAudioDataTo(data: audioData)
if interrupted {
sendEvent(name: "response_interrupted", data: ["status": "interrupted"])
}
audioPlayer?.stopAwaitSound()
return interrupted
}
@ -1256,7 +1256,6 @@ class ChatApiStreamCallback: StreamCallback {
if speakResponse && reply && broadcast{
try agentService.azureTtsHelper?.speakStream(token)
// 在开始流式TTS时立即停止气泡音
// try agentService.audioPlayer?.stopAwaitSound()
}
if (reply && token != "") {
try agentService.sendEvent(name: "assistant_token", data: ["token": token])

203
local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift

@ -447,14 +447,14 @@ private class GetCurrentLocationHandler: FunctionHandler {
return await withCheckedContinuation { continuation in
// 设置超时
let timeoutTask = Task {
try await Task.sleep(seconds: 15) // 15秒
try await Task.sleep(nanoseconds: 15_000_000_000) // 15秒 (15 * 1_000_000_000 nanoseconds)
continuation.resume(returning: self.createErrorResponse(message: "位置获取超时,请检查网络连接后重试"))
}
// 检查位置服务是否可用
guard CLLocationManager.locationServicesEnabled() else {
locationCompletion?(Result(success: false, message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务"))
locationCompletion = nil
timeoutTask.cancel()
continuation.resume(returning: self.createErrorResponse(message: "位置服务未启用。请前往 设置 > 隐私与安全 > 定位服务 启用定位服务"))
return
}
@ -729,85 +729,37 @@ private class SetReminderHandler: FunctionHandler {
}
private func setReminder(title: String, content: String, time: String) async -> Result {
Log.d(TAG, "设置提醒: $title, 时间: $timeStr")
guard let eventStore = eventStore else {
return Result(success: false, message: "事件存储未初始化")
}
try {
// 解析时间字符串
val time = try {
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault())
dateFormat.parse(timeStr)?.time ?: System.currentTimeMillis()
} catch (e: Exception) {
Log.e(TAG, "解析时间失败: $timeStr, ${e.message}")
return Result(
success = false,
message = "提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式"
)
}
// 检查日历权限
if (!checkPermissionAndGuide(Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) {
return Result(
success = false,
message = "需要日历权限才能创建提醒,请在权限管理中开启日历权限"
)
}
// 直接使用ContentProvider写入日历
val endTime = time + 3600000 // 默认1小时后结束
// 获取默认日历ID
val calendarId = getDefaultCalendarId()
if (calendarId == -1L) {
Log.w(TAG, "未找到默认日历")
return tryCreateAlarm(title, content, timeStr)
}
// 创建事件
val eventValues = ContentValues().apply {
put(CalendarContract.Events.CALENDAR_ID, calendarId)
put(CalendarContract.Events.TITLE, title)
put(CalendarContract.Events.DESCRIPTION, content)
put(CalendarContract.Events.DTSTART, time)
put(CalendarContract.Events.DTEND, endTime)
put(CalendarContract.Events.HAS_ALARM, 1)
put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id)
}
val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues)
if (eventUri == null) {
Log.e(TAG, "插入日历事件失败")
return tryCreateAlarm(title, content, timeStr)
}
// 获取事件ID
val eventId = ContentUris.parseId(eventUri)
// 添加提醒(15分钟前提醒)
val reminderValues = ContentValues().apply {
put(CalendarContract.Reminders.EVENT_ID, eventId)
put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒
}
val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues)
if (reminderUri != null) {
return Result(
success = true,
message = "已成功创建提醒: $title, 时间: $timeStr"
)
} else {
// 仍然创建了事件,但没有提醒
return Result(
success = true,
message = "已创建日历事件,但未设置提醒: $title"
)
}
} catch (e: Exception) {
Log.e(TAG, "设置提醒失败: ${e.message}", e)
return Result(
success = false,
message = "设置提醒失败: ${e.message}"
)
// 请求日历权限
let status = await requestCalendarAccess()
guard status == .authorized else {
return Result(success: false, message: "日历权限被拒绝")
}
// 解析时间
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
guard let date = formatter.date(from: time) else {
return Result(success: false, message: "时间格式错误")
}
// 创建提醒
let reminder = EKReminder(eventStore: eventStore)
reminder.title = title
reminder.notes = content
reminder.calendar = eventStore.defaultCalendarForNewReminders()
let alarm = EKAlarm(absoluteDate: date)
reminder.addAlarm(alarm)
do {
try eventStore.save(reminder, commit: true)
return Result(success: true, message: "提醒设置成功")
} catch {
return Result(success: false, message: "设置提醒失败:\(error.localizedDescription)")
}
}
@ -822,95 +774,6 @@ private class SetReminderHandler: FunctionHandler {
}
// /**
// * 设置提醒
// * @param title 提醒标题
// * @param content 提醒内容(可选)
// * @param timeStr 提醒时间,格式为"yyyy-MM-dd HH:mm"
// * @return 操作结果
// */
// fun setReminder(title: String, content: String, timeStr: String): Result {
// Log.d(TAG, "设置提醒: $title, 时间: $timeStr")
// try {
// // 解析时间字符串
// val time = try {
// val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm", java.util.Locale.getDefault())
// dateFormat.parse(timeStr)?.time ?: System.currentTimeMillis()
// } catch (e: Exception) {
// Log.e(TAG, "解析时间失败: $timeStr, ${e.message}")
// return Result(
// success = false,
// message = "提醒时间格式无效,请使用'yyyy-MM-dd HH:mm'格式"
// )
// }
// // 检查日历权限
// if (!checkPermissionAndGuide(Manifest.permission.WRITE_CALENDAR, "日历写入", "设置提醒")) {
// return Result(
// success = false,
// message = "需要日历权限才能创建提醒,请在权限管理中开启日历权限"
// )
// }
// // 直接使用ContentProvider写入日历
// val endTime = time + 3600000 // 默认1小时后结束
// // 获取默认日历ID
// val calendarId = getDefaultCalendarId()
// if (calendarId == -1L) {
// Log.w(TAG, "未找到默认日历")
// return tryCreateAlarm(title, content, timeStr)
// }
// // 创建事件
// val eventValues = ContentValues().apply {
// put(CalendarContract.Events.CALENDAR_ID, calendarId)
// put(CalendarContract.Events.TITLE, title)
// put(CalendarContract.Events.DESCRIPTION, content)
// put(CalendarContract.Events.DTSTART, time)
// put(CalendarContract.Events.DTEND, endTime)
// put(CalendarContract.Events.HAS_ALARM, 1)
// put(CalendarContract.Events.EVENT_TIMEZONE, java.util.TimeZone.getDefault().id)
// }
// val eventUri = context.contentResolver.insert(CalendarContract.Events.CONTENT_URI, eventValues)
// if (eventUri == null) {
// Log.e(TAG, "插入日历事件失败")
// return tryCreateAlarm(title, content, timeStr)
// }
// // 获取事件ID
// val eventId = ContentUris.parseId(eventUri)
// // 添加提醒(15分钟前提醒)
// val reminderValues = ContentValues().apply {
// put(CalendarContract.Reminders.EVENT_ID, eventId)
// put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT)
// put(CalendarContract.Reminders.MINUTES, 15) // 提前15分钟提醒
// }
// val reminderUri = context.contentResolver.insert(CalendarContract.Reminders.CONTENT_URI, reminderValues)
// if (reminderUri != null) {
// return Result(
// success = true,
// message = "已成功创建提醒: $title, 时间: $timeStr"
// )
// } else {
// // 仍然创建了事件,但没有提醒
// return Result(
// success = true,
// message = "已创建日历事件,但未设置提醒: $title"
// )
// }
// } catch (e: Exception) {
// Log.e(TAG, "设置提醒失败: ${e.message}", e)
// return Result(
// success = false,
// message = "设置提醒失败: ${e.message}"
// )
// }
// }
/// 获取当前时间处理器
private class GetCurrentTimeHandler: FunctionHandler {

Loading…
Cancel
Save