diff --git a/android/app/src/main/java/com/example/ai_chat_assistant/MainActivity.java b/android/app/src/main/java/com/example/ai_chat_assistant/MainActivity.java index 588d004..f9976c7 100644 --- a/android/app/src/main/java/com/example/ai_chat_assistant/MainActivity.java +++ b/android/app/src/main/java/com/example/ai_chat_assistant/MainActivity.java @@ -6,11 +6,13 @@ import android.util.Log; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.plugin.common.MethodChannel; + import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONObject; import com.alibaba.idst.nui.Constants; import com.alibaba.idst.nui.INativeStreamInputTtsCallback; import com.alibaba.idst.nui.NativeNui; +import com.example.ai_chat_assistant.token.AccessToken; import java.util.Map; @@ -18,7 +20,8 @@ public class MainActivity extends FlutterActivity { private static final String CHANNEL = "com.example.ai_chat_assistant/tts"; private static final String TAG = "StreamInputTts"; private static final String APP_KEY = "bXFFc1V65iYbW6EF"; - private static final String TOKEN = "9ae5be92a9fa4fb3ac9bcd56eba69437"; + private static final String ACCESS_KEY = "LTAI5t71JHxXRvt2mGuEVz9X"; + private static final String ACCESS_KEY_SECRET = "WQOUWvngxmCg4CxIG0qkSlkcH5hrVT"; private static final String URL = "wss://nls-gateway-cn-beijing.aliyuncs.com/ws/v1"; private final NativeNui streamInputTtsInstance = new NativeNui(Constants.ModeType.MODE_STREAM_INPUT_TTS); @@ -44,21 +47,25 @@ public class MainActivity extends FlutterActivity { new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL) .setMethodCallHandler((call, result) -> { + Map args = (Map) call.arguments; switch (call.method) { - case "startTts": - boolean isSuccess = startTts(); + case "start": + Object isChinese = args.get("isChinese"); + if (isChinese == null || isChinese.toString().isBlank()) { + return; + } + boolean isSuccess = start(Boolean.parseBoolean(isChinese.toString())); result.success(isSuccess); break; - case "sendTts": - Map args = (Map) call.arguments; + case "send": Object textArg = args.get("text"); if (textArg == null || textArg.toString().isBlank()) { return; } - sendTts(textArg.toString()); + send(textArg.toString()); break; - case "stopTts": - stopTts(); + case "stop": + stop(); result.success("已停止"); break; default: @@ -81,7 +88,7 @@ public class MainActivity extends FlutterActivity { streamInputTtsInstance.stopStreamInputTts(); } - private boolean startTts() { + private boolean start(boolean isChinese) { int ret = streamInputTtsInstance.startStreamInputTts(new INativeStreamInputTtsCallback() { @Override public void onStreamInputTtsEventCallback(INativeStreamInputTtsCallback.StreamInputTtsEvent event, String task_id, String session_id, int ret_code, String error_msg, String timestamp, String all_response) { @@ -117,13 +124,7 @@ public class MainActivity extends FlutterActivity { mAudioTrack.setAudioData(data); } } - - @Override - public void onStreamInputTtsLogTrackCallback(Constants.LogLevel level, String log) { - Log.i(TAG, "onStreamInputTtsLogTrackCallback log level:" + level + ", message -> " + log); - } - }, genTicket(), genParameters(), "", Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_VERBOSE), true); - + }, genTicket(), genParameters(isChinese), "", Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_NONE), false); if (Constants.NuiResultCode.SUCCESS != ret) { Log.i(TAG, "start tts failed " + ret); return false; @@ -132,11 +133,11 @@ public class MainActivity extends FlutterActivity { } } - private void sendTts(String text) { + private void send(String text) { streamInputTtsInstance.sendStreamInputTts(text); } - private void stopTts() { + private void stop() { streamInputTtsInstance.cancelStreamInputTts(); mAudioTrack.stop(); } @@ -144,20 +145,16 @@ public class MainActivity extends FlutterActivity { private String genTicket() { String str = ""; try { - //获取账号访问凭证: - Auth.GetTicketMethod method = Auth.GetTicketMethod.GET_TOKEN_IN_CLIENT_FOR_ONLINE_FEATURES; + Auth.GetTicketMethod method = Auth.GetTicketMethod.GET_ACCESS_IN_CLIENT_FOR_ONLINE_FEATURES; Auth.setAppKey(APP_KEY); - Auth.setToken(TOKEN); + Auth.setAccessKey(ACCESS_KEY); + Auth.setAccessKeySecret(ACCESS_KEY_SECRET); Log.i(TAG, "Use method:" + method); JSONObject object = Auth.getTicket(method); if (!object.containsKey("token") && !object.containsKey("sts_token")) { Log.e(TAG, "Cannot get token or sts_token!!!"); } object.put("url", URL); - - //过滤SDK内部日志通过回调送回到用户层 - object.put("log_track_level", String.valueOf(Constants.LogLevel.toInt(Constants.LogLevel.LOG_LEVEL_INFO))); - str = object.toString(); } catch (JSONException e) { e.printStackTrace(); @@ -166,15 +163,14 @@ public class MainActivity extends FlutterActivity { return str; } - private String genParameters() { + private String genParameters(boolean isChinese) { String str = ""; try { JSONObject object = new JSONObject(); - object.put("enable_subtitle", true); - object.put("voice", "zhixiaoxia"); + object.put("voice", isChinese ? "zhimiao_emo" : "beth"); object.put("format", "pcm"); object.put("sample_rate", 16000); - object.put("volume", 50); + object.put("volume", 100); object.put("speech_rate", 0); object.put("pitch_rate", 0); str = object.toString(); diff --git a/lib/services/chat_sse_service.dart b/lib/services/chat_sse_service.dart index cb24242..8f52997 100644 --- a/lib/services/chat_sse_service.dart +++ b/lib/services/chat_sse_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:ai_chat_assistant/utils/tts_util.dart'; import 'package:flutter/services.dart'; import '../utils/common_util.dart'; @@ -16,15 +17,20 @@ class ChatSseService { HttpClient? _currentClient; HttpClientResponse? _currentResponse; bool _isAborted = true; - bool isTtsStarted = false; + bool _isFirstData = true; + bool _isTtsStarted = false; String? get conversationId => _cachedConversationId; + Future? _startTtsFuture; + void request({ required String messageId, required String text, - required Function(String, String, String, bool) onStreamResponse, + required bool isChinese, + required Function(String, String, bool) onStreamResponse, }) async { + print("----------------------SSE Start"); _isAborted = false; if (_cachedUserId == null) { _cachedUserId = _generateRandomUserId(6); @@ -48,25 +54,17 @@ class ChatSseService { request.add(utf8.encode(json.encode(body))); _currentResponse = await request.close(); if (_currentResponse!.statusCode == 200) { - bool isFirst = true; + _startTtsFuture = startTts(isChinese); await for (final line in _currentResponse! .transform(utf8.decoder) .transform(const LineSplitter())) { if (_isAborted) { break; } - if (isFirst) { - if (!isTtsStarted) { - if (await _channel.invokeMethod("startTts") == true) { - isTtsStarted = true; - } - } - isFirst = false; - } if (line.startsWith('data:')) { final jsonStr = line.substring(5).trim(); if (jsonStr == '[DONE]' || jsonStr.contains('message_end')) { - onStreamResponse(messageId, responseText, tempText, true); + onStreamResponse(messageId, responseText, true); break; } try { @@ -81,13 +79,21 @@ class ChatSseService { if (_isAborted) { break; } - _channel.invokeMethod("sendTts", {"text": CommonUtil.cleanText(textChunk, true)}); + tempText += textChunk; + int endIndex = _getCompleteTextEndIndex(tempText); + String completeText = CommonUtil.cleanText(tempText.substring(0, endIndex).trim(), true); + if (completeText.isNotEmpty) { + if (_isFirstData && _startTtsFuture != null) { + await _startTtsFuture; + _isFirstData = false; + } + print("----------------------Send text: " + completeText); + completeText += isChinese ? ',' : ','; + TtsUtil.send(completeText); + } + tempText = tempText.substring(endIndex).trim(); responseText += textChunk; - // tempText += textChunk; - // int endIndex = _getCompleteTextEndIndex(tempText); - // final completeText = tempText.substring(0, endIndex).trim(); - // tempText = tempText.substring(endIndex).trim(); - onStreamResponse(messageId, responseText, "completeText", false); + onStreamResponse(messageId, responseText, false); } } catch (e) { print('解析 SSE 数据出错: $e, 原始数据: $jsonStr'); @@ -100,15 +106,22 @@ class ChatSseService { } catch (e) { // todo } finally { - isTtsStarted = false; resetRequest(); } } + Future startTts(bool isChinese) async { + print("----------------------TTS Start"); + if (!_isTtsStarted) { + if (await TtsUtil.start(isChinese) == true) { + _isTtsStarted = true; + } + } + } + Future abort() async { _isAborted = true; - _channel.invokeMethod("stopTts"); - isTtsStarted = false; + TtsUtil.stopTts(); resetRequest(); } @@ -119,11 +132,13 @@ class ChatSseService { _currentClient?.close(force: true); _currentClient = null; _currentResponse = null; + _isFirstData = true; + _isTtsStarted = false; } int _getCompleteTextEndIndex(String buffer) { // 支持句号、问号、感叹号和换行符作为分割依据 - final sentenceEnders = RegExp(r'[。!?\n]'); + final sentenceEnders = RegExp(r'[,!?:,。!?:\n]'); final matches = sentenceEnders.allMatches(buffer); return matches.isEmpty ? 0 : matches.last.end; } diff --git a/lib/services/message_service.dart b/lib/services/message_service.dart index 5d1627f..6ab5331 100644 --- a/lib/services/message_service.dart +++ b/lib/services/message_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:ai_chat_assistant/utils/common_util.dart'; +import 'package:ai_chat_assistant/utils/tts_util.dart'; import 'package:basic_intl/intl.dart'; import 'package:flutter/foundation.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -15,7 +16,6 @@ import '../services/classification_service.dart'; import '../services/control_recognition_service.dart'; import '../services/audio_recorder_service.dart'; import '../services/voice_recognition_service.dart'; -import '../services/tts_service.dart'; import 'command_service.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -213,10 +213,11 @@ class MessageService extends ChangeNotifier { id: _latestAssistantMessageId!, text: vehicleCommandResponse.tips!, status: MessageStatus.executing); - // if (!_isReplyAborted) { - // _ttsService.pushTextForStreamTTS(vehicleCommandResponse.tips!); - // _ttsService.markSSEStreamCompleted(); - // } + if (!_isReplyAborted) { + if (await TtsUtil.start(isChinese) == true) { + TtsUtil.send(vehicleCommandResponse.tips!); + } + } bool containOpenAC = false; for (var command in vehicleCommandResponse.commands) { if (_isReplyAborted) { @@ -309,6 +310,7 @@ class MessageService extends ChangeNotifier { _chatSseService.request( messageId: _latestAssistantMessageId!, text: text, + isChinese: isChinese, onStreamResponse: handleStreamResponse, ); } @@ -325,26 +327,18 @@ class MessageService extends ChangeNotifier { status: MessageStatus.normal); } - void handleStreamResponse(String messageId, String responseText, - String completeText, bool isComplete) { + void handleStreamResponse(String messageId, String responseText, bool isComplete) { if (_isReplyAborted) { return; } try { if (isComplete) { - // if (completeText.isNotEmpty) { - // _ttsService.pushTextForStreamTTS(completeText); - // } - // _ttsService.markSSEStreamCompleted(); replaceMessage( id: messageId, text: responseText, status: MessageStatus.completed, ); } else { - // if (completeText.isNotEmpty) { - // _ttsService.pushTextForStreamTTS(completeText); - // } replaceMessage( id: messageId, text: responseText, status: MessageStatus.thinking); } @@ -355,7 +349,6 @@ class MessageService extends ChangeNotifier { Future abortReply() async { _isReplyAborted = true; - // _ttsService.stop(); _chatSseService.abort(); int index = findMessageIndexById(_latestAssistantMessageId); if (index == -1 || messages[index].status != MessageStatus.thinking) { diff --git a/lib/utils/tts_util.dart b/lib/utils/tts_util.dart new file mode 100644 index 0000000..ba4d842 --- /dev/null +++ b/lib/utils/tts_util.dart @@ -0,0 +1,23 @@ +import 'package:flutter/services.dart'; + +class TtsUtil { + static const MethodChannel _channel = + MethodChannel('com.example.ai_chat_assistant/tts'); + + static Future execute(String method, + [Map? arguments]) { + return _channel.invokeMethod(method, arguments); + } + + static Future start(bool isChinese) async { + return await execute('start', {'isChinese': isChinese}); + } + + static Future send(String text) async { + await execute('send', {'text': text}); + } + + static Future stopTts() async { + await execute('stop'); + } +}