Browse Source

add

newdev_shunjiawei
wolfplus2048 1 year ago
parent
commit
38c7f8ff4c
  1. 34
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  2. 342
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt
  3. 177
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt
  4. 3
      local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt
  5. 8
      local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt

34
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -169,6 +169,40 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
audioDataListeners.remove(listener)
}
/**
* 设置是否使用内部播放器
*
* @param useInternalPlayer true: 使用内部播放器自动播放音频
* false: 仅通过音频数据监听器输出数据,不播放
*/
override fun setUseInternalPlayer(useInternalPlayer: Boolean) {
// Azure TTS 通过 AudioConfig 控制播放器
// 如果不使用内部播放器,需要重新配置 synthesizer
if (!useInternalPlayer && customAudioOutputStream == null) {
// 创建自定义音频输出流以便捕获音频数据
customAudioOutputStream = SimpleAudioPlayer(context).getAudioOutputStream()
// 如果已经初始化,需要重新创建 synthesizer
if (isInitialized && speechConfig != null) {
synthesizer?.close()
val audioConfig = AudioConfig.fromStreamOutput(customAudioOutputStream)
synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
setupEventListeners()
}
} else if (useInternalPlayer && customAudioOutputStream != null) {
// 切换回使用内部播放器
customAudioOutputStream = null
// 如果已经初始化,需要重新创建 synthesizer
if (isInitialized && speechConfig != null) {
synthesizer?.close()
val audioConfig = AudioConfig.fromDefaultSpeakerOutput()
synthesizer = SpeechSynthesizer(speechConfig, audioConfig)
setupEventListeners()
}
}
}
/**
* 触发事件通知
*/

342
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt

