yzx 16 jam lalu
induk
melakukan
81b258827e

+ 75 - 21
src/main/java/com/telerobot/fs/acd/CallHandler.java

@@ -9,11 +9,14 @@ import com.telerobot.fs.config.SystemConfig;
 import com.telerobot.fs.entity.bo.ChanneState;
 import com.telerobot.fs.entity.bo.ChannelFlag;
 import com.telerobot.fs.entity.bo.InboundDetail;
+import com.telerobot.fs.entity.dao.CallTaskEntity;
 import com.telerobot.fs.entity.dto.CallMonitorInfo;
 import com.telerobot.fs.entity.po.CdrDetail;
 import com.telerobot.fs.entity.pojo.AgentStatus;
+import com.telerobot.fs.entity.pojo.TtsProvider;
 import com.telerobot.fs.global.CdrPush;
 import com.telerobot.fs.ivr.IvrEngine;
+import com.telerobot.fs.robot.TransferToAgent;
 import com.telerobot.fs.service.AsrResultListener;
 import com.telerobot.fs.service.SysService;
 import com.telerobot.fs.utils.*;
@@ -135,6 +138,10 @@ public class CallHandler {
 	private Semaphore playBackStartSignal = new Semaphore(0);
 	private Semaphore playBackStoppedSignal = new Semaphore(0);
 	public void playWaitMusic(){
+		if (!shouldPlayWaitMusicForTransfer()) {
+			log.info("{} skip wait music before manual answer for xfyun acd transfer flow.", inboundDetail.getUuid());
+			return;
+		}
 		log.info("{} playWaitMusic for call session. ", inboundDetail.getUuid());
 		EslConnectionUtil.sendExecuteCommand(
 				"playback",
@@ -310,33 +317,41 @@ public class CallHandler {
 			long waitMills = (System.currentTimeMillis() - task.lastPlayNoFreeAgentTime);
 			long interval  = acdPlayQueueNumInterval *  1000;
 			if (notAnsweredAndNotHangup && waitMills >= interval && !task.transferring) {
-				StringBuilder sbTips = new StringBuilder();
-				String tips = getQueueNumTips(task);
-                if(!StringUtils.isNullOrEmpty(tips)){
-					sbTips.append("file_string://");
-					sbTips.append(tips);
-				}else{
-                     sbTips.append("$${sounds_dir}/ivr/noFreeAgent.wav");
-				}
+				if (task.shouldPlayWaitMusicForTransfer()) {
+					StringBuilder sbTips = new StringBuilder();
+					String tips = getQueueNumTips(task);
+	                if(!StringUtils.isNullOrEmpty(tips)){
+						sbTips.append("file_string://");
+						sbTips.append(tips);
+					}else{
+	                     sbTips.append("$${sounds_dir}/ivr/noFreeAgent.wav");
+					}
 
-				EslConnectionUtil.sendExecuteCommand(
-						"playback",
-						sbTips.toString(),
-						task.uuid,
-						task.eslConnectionPool
-				);
+					EslConnectionUtil.sendExecuteCommand(
+							"playback",
+							sbTips.toString(),
+							task.uuid,
+							task.eslConnectionPool
+					);
+				} else {
+					log.info("{} skip queue tips/noFreeAgent wait prompts before manual answer for xfyun acd transfer flow.", task.uuid);
+				}
 				task.lastPlayNoFreeAgentTime = System.currentTimeMillis();
 				continue;
 			}
 
 			if (notAnsweredAndNotHangup && !task.keepMusicPlayed ) {
-				task.playWaitMusic();
-				EslConnectionUtil.sendExecuteCommand(
-						"playback",
-						"$${sounds_dir}/ivr/pleaseWait.wav",
-						 task.uuid,
-					 	 task.eslConnectionPool
-				);
+				if (task.shouldPlayWaitMusicForTransfer()) {
+					task.playWaitMusic();
+					EslConnectionUtil.sendExecuteCommand(
+							"playback",
+							"$${sounds_dir}/ivr/pleaseWait.wav",
+							 task.uuid,
+						 	 task.eslConnectionPool
+					);
+				} else {
+					log.info("{} skip keep.wav and pleaseWait.wav before manual answer for xfyun acd transfer flow.", task.uuid);
+				}
 				task.keepMusicPlayed = true;
 			}
 		}
@@ -691,6 +706,7 @@ public class CallHandler {
 					inboundDetail.setManualAnsweredTime(System.currentTimeMillis());
 					inboundDetail.setTransferredSucceed(true);
 
+					stopRobotMediaAfterManualAnswer();
 					breakWaitMusic();
 					log.info("{} resetAgentBusyLockTime. uerId={}, extNum={}",
 							uuid,
@@ -704,6 +720,9 @@ public class CallHandler {
 		}
 
 		private void breakWaitMusic(){
+			if (!shouldPlayWaitMusicForTransfer()) {
+				return;
+			}
 			boolean getAllowSignal = getPlayKeepMusicSignal();
 			if(getAllowSignal) {
 				long startTime = System.currentTimeMillis();
@@ -775,6 +794,41 @@ public class CallHandler {
 		}
 	}
 
+	boolean shouldPlayWaitMusicForTransfer() {
+		return !shouldDelayTransferMediaStopUntilManualAnswer();
+	}
+
+	private boolean shouldDelayTransferMediaStopUntilManualAnswer() {
+		if (inboundDetail == null || inboundDetail.getOutboundPhoneInfo() == null
+				|| inboundDetail.getOutboundPhoneInfo().getTaskInfo() == null) {
+			return false;
+		}
+		CallTaskEntity taskInfo = inboundDetail.getOutboundPhoneInfo().getTaskInfo();
+		String voiceSource = taskInfo.getVoiceSource();
+		String transferType = taskInfo.getAiTransferType();
+		return TtsProvider.XFYUN.equalsIgnoreCase(StringUtils.isNullOrEmpty(voiceSource) ? "" : voiceSource.trim())
+				&& TransferToAgent.TRANSFER_TO_ACD.equalsIgnoreCase(StringUtils.isNullOrEmpty(transferType) ? "" : transferType.trim());
+	}
+
+	private void stopRobotMediaAfterManualAnswer() {
+		if (!shouldDelayTransferMediaStopUntilManualAnswer()) {
+			return;
+		}
+		log.info("{} xfyun acd transfer flow: manual answered, stop current robot media now.", uuid);
+		EslConnectionUtil.sendSyncApiCommand("uuid_break", String.format("%s all", inboundDetail.getUuid()));
+		if (inboundDetail.getOutboundPhoneInfo() != null && inboundDetail.getOutboundPhoneInfo().getTaskInfo() != null) {
+			String asrProvider = inboundDetail.getOutboundPhoneInfo().getTaskInfo().getAsrProvider();
+			if (!StringUtils.isNullOrEmpty(asrProvider)) {
+				EslConnectionUtil.sendExecuteCommand(
+						String.format("stop_%s_asr", asrProvider.trim()),
+						"",
+						inboundDetail.getUuid()
+				);
+				ThreadUtil.sleep(200);
+			}
+		}
+	}
+
 	private void reportExtensionStatusToWsClient() {
 		MessageHandlerEngineList.sendReplyToAgent(agentSessionEntity.getOpNum(),
 				new MessageResponse(RespStatus.EXTENSION_CANNOT_CONNECTED, JSON.toJSONString(agentSessionEntity))

+ 32 - 1
src/main/java/com/telerobot/fs/robot/AbstractChatRobot.java

@@ -9,6 +9,7 @@ import com.telerobot.fs.entity.dto.LlmAiphoneRes;
 import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
 import com.telerobot.fs.entity.po.HangupCause;
 import com.telerobot.fs.entity.pojo.LlmToolRequest;
+import com.telerobot.fs.entity.pojo.TtsProvider;
 import com.telerobot.fs.utils.RegExp;
 import link.thingscloud.freeswitch.esl.EslConnectionUtil;
 import net.sf.json.regexp.RegexpUtils;
@@ -97,6 +98,18 @@ public abstract class AbstractChatRobot implements IChatRobot {
         }
         return false;
     }
+
+    protected boolean isXfyunTtsProvider() {
+        return TtsProvider.XFYUN.equalsIgnoreCase(ttsProvider);
+    }
+
+    protected boolean shouldFlushStreamingTtsChunk(String speechContent) {
+        if (isXfyunTtsProvider()) {
+            return false;
+        }
+        return ttsTextLength >= 5 && checkPauseFlag(speechContent);
+    }
+
     protected void sendToTts() {
         StringBuilder tmpText = new StringBuilder("");
         while (ttsTextCache.peek() != null) {
@@ -167,7 +180,7 @@ public abstract class AbstractChatRobot implements IChatRobot {
         }
 
         if(TtsChannelState.CLOSED.getCode().equals(ttsChannelState.getCode())) {
-            EslConnectionUtil.sendExecuteCommand("speak", String.format("%s|%s|%s", ttsProvider, ttsVoiceName, text), uuid);
+            EslConnectionUtil.sendExecuteCommand("speak", buildSpeakCommand(text), uuid);
             ttsChannelState = TtsChannelState.TRYING_OPEN;
             logger.info("{} sendTtsRequest speak tts text {}", uuid, text);
         }else  if(TtsChannelState.OPENED.getCode().equals(ttsChannelState.getCode()))  {
@@ -188,11 +201,29 @@ public abstract class AbstractChatRobot implements IChatRobot {
         this.ttsVoiceName = voiceName;
     }
 
+    private String buildSpeakCommand(String text) {
+        if (!TtsProvider.XFYUN.equalsIgnoreCase(ttsProvider)) {
+            return String.format("%s|%s|%s", ttsProvider, ttsVoiceName, text);
+        }
+
+        StringBuilder inlineParams = new StringBuilder();
+        inlineParams.append("channel-uuid=").append(uuid);
+        inlineParams.append(",xf_tts_verify_peer=false");
+        if ("clone".equalsIgnoreCase(StringUtils.trimToEmpty(getAccount().ttsModels))) {
+            inlineParams.append(",xf_tts_mode=clone");
+        }
+        return String.format("%s|%s|{%s}%s", ttsProvider, ttsVoiceName, inlineParams, text);
+    }
+
     /**
      *  关闭tts通道
      */
     @Override
     public  void closeTts(){
+        if (TtsProvider.XFYUN.equalsIgnoreCase(ttsProvider)) {
+            logger.info("{} skip closeTts stop mark for xfyun, rely on natural stream completion.", uuid);
+            return;
+        }
         String cmd = "<StopSynthesis/>";
         if(TtsChannelState.OPENED.getCode().equals(ttsChannelState.getCode()))  {
             ttsRequestQueue.add(cmd);

+ 42 - 0
src/main/java/com/telerobot/fs/robot/RobotBase.java

@@ -8,7 +8,9 @@ import com.telerobot.fs.entity.bo.RobotInteractiveParam;
 import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
 import com.telerobot.fs.entity.po.HangupCause;
 import com.telerobot.fs.entity.pojo.SpeechResultEntity;
+import com.telerobot.fs.entity.pojo.TtsProvider;
 import com.telerobot.fs.entity.pojo.TtsFileInfo;
+import com.telerobot.fs.tts.xfyun.XfyunCloneTtsFileSynthesizer;
 import com.telerobot.fs.utils.CommonUtils;
 import com.telerobot.fs.utils.ThreadPoolCreator;
 import com.telerobot.fs.utils.ThreadUtil;
@@ -249,6 +251,7 @@ public abstract class RobotBase implements IEslEventListener {
     protected  volatile  boolean isHangup = false;
 
     protected volatile boolean ttsChannelClosed = false;
+    protected volatile boolean fileBasedTtsPlayback = false;
 
 
     public boolean getHangup() {
@@ -492,12 +495,51 @@ public abstract class RobotBase implements IEslEventListener {
         String ttsProvider =  chatRobot.getAccount().voiceSource;
         if(StringUtils.isEmpty(ttsProvider)) {
             startPlayback(AudioUtils.joinTtsFiles(tips));
+        }else if(playXfyunCloneTtsIfNeeded(tips)) {
+            logger.info("{} play sound by xfyun clone wav playback.", getTraceId());
         }else {
             chatRobot.sendTtsRequest(tips);
             chatRobot.closeTts();
         }
     }
 
+    protected boolean useXfyunCloneFilePlayback() {
+        if (chatRobot == null || chatRobot.getAccount() == null) {
+            return false;
+        }
+        AccountBaseEntity account = chatRobot.getAccount();
+        return TtsProvider.XFYUN.equalsIgnoreCase(StringUtils.trimToEmpty(account.voiceSource))
+                && "clone".equalsIgnoreCase(StringUtils.trimToEmpty(account.ttsModels))
+                && StringUtils.isNotBlank(account.voiceCode);
+    }
+
+    protected boolean playXfyunCloneTtsIfNeeded(String text) {
+        if (!useXfyunCloneFilePlayback() || StringUtils.isBlank(text)) {
+            return false;
+        }
+        String wavFilePath = XfyunCloneTtsFileSynthesizer.synthesizeToWavFile(
+                getTraceId(),
+                StringUtils.trimToEmpty(chatRobot.getAccount().voiceCode),
+                text
+        );
+        if (StringUtils.isBlank(wavFilePath)) {
+            return false;
+        }
+        startTtsFilePlayback(AudioUtils.joinTtsFiles(wavFilePath));
+        return true;
+    }
+
+    protected void startTtsFilePlayback(TtsFileInfo ttsFileInfo) {
+        if (ttsFileInfo == null || ttsFileInfo.getTtsFileNumber() <= 0 || StringUtils.isBlank(ttsFileInfo.getFilesString())) {
+            logger.warn("{} startTtsFilePlayback skipped because no playable file exists.", getTraceId());
+            return;
+        }
+        fileBasedTtsPlayback = true;
+        ttsChannelClosed = false;
+        recvPlayBackEndEvent = false;
+        startPlayback(ttsFileInfo);
+    }
+
     public void startPlayback(TtsFileInfo ttsFileInfo) {
         playbackStartTime = System.currentTimeMillis();
         playbackEndTime = playbackStartTime + ttsFileInfo.getTimeLength();

+ 222 - 14
src/main/java/com/telerobot/fs/robot/RobotChat.java

@@ -165,6 +165,15 @@ public class RobotChat extends RobotBase {
                     "xf_tts_mode=" + xfTtsMode,
                     uuid
             );
+            EslConnectionUtil.sendExecuteCommand("set",
+                    "xf_tts_verify_peer=false",
+                    uuid
+            );
+            // xfyun streaming continuation relies on xf_tts_resume, which needs cached speech handles.
+            EslConnectionUtil.sendExecuteCommand("set",
+                    "cache_speech_handles=true",
+                    uuid
+            );
         }
         if(ttsProvider.equalsIgnoreCase(TtsProvider.MICROSOFT)) {
             logger.info("{}  Current tts provider is microsoft!", getTraceId());
@@ -256,6 +265,12 @@ public class RobotChat extends RobotBase {
                     logger.info("{} recv PLAYBACK_STOP event for wav file {}. ", getTraceId(), playbackFilePath);
                 }else {
                     logger.info("{} The playback of the file {} has completed. ", getTraceId(), playbackFilePath);
+                    if(fileBasedTtsPlayback) {
+                        fileBasedTtsPlayback = false;
+                        ttsChannelClosed = true;
+                        chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
+                        logger.info("{} file-based tts playback finished.", getTraceId());
+                    }
                     recvPlayBackEndEvent = true;
                     playbackEndTime = System.currentTimeMillis();
                     releasePlayBackFinishedSignal();
@@ -374,6 +389,8 @@ public class RobotChat extends RobotBase {
                chatRobot.setTtsChannelState(TtsChannelState.CLOSED);
                logger.info("{}  TtsChannelClosed = true.", getTraceId());
                ttsChannelClosed = true;
+               recvPlayBackEndEvent = true;
+               playbackEndTime = System.currentTimeMillis();
                releasePlayBackFinishedSignal();
            }
            if("Speech-Open".equalsIgnoreCase(event)){
@@ -401,6 +418,21 @@ public class RobotChat extends RobotBase {
             if (null != asrResponse) {
                 asrResponse = headers.get("Detect-Speech-Result").trim();
             }
+            // #region debug-point xfyun-asr-no-response-asr-event
+            logger.info("{} dbg_asr_event recv event={}, recvPlayBackEndEvent={}, allowInterrupt={}, inSpeaking={}, isHangup={}, transferToAgent={}, keepAiDuringTransferWait={}, manualAnsweredTime={}, isReleased={}, response={}",
+                    getTraceId(),
+                    speechEvent,
+                    recvPlayBackEndEvent,
+                    getAllowInterrupt(),
+                    interactiveParam.checkInSpeaking(),
+                    isHangup,
+                    transferToAgent,
+                    shouldKeepAiConversationDuringTransferWait(),
+                    callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
+                    isReleased,
+                    asrResponse
+            );
+            // #endregion
 
             if ("NetworkError".equalsIgnoreCase(speechEvent)) {
                 CommonUtils.setHangupCauseDetail(
@@ -415,12 +447,35 @@ public class RobotChat extends RobotBase {
 
             lastTalkTime = System.currentTimeMillis();
 
-            if (isHangup || interactiveParam.checkInHangupState() ||  transferToAgent) {
+            boolean transferWaitingForManualAnswer = shouldKeepAiConversationDuringTransferWait();
+            if (isHangup || interactiveParam.checkInHangupState() ||  (transferToAgent && !transferWaitingForManualAnswer)) {
+                // #region debug-point xfyun-asr-no-response-drop-hangup
+                logger.info("{} dbg_asr_event drop by hangup-state event={}, isHangup={}, inHangupState={}, transferToAgent={}, transferWaitingForManualAnswer={}, response={}",
+                        getTraceId(),
+                        speechEvent,
+                        isHangup,
+                        interactiveParam.checkInHangupState(),
+                        transferToAgent,
+                        transferWaitingForManualAnswer,
+                        asrResponse
+                );
+                // #endregion
                 logger.info("{} Session is going to be hangup or is already being transferred to human operator, drop asr result: {}", getTraceId(), asrResponse);
                 return;
             }
 
-            if (!getAllowInterrupt() && !recvPlayBackEndEvent) {
+            boolean allowTransferWaitBargeIn = shouldKeepAiConversationDuringTransferWait();
+            if (!getAllowInterrupt() && !recvPlayBackEndEvent && !allowTransferWaitBargeIn) {
+                // #region debug-point xfyun-asr-no-response-drop-playback
+                logger.info("{} dbg_asr_event drop by playback-gate event={}, recvPlayBackEndEvent={}, allowInterrupt={}, allowTransferWaitBargeIn={}, response={}",
+                        getTraceId(),
+                        speechEvent,
+                        recvPlayBackEndEvent,
+                        getAllowInterrupt(),
+                        allowTransferWaitBargeIn,
+                        asrResponse
+                );
+                // #endregion
                 if ("vad".equalsIgnoreCase(speechEvent)) {
                     dropAsrCounter.incrementAndGet();
                     logger.info("{} (vad event) drop asr result: {}", getTraceId(), asrResponse);
@@ -438,7 +493,7 @@ public class RobotChat extends RobotBase {
                         getAllowInterrupt(),
                         !interactiveParam.checkInSpeaking()
                 );
-                if (recvPlayBackEndEvent || getAllowInterrupt()) {
+                if (recvPlayBackEndEvent || getAllowInterrupt() || allowTransferWaitBargeIn) {
                     if (!interactiveParam.checkInSpeaking()) {
                         String lockerKey = String.format("%s%s", uuid, "checkInSpeaking");
                         synchronized (lockerKey.intern()) {
@@ -447,7 +502,12 @@ public class RobotChat extends RobotBase {
                                 // Main thread awakened to extend customer speaking time beyond 6 seconds.
                                 logger.info("{} customer speech detected. ", getTraceId());
 
-                                if (chatRobot.getAccount().interruptFlag == 2) {
+                                if (allowTransferWaitBargeIn && !recvPlayBackEndEvent) {
+                                    logger.info("{} transfer-wait barge-in detected by middle event, interrupt current robot speech.", getTraceId());
+                                    interruptRobotSpeech();
+                                    releasePlayBackFinishedSignal();
+                                    ThreadUtil.sleep(100);
+                                } else if (chatRobot.getAccount().interruptFlag == 2) {
                                     interruptRobotSpeech();
                                     releasePlayBackFinishedSignal();
                                     ThreadUtil.sleep(100);
@@ -467,9 +527,21 @@ public class RobotChat extends RobotBase {
 
                 if (!StringUtil.isNullOrEmpty(asrResponse)) {
                     asrResultEx.add(asrResponse);
+                    // #region debug-point xfyun-asr-no-response-asr-cache
+                    logger.info("{} dbg_asr_event cached vad result, queueSize={}, response={}",
+                            getTraceId(),
+                            asrResultEx.size(),
+                            asrResponse
+                    );
+                    // #endregion
                 }
 
-                if(chatRobot.getAccount().interruptFlag == 1 && !recvPlayBackEndEvent) {
+                if (allowTransferWaitBargeIn && !recvPlayBackEndEvent) {
+                    logger.info("{} transfer-wait barge-in detected by vad event, interrupt current robot speech.", getTraceId());
+                    interruptRobotSpeech();
+                    releasePlayBackFinishedSignal();
+                    ThreadUtil.sleep(100);
+                } else if(chatRobot.getAccount().interruptFlag == 1 && !recvPlayBackEndEvent) {
                     if (checkSpeechInterrupt(asrResponse)) {
                         interruptRobotSpeech();
                         releasePlayBackFinishedSignal();
@@ -479,7 +551,17 @@ public class RobotChat extends RobotBase {
                     }
                 }
 
-                if(recvPlayBackEndEvent || getAllowInterrupt()){
+                if(recvPlayBackEndEvent || getAllowInterrupt() || allowTransferWaitBargeIn){
+                    // #region debug-point xfyun-asr-no-response-release-signal
+                    logger.info("{} dbg_asr_event release signal by vad event, queueSize={}, recvPlayBackEndEvent={}, allowInterrupt={}, allowTransferWaitBargeIn={}, response={}",
+                            getTraceId(),
+                            asrResultEx.size(),
+                            recvPlayBackEndEvent,
+                            getAllowInterrupt(),
+                            allowTransferWaitBargeIn,
+                            asrResponse
+                    );
+                    // #endregion
                     logger.info("{} releaseSignal for vad event.", getTraceId());
                     releaseSignal();
                 }
@@ -639,6 +721,20 @@ public class RobotChat extends RobotBase {
         if (checkCallSession()) {
             return;
         }
+        // #region debug-point xfyun-asr-no-response-interact-enter
+        logger.info("{} dbg_interact enter, asrQueueSize={}, recvPlayBackEndEvent={}, ttsChannelClosed={}, inSpeaking={}, noVoiceCounter={}, transferToAgent={}, keepAiDuringTransferWait={}, manualAnsweredTime={}, isReleased={}",
+                getTraceId(),
+                asrResultEx.size(),
+                recvPlayBackEndEvent,
+                ttsChannelClosed,
+                interactiveParam.checkInSpeaking(),
+                noVoiceCounter.get(),
+                transferToAgent,
+                shouldKeepAiConversationDuringTransferWait(),
+                callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
+                isReleased
+        );
+        // #endregion
         interactiveParam.setAllowInterrupt(0);
         recvPlayBackEndEvent = false;
         firstSpeak = false;
@@ -653,6 +749,13 @@ public class RobotChat extends RobotBase {
         for (String result : asrResultEx) {
             asrStr.append(result);
         }
+        // #region debug-point xfyun-asr-no-response-question-built
+        logger.info("{} dbg_interact built question='{}', cachedResultCount={}",
+                getTraceId(),
+                asrStr.toString(),
+                asrResultEx.size()
+        );
+        // #endregion
 
         // 清空 asrResultEx; 重新初始化字段;
         asrResultEx.clear();
@@ -664,6 +767,14 @@ public class RobotChat extends RobotBase {
             try {
                 String question = asrStr.toString();
                 if (StringUtils.isEmpty(question)) {
+                    // #region debug-point xfyun-asr-no-response-empty-question
+                    logger.info("{} dbg_interact question empty, noVoiceCounterBefore={}, recvPlayBackEndEvent={}, ttsChannelClosed={}",
+                            getTraceId(),
+                            noVoiceCounter.get(),
+                            recvPlayBackEndEvent,
+                            ttsChannelClosed
+                    );
+                    // #endregion
                     int counter = noVoiceCounter.incrementAndGet();
                     if (counter > MAX_CONSECUTIVE_NO_VOICE_NUMBER) {
                         logger.info("{} There has been no sound for {} consecutive times. Play hangupTips and then hangup call.",
@@ -830,10 +941,31 @@ public class RobotChat extends RobotBase {
 
     private void playResponse(LlmAiphoneRes aiphoneRes){
         String ttsFilePathList = aiphoneRes.getTtsFilePathList();
+        if (useXfyunCloneFilePlayback()
+                && talkRound.longValue() == 1
+                && StringUtils.isEmpty(ttsFilePathList)
+                && StringUtils.isNotEmpty(aiphoneRes.getBody())) {
+            logger.info("{} skip duplicate first-round xfyun clone playback, text={}.",
+                    getTraceId(),
+                    aiphoneRes.getBody()
+            );
+            return;
+        }
+        if(StringUtils.isEmpty(ttsFilePathList) && useXfyunCloneFilePlayback()) {
+            if (playXfyunCloneTtsIfNeeded(aiphoneRes.getBody())) {
+                logger.info("{} try to play xfyun clone wav file for text {}.", getTraceId(), aiphoneRes.getBody());
+            } else if(StringUtils.isNotEmpty(aiphoneRes.getBody())) {
+                logger.info("{} fallback to streaming xfyun clone tts for text {}.", getTraceId(), aiphoneRes.getBody());
+                chatRobot.sendTtsRequest(aiphoneRes.getBody());
+                chatRobot.closeTts();
+            }
+            return;
+        }
         if(!StringUtils.isEmpty(ttsFilePathList)){
             TtsFileInfo ttsFileInfo = AudioUtils.joinTtsFiles(ttsFilePathList);
             logger.info("{} try to play wav file for text {}.", getTraceId(), aiphoneRes.getBody());
             startPlayback(ttsFileInfo);
+            return;
         }
     }
 
@@ -889,25 +1021,72 @@ public class RobotChat extends RobotBase {
             waitForPlayBackFinished(9000);
         }
 
-        // stop_asr 的顺序很重要,需要放在播放tts之后,否则不起作用;会被uuid_break清空指令;
-        logger.info("{} Try to stop asr {}", getTraceId(), chatRobot.getAccount().asrProvider);
-        EslConnectionUtil.sendExecuteCommand(
-                String.format("stop_%s_asr",  chatRobot.getAccount().asrProvider), "", uuid);
+        boolean delayTransferMediaStopUntilManualAnswer = shouldDelayTransferMediaStopUntilManualAnswer();
+        if (delayTransferMediaStopUntilManualAnswer) {
+            logger.info("{} xfyun transfer flow keeps TTS/ASR active until manual agent answers. manualAnsweredTime={}, isReleased={}",
+                    getTraceId(),
+                    callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
+                    isReleased);
+        } else {
+            // stop_asr 的顺序很重要,需要放在播放tts之后,否则不起作用;会被uuid_break清空指令;
+            logger.info("{} Try to stop asr {}", getTraceId(), chatRobot.getAccount().asrProvider);
+            EslConnectionUtil.sendExecuteCommand(
+                    String.format("stop_%s_asr",  chatRobot.getAccount().asrProvider), "", uuid);
 
-        ThreadUtil.sleep(200);
+            ThreadUtil.sleep(200);
+        }
 
         if(!isHangup) {
-            releaseThreadNum();
-            TransferToAgent.transfer(callDetail, chatRobot.getAccount());
+            // #region debug-point xfyun-transfer-wait-release-thread
+            logger.info("{} dbg_transfer_wait before releaseThreadNum transferToAgent={}, transferToAgentExecuted={}, keepAiDuringTransferWait={}, manualAnsweredTime={}, isReleased={}",
+                    getTraceId(),
+                    transferToAgent,
+                    transferToAgentExecuted,
+                    shouldKeepAiConversationDuringTransferWait(),
+                    callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
+                    isReleased
+            );
+            // #endregion
+            if (!delayTransferMediaStopUntilManualAnswer) {
+                releaseThreadNum();
+                TransferToAgent.transfer(callDetail, chatRobot.getAccount());
+            } else {
+                logger.info("{} postpone releaseThreadNum until manual agent answers.", getTraceId());
+                final InboundDetail currentCallDetail = callDetail;
+                final AccountBaseEntity currentAccount = chatRobot.getAccount();
+                getRobotMainThreadPool().execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        ThreadLocalTraceId.getInstance().setTraceId(uuid);
+                        logger.info("{} async transfer-to-agent task started while AI keeps talking before manual answer.", getTraceId());
+                        TransferToAgent.transfer(currentCallDetail, currentAccount);
+                    }
+                });
+                waitForCustomerSpeakEx();
+            }
         }
     }
 
+    private boolean shouldDelayTransferMediaStopUntilManualAnswer() {
+        String voiceSource = StringUtils.trimToEmpty(chatRobot.getAccount().voiceSource);
+        String transferType = StringUtils.trimToEmpty(chatRobot.getAccount().aiTransferType);
+        return TtsProvider.XFYUN.equalsIgnoreCase(voiceSource)
+                && !TransferToAgent.TRANSFER_TO_ACD.equalsIgnoreCase(transferType);
+    }
+
+    private boolean shouldKeepAiConversationDuringTransferWait() {
+        return transferToAgent
+                && shouldDelayTransferMediaStopUntilManualAnswer()
+                && callDetail != null
+                && callDetail.getManualAnsweredTime() == 0L;
+    }
+
     /**
      * Check if the call has been hung up or has been transferred to a human handler.
      * @return
      */
     private boolean checkCallSession(){
-        return isHangup || transferToAgent;
+        return isHangup || (transferToAgent && !shouldKeepAiConversationDuringTransferWait());
     }
 
     /**
@@ -918,6 +1097,19 @@ public class RobotChat extends RobotBase {
             return;
         }
 
+        // #region debug-point xfyun-asr-no-response-wait-enter
+        logger.info("{} dbg_wait_customer enter, recvPlayBackEndEvent={}, ttsChannelClosed={}, asrQueueSize={}, inSpeaking={}, transferToAgent={}, keepAiDuringTransferWait={}, manualAnsweredTime={}, isReleased={}",
+                getTraceId(),
+                recvPlayBackEndEvent,
+                ttsChannelClosed,
+                asrResultEx.size(),
+                interactiveParam.checkInSpeaking(),
+                transferToAgent,
+                shouldKeepAiConversationDuringTransferWait(),
+                callDetail == null ? -1L : callDetail.getManualAnsweredTime(),
+                isReleased
+        );
+        // #endregion
         logger.info("{} enter into waitForCustomerSpeak  ...", getTraceId());
 
         // The duration of streaming TTS playback should not exceed 181 seconds.
@@ -953,6 +1145,14 @@ public class RobotChat extends RobotBase {
                 getTraceId(),
                 System.currentTimeMillis() - startWaitTimeMills
         );
+        // #region debug-point xfyun-asr-no-response-wait-after-acquire
+        logger.info("{} dbg_wait_customer after acquire, asrQueueSize={}, inSpeaking={}, recvPlayBackEndEvent={}",
+                getTraceId(),
+                asrResultEx.size(),
+                interactiveParam.checkInSpeaking(),
+                recvPlayBackEndEvent
+        );
+        // #endregion
 
         if (checkCallSession()) {
             return;
@@ -998,6 +1198,14 @@ public class RobotChat extends RobotBase {
         if (asrResultEx.size() == 0) {
             acquire(500);
         }
+        // #region debug-point xfyun-asr-no-response-wait-exit
+        logger.info("{} dbg_wait_customer exit, asrQueueSize={}, inSpeaking={}, noVoiceCounter={}",
+                getTraceId(),
+                asrResultEx.size(),
+                interactiveParam.checkInSpeaking(),
+                noVoiceCounter.get()
+        );
+        // #endregion
 
         if (checkCallSession()) {
             return;

+ 95 - 1
src/main/java/com/telerobot/fs/robot/TransferListener.java

@@ -5,13 +5,16 @@ import com.alibaba.fastjson.JSONObject;
 import com.telerobot.fs.acd.AcdSqlQueue;
 import com.telerobot.fs.config.AppContextProvider;
 import com.telerobot.fs.entity.bo.InboundDetail;
+import com.telerobot.fs.entity.dao.CallTaskEntity;
 import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
 import com.telerobot.fs.entity.po.CdrDetail;
+import com.telerobot.fs.entity.pojo.TtsProvider;
 import com.telerobot.fs.global.BizThreadPoolForEsl;
 import com.telerobot.fs.global.CdrPush;
 import com.telerobot.fs.ivr.IvrEngine;
 import com.telerobot.fs.service.AsrResultListener;
 import com.telerobot.fs.utils.StringUtils;
+import com.telerobot.fs.utils.ThreadUtil;
 import link.thingscloud.freeswitch.esl.EslConnectionUtil;
 import link.thingscloud.freeswitch.esl.IEslEventListener;
 import link.thingscloud.freeswitch.esl.constant.EventNames;
@@ -65,7 +68,70 @@ public class TransferListener implements  IEslEventListener {
     protected final String WAIT_WAV_FILE = "ivr/llm_wait.wav";
     private Semaphore playBackStartSignal = new Semaphore(0);
     private Semaphore playBackStoppedSignal = new Semaphore(0);
+    boolean shouldPlayWaitMusic() {
+        return !shouldDelayTransferMediaStopUntilManualAnswer();
+    }
+
+    private boolean shouldDelayTransferMediaStopUntilManualAnswer() {
+        String voiceSource = resolveVoiceSource();
+        String transferType = resolveTransferType();
+        boolean shouldDelay = TtsProvider.XFYUN.equalsIgnoreCase(voiceSource)
+                && !TransferToAgent.TRANSFER_TO_ACD.equalsIgnoreCase(transferType);
+        logger.info("{} transfer media decision in listener: shouldDelay={}, voiceSource={}, transferType={}, asrProvider={}, accountVoiceSource={}, accountTransferType={}.",
+                inboundDetail.getUuid(),
+                shouldDelay,
+                voiceSource,
+                transferType,
+                resolveAsrProvider(),
+                account == null ? "" : safeTrim(account.voiceSource),
+                account == null ? "" : safeTrim(account.aiTransferType)
+        );
+        return shouldDelay;
+    }
+
+    private String resolveVoiceSource() {
+        String voiceSource = account == null ? "" : safeTrim(account.voiceSource);
+        if (!StringUtils.isNullOrEmpty(voiceSource)) {
+            return voiceSource;
+        }
+        CallTaskEntity taskInfo = getOutboundTaskInfo();
+        return taskInfo == null ? "" : safeTrim(taskInfo.getVoiceSource());
+    }
+
+    private String resolveTransferType() {
+        String transferType = account == null ? "" : safeTrim(account.aiTransferType);
+        if (!StringUtils.isNullOrEmpty(transferType)) {
+            return transferType;
+        }
+        CallTaskEntity taskInfo = getOutboundTaskInfo();
+        return taskInfo == null ? "" : safeTrim(taskInfo.getAiTransferType());
+    }
+
+    private String resolveAsrProvider() {
+        String asrProvider = account == null ? "" : safeTrim(account.asrProvider);
+        if (!StringUtils.isNullOrEmpty(asrProvider)) {
+            return asrProvider;
+        }
+        CallTaskEntity taskInfo = getOutboundTaskInfo();
+        return taskInfo == null ? "" : safeTrim(taskInfo.getAsrProvider());
+    }
+
+    private CallTaskEntity getOutboundTaskInfo() {
+        if (inboundDetail == null || inboundDetail.getOutboundPhoneInfo() == null) {
+            return null;
+        }
+        return inboundDetail.getOutboundPhoneInfo().getTaskInfo();
+    }
+
+    private String safeTrim(String value) {
+        return StringUtils.isNullOrEmpty(value) ? "" : value.trim();
+    }
+
     public void playWaitMusic(){
+        if (!shouldPlayWaitMusic()) {
+            logger.info("{} skip wait music before manual answer for xfyun transfer flow.", inboundDetail.getUuid());
+            return;
+        }
         logger.info("{} playWaitMusic for call session. ", inboundDetail.getUuid());
         EslConnectionUtil.sendExecuteCommand(
                 "playback",
@@ -108,6 +174,10 @@ public class TransferListener implements  IEslEventListener {
     }
 
     private void breakWaitMusic(){
+        if (!shouldPlayWaitMusic()) {
+            logger.info("{} skip breakWaitMusic because no wait music should be playing for current transfer flow.", inboundDetail.getUuid());
+            return;
+        }
         boolean getAllowSignal = getPlayKeepMusicSignal();
         if(getAllowSignal) {
             long startTime = System.currentTimeMillis();
@@ -147,7 +217,7 @@ public class TransferListener implements  IEslEventListener {
                 logger.info("{} recv PLAYBACK_STOP event for wav file {}. ", inboundDetail.getUuid(), playbackFilePath);
                 boolean getAllowSignal = getPlayKeepMusicSignal();
                 if(getAllowSignal) {
-                    if (inboundDetail.getManualAnsweredTime() == 0L) {
+                        if (inboundDetail.getManualAnsweredTime() == 0L && shouldPlayWaitMusic()) {
                         playWaitMusic();
                         waitForPlayBackStartSignal();
                     }
@@ -169,6 +239,7 @@ public class TransferListener implements  IEslEventListener {
                 answered.set(true);
                 continueSignal.release();
 
+                stopRobotMediaAfterManualAnswer();
                 breakWaitMusic();
 
                 logger.info("{} callee answered, try to bridge call session.", inboundDetail.getUuid());
@@ -250,6 +321,29 @@ public class TransferListener implements  IEslEventListener {
         return this.getClass().getName();
     }
 
+    private void stopRobotMediaAfterManualAnswer() {
+        if (!shouldDelayTransferMediaStopUntilManualAnswer()) {
+            return;
+        }
+        logger.info("{} xfyun transfer flow: manual answered, stop current robot media now.", inboundDetail.getUuid());
+        RobotBase robotSession = RobotBase.callTaskList.get(inboundDetail.getUuid());
+        if (robotSession != null) {
+            robotSession.releaseThreadNum();
+            logger.info("{} xfyun transfer flow: release robot session after manual answer.", inboundDetail.getUuid());
+        }
+        EslConnectionUtil.sendSyncApiCommand("uuid_break", String.format("%s all", inboundDetail.getUuid()));
+        String asrProvider = resolveAsrProvider();
+        if (!StringUtils.isNullOrEmpty(asrProvider)) {
+            EslConnectionUtil.sendExecuteCommand(
+                    String.format("stop_%s_asr", asrProvider),
+                    "",
+                    inboundDetail.getUuid()
+            );
+            // Make sure the stop_asr execute finishes before uuid_bridge reattaches media.
+            ThreadUtil.sleep(200);
+        }
+    }
+
     private static void hangupCallSession(String uuid, String reason) {
         EslConnectionUtil.sendExecuteCommand("hangup", reason, uuid);
     }

+ 43 - 3
src/main/java/com/telerobot/fs/robot/TransferToAgent.java

@@ -12,6 +12,7 @@ import com.telerobot.fs.entity.bo.InboundDetail;
 import com.telerobot.fs.entity.dto.GatewayConfig;
 import com.telerobot.fs.entity.dto.llm.AccountBaseEntity;
 import com.telerobot.fs.entity.po.CdrDetail;
+import com.telerobot.fs.entity.pojo.TtsProvider;
 import com.telerobot.fs.global.BizThreadPoolForEsl;
 import com.telerobot.fs.global.CdrPush;
 import com.telerobot.fs.ivr.IvrEngine;
@@ -65,10 +66,30 @@ public class TransferToAgent {
      * @param callDetail
      */
     public  static  void transfer(InboundDetail callDetail, AccountBaseEntity account){
+        if (account == null) {
+            logger.error("{} transfer account is null.", callDetail.getUuid());
+            return;
+        }
         String transferType = account.aiTransferType;
         String transferData = account.aiTransferData;
         String satisfSurveyIvrId = account.satisfSurveyIvrId;
-        transfer(callDetail, transferType, transferData, satisfSurveyIvrId);
+        logger.info("{} transfer-to-agent-type = {} .", callDetail.getUuid(), transferType);
+        logger.info("{} transfer account snapshot: voiceSource={}, transferType={}, asrProvider={}.",
+                callDetail.getUuid(),
+                safeTrim(account.voiceSource),
+                safeTrim(account.aiTransferType),
+                safeTrim(account.asrProvider)
+        );
+
+        if(transferType.equalsIgnoreCase(TRANSFER_TO_ACD)) {
+            transfer(callDetail, transferType, transferData, satisfSurveyIvrId);
+        }else  if(transferType.equalsIgnoreCase(TRANSFER_TO_GATEWAY)) {
+            logger.info("{} Try to bridge call to external gateway. {}", callDetail.getUuid(),  transferData);
+            transferToAgentUsingGateway(callDetail, account);
+        }else  if(transferType.equalsIgnoreCase(TRANSFER_TO_EXTENSION)) {
+            logger.info("{} Try to bridge call to internal extension. {}", callDetail.getUuid(), transferData);
+            transferToAgentUsingExtension(callDetail, account);
+        }
     }
 
     public  static  void transfer(InboundDetail callDetail,  String transferType,
@@ -129,16 +150,21 @@ public class TransferToAgent {
                         account, answered, continueSignal, calleeHangup);
                 EslConnectionUtil.getDefaultEslConnectionPool().getDefaultEslConn().addListener(calleeUuid, listener);
                 EslConnectionUtil.getDefaultEslConnectionPool().getDefaultEslConn().addListener(inboundDetail.getUuid(), listener);
+                String[] keepListenerKeys = shouldKeepRobotListenerDuringTransferWait(account)
+                        ? new String[]{UuidKeys.DEFAULT, UuidKeys.BATCH_CALL, UuidKeys.ROBOT}
+                        : new String[]{UuidKeys.DEFAULT, UuidKeys.BATCH_CALL};
                 EslConnectionUtil.getDefaultEslConnectionPool().getDefaultEslConn()
                         .removeOtherListenersExcludeByUuidKeys(inboundDetail.getUuid(),
-                                new String[]{UuidKeys.DEFAULT, UuidKeys.BATCH_CALL}
+                                keepListenerKeys
                 );
 
-                if(!playedWaitMusic) {
+                if(!playedWaitMusic && listener.shouldPlayWaitMusic()) {
                     playedWaitMusic = true;
                     listener.playWaitMusic();
                     listener.waitForPlayBackStartSignal();
                     ThreadUtil.sleep(200);
+                } else if (!playedWaitMusic) {
+                    logger.info("{} skip llm_wait.wav before manual answer for xfyun transfer flow.", inboundDetail.getUuid());
                 }
 
                 String originateStr =  bridgeString.replace("callee_number", calleeNumber);
@@ -279,4 +305,18 @@ public class TransferToAgent {
         bridgeCall(inboundDetail, calleeList, bridgeString, outboundUuid, callTimeout, account);
     }
 
+    private static String safeTrim(String value) {
+        return StringUtils.isNullOrEmpty(value) ? "" : value.trim();
+    }
+
+    private static boolean shouldKeepRobotListenerDuringTransferWait(AccountBaseEntity account) {
+        if (account == null) {
+            return false;
+        }
+        String voiceSource = safeTrim(account.voiceSource);
+        String transferType = safeTrim(account.aiTransferType);
+        return TtsProvider.XFYUN.equalsIgnoreCase(voiceSource)
+                && !TRANSFER_TO_ACD.equalsIgnoreCase(transferType);
+    }
+
 }

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/ChatGPT.java

@@ -208,7 +208,7 @@ public class ChatGPT extends AbstractChatRobot {
                                     ttsTextCache.add(speechContent);
                                     ttsTextLength += speechContent.length();
                                     // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                    if (ttsTextLength >= 5 && checkPauseFlag(speechContent)) {
+                                    if (shouldFlushStreamingTtsChunk(speechContent)) {
                                         sendToTts();
 
                                         if (!recvData) {

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/ClaudeChat.java

@@ -229,7 +229,7 @@ public class ClaudeChat extends AbstractChatRobot {
                                     ttsTextCache.add(speechContent);
                                     ttsTextLength += speechContent.length();
                                     // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                    if (ttsTextLength >= 5 && checkPauseFlag(speechContent)) {
+                                    if (shouldFlushStreamingTtsChunk(speechContent)) {
                                         sendToTts();
 
                                         if (!recvData) {

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/Coze.java

@@ -239,7 +239,7 @@ public class Coze  extends AbstractChatRobot {
                                     ttsTextCache.add(tmpText);
                                     ttsTextLength += tmpText.length();
                                     // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                    if (ttsTextLength >= 10 && checkPauseFlag(tmpText)) {
+                                    if (shouldFlushStreamingTtsChunk(tmpText)) {
                                         sendToTts();
                                     }
                                 }

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/DeepSeekChat.java

@@ -204,7 +204,7 @@ public class DeepSeekChat extends AbstractChatRobot {
                                 ttsTextCache.add(speechContent);
                                 ttsTextLength += speechContent.length();
                                 // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                if (ttsTextLength >= 5 && checkPauseFlag(speechContent)) {
+                                if (shouldFlushStreamingTtsChunk(speechContent)) {
                                     sendToTts();
 
                                     if (!recvData) {

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/Dify.java

@@ -172,7 +172,7 @@ public class Dify extends AbstractChatRobot {
                                 ttsTextCache.add(speechContent);
                                 ttsTextLength += speechContent.length();
                                 // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                if (ttsTextLength >= 10 && checkPauseFlag(speechContent)) {
+                                if (shouldFlushStreamingTtsChunk(speechContent)) {
                                     sendToTts();
                                 }
                             }

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/JiutianAgent.java

@@ -184,7 +184,7 @@ public class JiutianAgent extends AbstractChatRobot {
                                 ttsTextCache.add(speechContent);
                                 ttsTextLength += speechContent.length();
                                 // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                if (ttsTextLength >= 10 && checkPauseFlag(speechContent)) {
+                                if (shouldFlushStreamingTtsChunk(speechContent)) {
                                     sendToTts();
                                 }
                             }

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/JiutianChat.java

@@ -175,7 +175,7 @@ public class JiutianChat extends AbstractChatRobot {
                                 ttsTextCache.add(speechContent);
                                 ttsTextLength += speechContent.length();
                                 // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                if (ttsTextLength >= 10 && checkPauseFlag(speechContent)) {
+                                if (shouldFlushStreamingTtsChunk(speechContent)) {
                                     sendToTts();
                                 }
                             }

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/JiutianWorkflow.java

@@ -179,7 +179,7 @@ public class JiutianWorkflow extends AbstractChatRobot {
                     ttsTextCache.add(speechContent);
                     ttsTextLength += speechContent.length();
                     // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                    if (ttsTextLength >= 10 && checkPauseFlag(speechContent)) {
+                    if (shouldFlushStreamingTtsChunk(speechContent)) {
                         sendToTts();
                     }
                 }

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/LocalLlmChat.java

@@ -197,7 +197,7 @@ public class LocalLlmChat extends AbstractChatRobot {
                                 ttsTextCache.add(speechContent);
                                 ttsTextLength += speechContent.length();
                                 // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                if (ttsTextLength >= 5 && checkPauseFlag(speechContent)) {
+                                if (shouldFlushStreamingTtsChunk(speechContent)) {
                                     sendToTts();
 
                                     if (!recvData) {

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/MaxKB.java

@@ -165,7 +165,7 @@ public class MaxKB extends AbstractChatRobot {
                                 ttsTextCache.add(tmpText);
                                 ttsTextLength += tmpText.length();
                                 // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                if (ttsTextLength >= 10 && checkPauseFlag(tmpText)) {
+                                if (shouldFlushStreamingTtsChunk(tmpText)) {
                                     sendToTts();
                                 }
                             }

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/TencentChat.java

@@ -204,7 +204,7 @@ public class TencentChat extends AbstractChatRobot {
                                 ttsTextCache.add(speechContent);
                                 ttsTextLength += speechContent.length();
                                 // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                                if (ttsTextLength >= 5 && checkPauseFlag(speechContent)) {
+                                if (shouldFlushStreamingTtsChunk(speechContent)) {
                                     sendToTts();
 
                                     if (!recvData) {

+ 1 - 1
src/main/java/com/telerobot/fs/robot/impl/XingWenChat.java

@@ -228,7 +228,7 @@ public class XingWenChat extends AbstractChatRobot {
                             ttsTextCache.add(speechContent);
                             ttsTextLength += speechContent.length();
                             // 积攒足够的字数之后,才发送给tts,避免播放异常;
-                            if (!StringUtils.isEmpty(speechContent) && ttsTextLength >= 10 && checkPauseFlag(speechContent)) {
+                            if (!StringUtils.isEmpty(speechContent) && shouldFlushStreamingTtsChunk(speechContent)) {
                                 sendToTts();
                             }
                         }