371 lines
12 KiB
Dart
371 lines
12 KiB
Dart
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';
|
||
import 'package:uuid/uuid.dart';
|
||
import '../enums/vehicle_command_type.dart';
|
||
import '../enums/message_service_state.dart';
|
||
import '../enums/message_status.dart';
|
||
import '../models/chat_message.dart';
|
||
import '../models/vehicle_cmd.dart';
|
||
import '../services/chat_sse_service.dart';
|
||
import '../services/classification_service.dart';
|
||
import '../services/control_recognition_service.dart';
|
||
import '../services/audio_recorder_service.dart';
|
||
import '../services/voice_recognition_service.dart';
|
||
import 'command_service.dart';
|
||
import 'package:fluttertoast/fluttertoast.dart';
|
||
|
||
class MessageService extends ChangeNotifier {
|
||
static final MessageService _instance = MessageService._internal();
|
||
|
||
factory MessageService() => _instance;
|
||
|
||
MessageService._internal();
|
||
|
||
final ChatSseService _chatSseService = ChatSseService();
|
||
// final LocalTtsService _ttsService = LocalTtsService();
|
||
final AudioRecorderService _audioService = AudioRecorderService();
|
||
final VoiceRecognitionService _recognitionService = VoiceRecognitionService();
|
||
final TextClassificationService _classificationService =
|
||
TextClassificationService();
|
||
final VehicleCommandService _vehicleCommandService = VehicleCommandService();
|
||
|
||
final List<ChatMessage> _messages = [];
|
||
|
||
List<ChatMessage> get messages => List.unmodifiable(_messages);
|
||
|
||
String? _latestUserMessageId;
|
||
String? _latestAssistantMessageId;
|
||
|
||
MessageServiceState _state = MessageServiceState.idle;
|
||
|
||
bool get isRecording => _state == MessageServiceState.recording;
|
||
|
||
bool get isRecognizing => _state == MessageServiceState.recognizing;
|
||
|
||
bool get isReplying => _state == MessageServiceState.replying;
|
||
|
||
bool get isBusy => _state != MessageServiceState.idle;
|
||
|
||
bool _isReplyAborted = true;
|
||
|
||
void initializeEmpty() {
|
||
_messages.clear();
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> startVoiceInput() async {
|
||
if (await Permission.microphone.status == PermissionStatus.denied) {
|
||
PermissionStatus status = await Permission.microphone.request();
|
||
if (status == PermissionStatus.permanentlyDenied) {
|
||
await Fluttertoast.showToast(
|
||
msg: Intl.getCurrentLocale().startsWith('zh')
|
||
? "请在设置中开启麦克风权限"
|
||
: "Please enable microphone in the settings");
|
||
}
|
||
return;
|
||
}
|
||
if (_state != MessageServiceState.idle) {
|
||
return;
|
||
}
|
||
try {
|
||
abortReply();
|
||
_latestUserMessageId = null;
|
||
_latestAssistantMessageId = null;
|
||
_isReplyAborted = false;
|
||
changeState(MessageServiceState.recording);
|
||
await _audioService.startRecording();
|
||
addMessage("", true, MessageStatus.listening);
|
||
_latestUserMessageId = messages.last.id;
|
||
} catch (e) {
|
||
print('录音开始出错: $e');
|
||
}
|
||
}
|
||
|
||
void changeState(MessageServiceState state) {
|
||
_state = state;
|
||
notifyListeners();
|
||
}
|
||
|
||
void addMessage(String text, bool isUser, MessageStatus status) {
|
||
_messages.add(ChatMessage(
|
||
id: Uuid().v1(),
|
||
text: text,
|
||
isUser: isUser,
|
||
timestamp: DateTime.now(),
|
||
status: status));
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> stopAndProcessVoiceInput() async {
|
||
if (_state != MessageServiceState.recording) {
|
||
changeState(MessageServiceState.idle);
|
||
return;
|
||
}
|
||
try {
|
||
changeState(MessageServiceState.recognizing);
|
||
final audioData = await _audioService.stopRecording();
|
||
replaceMessage(
|
||
id: _latestUserMessageId!,
|
||
text: "",
|
||
status: MessageStatus.recognizing);
|
||
if (audioData == null || audioData.isEmpty) {
|
||
removeMessageById(_latestUserMessageId!);
|
||
return;
|
||
}
|
||
final recognizedText =
|
||
await _recognitionService.recognizeSpeech(audioData);
|
||
if (recognizedText == null || recognizedText.isEmpty) {
|
||
removeMessageById(_latestUserMessageId!);
|
||
return;
|
||
}
|
||
replaceMessage(
|
||
id: _latestUserMessageId!,
|
||
text: recognizedText,
|
||
status: MessageStatus.normal);
|
||
changeState(MessageServiceState.replying);
|
||
await reply(recognizedText);
|
||
} catch (e) {
|
||
// todo
|
||
} finally {
|
||
changeState(MessageServiceState.idle);
|
||
}
|
||
}
|
||
|
||
int findMessageIndexById(String? id) {
|
||
return id == null ? -1 : _messages.indexWhere((msg) => msg.id == id);
|
||
}
|
||
|
||
void replaceMessage({
|
||
required String id,
|
||
String? text,
|
||
MessageStatus? status,
|
||
}) {
|
||
final index = findMessageIndexById(id);
|
||
if (index == -1) {
|
||
return;
|
||
}
|
||
final message = _messages[index];
|
||
_messages[index] = ChatMessage(
|
||
id: message.id,
|
||
text: text ?? message.text,
|
||
isUser: message.isUser,
|
||
timestamp: message.timestamp,
|
||
status: status ?? message.status,
|
||
);
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> reply(String text) async {
|
||
addMessage("", false, MessageStatus.thinking);
|
||
_latestAssistantMessageId = messages.last.id;
|
||
bool isChinese = CommonUtil.containChinese(text);
|
||
try {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
final category = await _classificationService.classifyText(text);
|
||
switch (category) {
|
||
case -1:
|
||
await occurError(isChinese);
|
||
break;
|
||
case 2:
|
||
await handleVehicleControl(text, isChinese);
|
||
break;
|
||
case 4:
|
||
await answerWrongQuestion(isChinese);
|
||
break;
|
||
default:
|
||
await answerQuestion(text, isChinese);
|
||
break;
|
||
}
|
||
} catch (e) {
|
||
await occurError(isChinese);
|
||
}
|
||
}
|
||
|
||
Future<void> handleVehicleControl(String text, bool isChinese) async {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
final vehicleCommandResponse =
|
||
await _vehicleCommandService.getCommandFromText(text);
|
||
if (vehicleCommandResponse == null) {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
String msg = isChinese
|
||
? "无法识别车辆控制命令,请重试"
|
||
: "Cannot recognize the vehicle control command, please try again";
|
||
replaceMessage(
|
||
id: _latestAssistantMessageId!,
|
||
text: msg,
|
||
status: MessageStatus.normal);
|
||
} else {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
replaceMessage(
|
||
id: _latestAssistantMessageId!,
|
||
text: vehicleCommandResponse.tips!,
|
||
status: MessageStatus.executing);
|
||
if (!_isReplyAborted) {
|
||
if (await TtsUtil.start(isChinese) == true) {
|
||
TtsUtil.send(vehicleCommandResponse.tips!);
|
||
}
|
||
}
|
||
bool containOpenAC = false;
|
||
for (var command in vehicleCommandResponse.commands) {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
if (command.type == VehicleCommandType.unknown ||
|
||
command.error.isNotEmpty) {
|
||
continue;
|
||
}
|
||
if (command.type == VehicleCommandType.openAC) {
|
||
containOpenAC = true;
|
||
}
|
||
if (containOpenAC && command.type == VehicleCommandType.changeACTemp) {
|
||
await Future.delayed(const Duration(milliseconds: 2000),
|
||
() => processCommand(command, isChinese));
|
||
} else {
|
||
await processCommand(command, isChinese);
|
||
}
|
||
}
|
||
replaceMessage(
|
||
id: _latestAssistantMessageId!,
|
||
text: vehicleCommandResponse.tips!,
|
||
status: MessageStatus.normal);
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
Future<void> processCommand(VehicleCommand command, bool isChinese) async {
|
||
String msg;
|
||
MessageStatus status;
|
||
final (isSuccess, result) = await CommandService.executeCommand(
|
||
command.type,
|
||
params: command.params);
|
||
if (command.type == VehicleCommandType.locateCar) {
|
||
msg = isChinese
|
||
? "很抱歉,定位车辆位置失败"
|
||
: "Sorry, locate the vehicle unsuccessfully";
|
||
status = MessageStatus.failure;
|
||
if (isSuccess && result != null && result.isNotEmpty) {
|
||
String address = result['address'];
|
||
if (address.isNotEmpty) {
|
||
msg = isChinese
|
||
? "已为您找到车辆位置:\"$address\""
|
||
: "The vehicle location is \"$address\"";
|
||
status = MessageStatus.success;
|
||
}
|
||
}
|
||
} else {
|
||
if (isSuccess) {
|
||
msg = isChinese
|
||
? "为您执行\"${command.type.chinese}\"成功"
|
||
: "Execute \"${command.type.english}\" successful";
|
||
status = MessageStatus.success;
|
||
} else {
|
||
msg = isChinese
|
||
? "很抱歉,\"${command.type.chinese}\"失败"
|
||
: "Sorry, execute \"${command.type.english}\" unsuccessfully";
|
||
status = MessageStatus.failure;
|
||
}
|
||
}
|
||
addMessage(msg, false, status);
|
||
}
|
||
|
||
Future<void> answerWrongQuestion(bool isChinese) async {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
const chineseAnswer = "尊敬的ID.UNYX车主:\n" +
|
||
"很抱歉,我暂时无法理解您的需求,请您更详细地描述,以便我能更好地为您服务。\n" +
|
||
"作为您的智能助手,我专注于以下服务:\n" +
|
||
"1. ID.UNYX车型功能咨询,例如:如何调节座椅记忆功能?\n" +
|
||
"2. 日常用车问题解答,例如:充电注意事项有哪些?\n" +
|
||
"3. 基础车辆控制协助,例如:帮我打开空调。";
|
||
const englishAnswer = "Dear ID.UNYX Owner,\n" +
|
||
"I'm sorry that unable to understand your request currently. Please describe it in more detail so that I can better assist you.\n" +
|
||
"As your intelligent assistant, I specialize in the following services:\n" +
|
||
"1. ID.UNYX model feature consultation, such as: How do I adjust the seat memory function?\n" +
|
||
"2. Daily vehicle usage inquiries, such as: What are the charging precautions?\n" +
|
||
"3. Basic vehicle control assistance, such as: Help me turn on the air conditioning.";
|
||
replaceMessage(
|
||
id: _latestAssistantMessageId!,
|
||
text: isChinese ? chineseAnswer : englishAnswer,
|
||
status: MessageStatus.normal);
|
||
}
|
||
|
||
Future<void> answerQuestion(String text, isChinese) async {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
_chatSseService.request(
|
||
messageId: _latestAssistantMessageId!,
|
||
text: text,
|
||
isChinese: isChinese,
|
||
onStreamResponse: handleStreamResponse,
|
||
);
|
||
}
|
||
|
||
Future<void> occurError(bool isChinese) async {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
replaceMessage(
|
||
id: _latestAssistantMessageId!,
|
||
text: isChinese
|
||
? "很抱歉,众众暂时无法回答这个问题,请重试"
|
||
: "Sorry, I can not answer this question for the moment, please try again",
|
||
status: MessageStatus.normal);
|
||
}
|
||
|
||
void handleStreamResponse(String messageId, String responseText, bool isComplete) {
|
||
if (_isReplyAborted) {
|
||
return;
|
||
}
|
||
try {
|
||
if (isComplete) {
|
||
replaceMessage(
|
||
id: messageId,
|
||
text: responseText,
|
||
status: MessageStatus.completed,
|
||
);
|
||
} else {
|
||
replaceMessage(
|
||
id: messageId, text: responseText, status: MessageStatus.thinking);
|
||
}
|
||
} catch (e) {
|
||
abortReply();
|
||
}
|
||
}
|
||
|
||
Future<void> abortReply() async {
|
||
_isReplyAborted = true;
|
||
_chatSseService.abort();
|
||
int index = findMessageIndexById(_latestAssistantMessageId);
|
||
if (index == -1 || messages[index].status != MessageStatus.thinking) {
|
||
return;
|
||
}
|
||
replaceMessage(
|
||
id: _latestAssistantMessageId!, status: MessageStatus.aborted);
|
||
}
|
||
|
||
void removeMessageById(String id) {
|
||
_messages.removeWhere((msg) => msg.id == id);
|
||
notifyListeners();
|
||
}
|
||
|
||
void clearMessages() {
|
||
_messages.clear();
|
||
notifyListeners();
|
||
}
|
||
}
|