@ -7,112 +7,134 @@ import android.media.AudioTrack
import android.os.Build
import android.util.Log
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.CancellationException
/**
* 字节跳动语音合成音频播放器
*
* 使用AudioTrack播放PCM格式音频流,基于Kotlin协程实现异步处理
* 简化的音频流播放器
* 基于位置标记触发播放完成回调
*/
class BytedanceAudioPlayer : AudioDataListener, CoroutineScope {
class BytedanceAudioPlayer : AudioDataListener {
companion object {
private const val TAG = "BytedanceAudioPlayer"
private const val SAMPLE_RATE = 24000 // 样本率
private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO // 单声道
private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT // 16位PCM
private const val IDLE_TIMEOUT_MS = 800L // 无数据超时时间
private const val SAMPLE_RATE = 24000
private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO
private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
}
// 协程相关
private val job = SupervisorJob()
override val coroutineContext = Dispatchers.IO + job
// 音频处理相关
private var audioTrack: AudioTrack? = null
private val isPlaying = AtomicBoolean(false)
private val isPaused = AtomicBoolean(false)
// 播放状态
private val playStarted = AtomicBoolean(false)
private val completionReported = AtomicBoolean(false)
private var isFirstData = true // 是否是第一次接收数据
private var totalBytesWritten = 0 // 总共写入的字节数
private var sessionActive = true // 会话是否活跃
// 使用简单的并发队列,保证线程安全
private val audioDataQueue = ConcurrentLinkedQueue<ByteArray>()
// 播放状态监听
// 回调
private var onPlayStarted: (() -> Unit)? = null
private var onPlayCompleted: (() -> Unit)? = null
private var onError: ((String) -> Unit)? = null
// 处理协程
private var processingJob: Job? = null
private var lastDataTime = 0L
// 播放位置监听器
private val playbackListener = object : AudioTrack.OnPlaybackPositionUpdateListener {
override fun onMarkerReached(track: AudioTrack) {
Log.d(TAG, "播放到达标记位置: ${track.playbackHeadPosition}")
onPlayCompleted?.invoke()
}
override fun onPeriodicNotification(track: AudioTrack) {
// 不使用周期性通知
}
}
init {
Log.d(TAG, "BytedanceAudioPlayer初始化")
/**
* 开始新的播放会话
*/
fun startSession() {
Log.d(TAG, "开始新会话")
// 重置状态
isFirstData = true
totalBytesWritten = 0
sessionActive = true
// 重置 AudioTrack
audioTrack?.let { track ->
if (track.state == AudioTrack.STATE_INITIALIZED) {
// 停止并清空缓冲区
track.pause()
track.flush()
// 重新开始播放
track.play()
Log.d(TAG, "AudioTrack 已重置")
}
} ?: initAudioTrack() // 如果没有初始化,则初始化
}
/**
* 开始播放
* 标记数据流结束
*/
fun start() {
if (isPlaying.getAndSet(true)) return
fun endSession() {
if (!sessionActive) return
isPaused.set(false)
playStarted.set(false)
completionReported.set(false)
lastDataTime = System.currentTimeMillis()
Log.d(TAG, "数据流结束,总共写入字节数: $totalBytesWritten")
sessionActive = false
processingJob = launch {
try {
initAudioTrack()
audioTrack?.let { track ->
if (totalBytesWritten > 0) {
// 计算总帧数(16-bit 单声道,每帧2字节)
val totalFrames = totalBytesWritten / 2
// 处理队列中的音频数据
while (isActive && isPlaying.get()) {
processQueuedAudio()
// 检查是否播放完成
checkPlaybackCompletion()
// 队列为空时短暂延迟
if (audioDataQueue.isEmpty()) {
delay(10)
}
// 设置标记位置
try {
track.setNotificationMarkerPosition(totalFrames)
Log.d(TAG, "设置播放完成标记位置: $totalFrames")
} catch (e: Exception) {
Log.e(TAG, "设置标记失败: ${e.message}")
// 设置失败时,使用延迟触发作为后备
val durationMs = (totalFrames * 1000L) / SAMPLE_RATE
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
onPlayCompleted?.invoke()
}, durationMs + 500)
}
} catch (e: CancellationException) {
// 协程被取消,正常行为
} catch (e: Exception) {
Log.e(TAG, "播放错误: ${e.message}")
onError?.invoke("播放错误: ${e.message}")
stopInternal(false)
} else {
// 没有数据,直接触发完成
onPlayCompleted?.invoke()
}
}
}
/**
* 暂停播放
* 接收音频数据
*/
fun pause() {
if (!isPlaying.get() || isPaused.getAndSet(true)) return
audioTrack?.pause()
}
/**
* 恢复播放
*/
fun resume() {
if (!isPlaying.get() || !isPaused.getAndSet(false)) return
audioTrack?.play()
override fun onAudioData(data: ByteArray) {
if (data.isEmpty()) return
if (!sessionActive) {
Log.w(TAG, "收到音频数据但会话未激活,忽略数据")
return
}
if (audioTrack == null || audioTrack?.state != AudioTrack.STATE_INITIALIZED) {
Log.w(TAG, "收到音频数据但 AudioTrack 未初始化或状态不正确,忽略数据")
return
}
try {
audioTrack?.let { track ->
if (track.state == AudioTrack.STATE_INITIALIZED) {
val bytesWritten = track.write(data, 0, data.size)
if (bytesWritten > 0) {
// 第一次写入数据时自动触发开始回调
if (isFirstData) {
isFirstData = false
onPlayStarted?.invoke()
Log.d(TAG, "播放开始")
}
// 累计写入字节数
if (sessionActive) {
totalBytesWritten += bytesWritten
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "写入音频数据失败: ${e.message}")
}
}
/**
@ -120,44 +142,32 @@ class BytedanceAudioPlayer : AudioDataListener, CoroutineScope {
*/
fun stop() {
Log.d(TAG, "停止播放")
stopInternal(true)
}
/**
* 内部停止处理
*/
private fun stopInternal(reportCompletion: Boolean) {
if (!isPlaying.getAndSet(false)) return
isPaused.set(false)
processingJob?.cancel()
audioTrack?.stop()
// 不在stopInternal中释放资源,只停止播放
// 清空队列
audioDataQueue.clear()
sessionActive = false
// 播放完成通知
if (reportCompletion && playStarted.get() && !completionReported.getAndSet(true)) {
launch(Dispatchers.Main) {
onPlayCompleted?.invoke()
audioTrack?.let { track ->
if (track.state == AudioTrack.STATE_INITIALIZED) {
track.pause()
track.flush()
}
}
// 如果已经开始播放,触发完成回调
if (!isFirstData) {
onPlayCompleted?.invoke()
}
}
/**
* 释放资源
*/
fun release() {
stopInternal(false)
Log.d(TAG, "释放资源")
stop()
// 在release方法中释放AudioTrack资源
audioTrack?.release()
audioTrack = null
// 释放协程资源
job.cancel()
}
/**
@ -175,120 +185,32 @@ class BytedanceAudioPlayer : AudioDataListener, CoroutineScope {
}
/**
* 设置错误回调
* 设置错误回调(兼容接口)
*/
fun setOnError(listener: (String) -> Unit) {
onError = listener
}
/**
* 接收音频数据(实现AudioDataListener接口)
*/
override fun onAudioData(data: ByteArray) {
if (data.isEmpty()) return
lastDataTime = System.currentTimeMillis()
if (!isPlaying.get()) {
start()
}
// 添加到队列 - 简单有效,保证FIFO顺序
audioDataQueue.add(data.copyOf())
// 不实现,仅为兼容
}
/**
* 检查是否正在播放
*/
fun isPlaying(): Boolean = isPlaying.get() && !isPaused.get()
/**
* 处理队列中的音频数据
*/
private suspend fun processQueuedAudio() {
if (isPaused.get() || !isPlaying.get() || !isActive) return
// 获取并处理队列中的数据
val data = audioDataQueue.poll() ?: return
// 写入音频数据
val result = audioTrack?.write(data, 0, data.size) ?: 0
if (result > 0) {
// 标记播放开始
if (!playStarted.getAndSet(true)) {
withContext(Dispatchers.Main) {
onPlayStarted?.invoke()
}
}
} else if (result < 0) {
// 处理错误
handleAudioTrackError(result)
}
fun isPlaying(): Boolean {
return audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING
}
/**
* 检查播放是否完成(超时无数据)
*/
private suspend fun checkPlaybackCompletion() {
val currentTime = System.currentTimeMillis()
// 超时判断 - 队列为空且超过超时时间
if (playStarted.get() && audioDataQueue.isEmpty() &&
currentTime - lastDataTime > IDLE_TIMEOUT_MS && isPlaying.get()) {
// 播放完成
if (!completionReported.getAndSet(true)) {
withContext(Dispatchers.Main) {
onPlayCompleted?.invoke()
}
stopInternal(false) // 已报告,不需要再次报告
}
}
}
/**
* 处理AudioTrack错误
*/
private fun handleAudioTrackError(result: Int) {
val errorMsg = when(result) {
AudioTrack.ERROR_INVALID_OPERATION -> "无效操作"
AudioTrack.ERROR_BAD_VALUE -> "参数错误"
AudioTrack.ERROR_DEAD_OBJECT -> "对象已销毁"
else -> "未知错误"
}
// 对象已销毁时尝试重建
if (result == AudioTrack.ERROR_DEAD_OBJECT) {
try {
audioTrack?.release()
initAudioTrack()
} catch (e: Exception) {
Log.e(TAG, "重建AudioTrack失败: ${e.message}")
throw e
}
} else {
Log.e(TAG, "AudioTrack错误: $errorMsg")
}
}
/**
* 初始化AudioTrack
* 初始化 AudioTrack
*/
private fun initAudioTrack() {
val minBufferSize = AudioTrack.getMinBufferSize(
SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT
)
if (minBufferSize == AudioTrack.ERROR || minBufferSize == AudioTrack.ERROR_BAD_VALUE) {
throw IllegalStateException("无法获取有效的音频缓冲区大小")
}
// 释放旧实例
audioTrack?.release()
// 使用较大的缓冲区提高稳定性
val bufferSize = minBufferSize * 4
// 计算缓冲区大小
val minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT)
val bufferSize = minBufferSize * 2
// 创建AudioTrack
audioTrack = if (Build.VERSION.SDK_INT >= 23) { // Android M (6.0)
// 创建 AudioTrack
audioTrack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
@ -318,12 +240,12 @@ class BytedanceAudioPlayer : AudioDataListener, CoroutineScope {
)
}
// 检查初始化状态
if (audioTrack?.state != AudioTrack.STATE_INITIALIZED) {
throw IllegalStateException("AudioTrack初始化失败")
}
// 设置播放位置监听器
audioTrack?.setPlaybackPositionUpdateListener(playbackListener)
// 开始播放
audioTrack?.play()
Log.d(TAG, "AudioTrack 初始化成功")
}
}
}

177
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt

@ -100,6 +100,9 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
// 内部音频播放器
private val audioPlayer = BytedanceAudioPlayer()
// 是否使用内部播放器
private var useInternalPlayer = true
// WebSocket连接
private var webSocket: WebSocket? = null
private val client: OkHttpClient
@ -119,9 +122,7 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
private var language: String = "zh-CN"
// 流式处理状态
private var isStreamMode = false
private var currentStatus = STATUS_STOPPED
private var isSpeaking = false
// 连接相关状态
private var connectionAttempts = 0
@ -155,12 +156,8 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
* 初始化音频播放器回调
*/
private fun initAudioPlayerCallbacks() {
// 添加播放器作为音频数据监听器
addAudioDataListener(audioPlayer)
audioPlayer.setOnPlayStarted {
Log.d(TAG, "播放开始")
isSpeaking = true
updateStatus(STATUS_SPEAKING)
notifyEvent(TtsEventType.SYNTHESIS_STARTED)
}
@ -168,17 +165,18 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
// 设置播放完成回调
audioPlayer.setOnPlayCompleted {
Log.d(TAG, "播放结束")
isSpeaking = false
updateStatus(if (isStreamMode) STATUS_READY else STATUS_STOPPED)
updateStatus(STATUS_READY)
notifyEvent(TtsEventType.SYNTHESIS_COMPLETED)
}
// 设置错误回调
audioPlayer.setOnError { errorMsg ->
isSpeaking = false
updateStatus(STATUS_ERROR)
notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "PLAYER_ERROR", "errorMessage" to errorMsg))
}
// 根据设置决定是否使用内部播放器
updateInternalPlayerUsage()
}
/**
@ -208,16 +206,19 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
*/
override fun stop(): Boolean {
try {
audioPlayer.stop()
Log.i(TAG, ">stop()")
if (useInternalPlayer) {
audioPlayer.stop()
}
textProcessingJob?.cancel()
launch {
if (isSessionStarted) {
finishSession()
}
isSpeaking = false // 显式设置,因为这是强制停止
updateStatus(STATUS_STOPPED)
if (isSessionStarted) {
finishSession()
}
updateStatus(STATUS_STOPPED)
return true
} catch (e: Exception) {
Log.e(TAG, "停止合成失败: ${e.message}")
@ -277,7 +278,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
isConnected = false
isSessionStarted = false
isSpeaking = false
updateStatus(STATUS_STOPPED)
startTextProcessing()
@ -304,29 +304,9 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
* 单次播放(非流式)
*/
override fun speakOnce(text: String): Boolean {
try {
if (text.isEmpty()) {
return false
}
isStreamMode = false
launch {
if (!ensureSessionReady()) {
notifyEvent(TtsEventType.ERROR, mapOf("errorCode" to "SESSION_ERROR",
"errorMessage" to "无法建立会话"))
return@launch
}
sendTextRequest(text)
}
return true
} catch (e: Exception) {
Log.e(TAG, "语音合成失败: ${e.message}")
updateStatus(STATUS_ERROR)
return false
}
// 未实现:只支持流式模式
Log.w(TAG, "speakOnce未实现,请使用speakStream")
return false
}
/**
@ -337,7 +317,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
if (text.isEmpty()) {
return true
}
isStreamMode = true
// 确保会话已就绪
if (!isSessionStarted && !ensureSessionReady()) {
@ -365,17 +344,11 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
*/
override fun flushStream(): Boolean {
try {
if (!isStreamMode) return true
// Log.d(TAG, "flushStream: $sessionId")
launch {
withContext(Dispatchers.IO) {
delay(300) // 确保现有文本处理完成
}
if (isSessionStarted) {
isSessionStarted = false
finishSession()
}
if (isSessionStarted) {
isSessionStarted = false
finishSession()
}
return true
@ -440,6 +413,29 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
audioDataListeners.remove(listener)
}
/**
* 设置是否使用内部播放器
*/
override fun setUseInternalPlayer(useInternalPlayer: Boolean) {
this.useInternalPlayer = useInternalPlayer
updateInternalPlayerUsage()
}
/**
* 更新内部播放器的使用状态
*/
private fun updateInternalPlayerUsage() {
if (useInternalPlayer) {
// 使用内部播放器
if (!audioDataListeners.contains(audioPlayer)) {
audioDataListeners.add(audioPlayer)
}
} else {
// 不使用内部播放器
audioDataListeners.remove(audioPlayer)
}
}
/**
* 通知事件处理
*/
@ -461,15 +457,14 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
* 通知音频数据
*/
private fun notifyAudioData(data: ByteArray) {
launch(Dispatchers.Main) {
for (listener in audioDataListeners) {
try {
listener.onAudioData(data)
} catch (e: Exception) {
Log.e(TAG, "音频数据回调异常: ${e.message}")
}
for (listener in audioDataListeners) {
try {
listener.onAudioData(data)
} catch (e: Exception) {
Log.e(TAG, "音频数据回调异常: ${e.message}")
}
}
}
/**
@ -483,7 +478,7 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
// 如果已连接但会话未开始,只需要开始会话
if (isConnected && !isSessionStarted) {
Log.d(TAG, "WebSocket已连接,正在启动新会话...")
// Log.d(TAG, "WebSocket已连接,正在启动新会话...")
return startSessionOnly()
}
@ -648,7 +643,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
"errorMessage" to errorMsg))
webSocket.close(1000, "Error")
updateStatus(STATUS_ERROR)
isSpeaking = false // 错误情况下显式设置
isSessionStarted = false
isConnected = false
isConnecting.set(false)
@ -656,6 +650,10 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
EVENT_SESSION_STARTED -> {
connectionAttempts = 0
// 会话开始时,如果使用内部播放器则启动播放器会话
if (useInternalPlayer) {
audioPlayer.startSession()
}
}
EVENT_TTS_RESPONSE -> {
@ -673,16 +671,18 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
EVENT_SESSION_FINISHED -> {
isSessionStarted = false
if (!isStreamMode) {
finishConnection(webSocket)
Log.i(TAG, "EVENT_SESSION_FINISHED, ${sessionId}")
// 如果使用内部播放器则结束播放器会话
if (useInternalPlayer) {
audioPlayer.endSession()
}
// 流式模式下不自动关闭连接
}
}
} catch (e: Exception) {
Log.e(TAG, "解析响应失败: ${e.message}")
updateStatus(STATUS_ERROR)
isSpeaking = false // 错误情况下显式设置
isConnecting.set(false)
}
}
@ -690,7 +690,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
isConnected = false
isSessionStarted = false
isSpeaking = false
isConnecting.set(false)
Log.e(TAG, "WebSocket连接失败: ${t.message}")
@ -702,7 +701,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
isConnected = false
isSessionStarted = false
isSpeaking = false
isConnecting.set(false)
updateStatus(STATUS_STOPPED)
notifyEvent(TtsEventType.SYNTHESIS_CANCELED)
@ -798,53 +796,6 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
sendEvent(webSocket, header, optional, payload)
}
/**
* 发送文本进行合成 (非流式)
*/
private fun sendTextRequest(text: String) {
webSocket?.let { ws ->
val header = Header(
protocolVersion = PROTOCOL_VERSION,
headerSize = DEFAULT_HEADER_SIZE,
messageType = FULL_CLIENT_REQUEST,
messageTypeSpecificFlags = MSG_TYPE_FLAG_WITH_EVENT,
serializationMethod = JSON,
messageCompression = COMPRESSION_NO,
reserved = 0
)
val optional = Optional(
event = EVENT_TASK_REQUEST,
sessionId = sessionId
)
val jsonObject = JSONObject()
val user = JSONObject()
user.put("uid", "123456")
jsonObject.put("user", user)
jsonObject.put("event", EVENT_TASK_REQUEST)
jsonObject.put("namespace", "BidirectionalTTS")
val reqParams = JSONObject()
// 将#和*替换为空格
val processedText = text.replace("#", " ").replace("*", " ")
reqParams.put("text", processedText)
reqParams.put("speaker", speaker)
val audioParams = JSONObject()
audioParams.put("format", format)
audioParams.put("sample_rate", sampleRate)
audioParams.put("emotion", "happy")
reqParams.put("audio_params", audioParams)
jsonObject.put("req_params", reqParams)
val payload = jsonObject.toString().toByteArray()
Log.d(TAG, "发送文本合成请求: ${jsonObject.toString()}")
sendEvent(ws, header, optional, payload)
}
}
/**
* 发送文本进行合成 (流式)

3
local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/ChatApiService.kt

@ -157,7 +157,8 @@ class ChatApiService(private val context: android.content.Context? = null) : Cor
currentStreamJob = null
// 2. 通知旧会话被中止
currSessionCallback?.onError(ChatApiException("Session aborted by new request"))
// currSessionCallback?.onError(ChatApiException("Session aborted by new request"))
currSessionCallback?.onComplete() // 直接完成当前会话
// 3. 清理状态
currSessionId = ""

8
local_plugins/speech/android/src/main/kotlin/com/deep_voice/speech/ITtsService.kt

@ -89,4 +89,12 @@ interface ITtsService {
* @param listener 要移除的音频数据监听器
*/
fun removeAudioDataListener(listener: AudioDataListener)
/**
* 设置是否使用内部播放器
*
* @param useInternalPlayer true: 使用内部播放器自动播放音频
* false: 仅通过音频数据监听器输出数据,不播放
*/
fun setUseInternalPlayer(useInternalPlayer: Boolean)
}
Loading…
Cancel
Save