0812
This commit is contained in:
83
lib/services/audio_recorder_service.dart
Normal file
83
lib/services/audio_recorder_service.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:record/record.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
// 负责录音相关功能
|
||||
class AudioRecorderService {
|
||||
final AudioRecorder _recorder = AudioRecorder();
|
||||
bool _isRecording = false;
|
||||
String? _tempFilePath;
|
||||
|
||||
bool get isRecording => _isRecording;
|
||||
|
||||
Future<bool> hasPermission() => _recorder.hasPermission();
|
||||
|
||||
Future<void> checkPermission() async {
|
||||
final hasPermission = await _recorder.hasPermission();
|
||||
print('麦克风权限状态: $hasPermission');
|
||||
}
|
||||
|
||||
Future<void> startRecording() async {
|
||||
print('开始录音...');
|
||||
|
||||
try {
|
||||
// 使用文件录音可能更稳定
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
_tempFilePath =
|
||||
'${tempDir.path}/temp_audio_${DateTime.now().millisecondsSinceEpoch}.opus';
|
||||
|
||||
print('录音文件路径: $_tempFilePath');
|
||||
|
||||
if (await _recorder.hasPermission()) {
|
||||
await _recorder.start(
|
||||
const RecordConfig(encoder: AudioEncoder.opus),
|
||||
path: _tempFilePath!,
|
||||
);
|
||||
|
||||
_isRecording = true;
|
||||
print('录音已开始,使用OPUS格式,文件: $_tempFilePath');
|
||||
} else {
|
||||
print('没有麦克风权限,无法开始录音');
|
||||
}
|
||||
} catch (e) {
|
||||
print('录音开始出错: $e');
|
||||
_isRecording = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<int>?> stopRecording() async {
|
||||
if (!_isRecording) return null;
|
||||
|
||||
_isRecording = false;
|
||||
|
||||
print('停止录音...');
|
||||
try {
|
||||
final path = await _recorder.stop();
|
||||
print('录音已停止,文件路径: $path');
|
||||
|
||||
if (path != null) {
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
final bytes = await file.readAsBytes();
|
||||
print('读取到录音数据: ${bytes.length} 字节');
|
||||
|
||||
if (bytes.isNotEmpty) {
|
||||
return bytes;
|
||||
} else {
|
||||
print('录音数据为空');
|
||||
}
|
||||
} else {
|
||||
print('录音文件不存在: $path');
|
||||
}
|
||||
} else {
|
||||
print('录音路径为空');
|
||||
}
|
||||
} catch (e) {
|
||||
print('停止录音或发送过程出错: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void dispose() => _recorder.dispose();
|
||||
}
|
||||
123
lib/services/chat_sse_service.dart
Normal file
123
lib/services/chat_sse_service.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
// 负责SSE通信
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
class ChatSseService {
|
||||
// 缓存用户ID和会话ID
|
||||
String? _cachedUserId;
|
||||
String? _cachedConversationId;
|
||||
|
||||
HttpClient? _currentClient;
|
||||
HttpClientResponse? _currentResponse;
|
||||
bool _isAborted = true;
|
||||
|
||||
String? get conversationId => _cachedConversationId;
|
||||
|
||||
void request({
|
||||
required String messageId,
|
||||
required String text,
|
||||
required Function(String, String, String, bool) onStreamResponse,
|
||||
}) async {
|
||||
_isAborted = false;
|
||||
if (_cachedUserId == null) {
|
||||
_cachedUserId = _generateRandomUserId(6);
|
||||
print('初始化用户ID: $_cachedUserId');
|
||||
}
|
||||
String responseText = '';
|
||||
String tempText = '';
|
||||
_currentClient = HttpClient();
|
||||
try {
|
||||
final chatUri = Uri.parse('http://143.64.185.20:18606/chat');
|
||||
final request = await _currentClient!.postUrl(chatUri);
|
||||
request.headers.set('Content-Type', 'application/json');
|
||||
request.headers.set('Accept', 'text/event-stream');
|
||||
final body = {
|
||||
'message': text,
|
||||
'user': _cachedUserId,
|
||||
};
|
||||
if (_cachedConversationId != null) {
|
||||
body['conversation_id'] = _cachedConversationId;
|
||||
}
|
||||
request.add(utf8.encode(json.encode(body)));
|
||||
_currentResponse = await request.close();
|
||||
if (_currentResponse!.statusCode == 200) {
|
||||
await for (final line in _currentResponse!
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())) {
|
||||
if (_isAborted) {
|
||||
break;
|
||||
}
|
||||
if (line.startsWith('data:')) {
|
||||
final jsonStr = line.substring(5).trim();
|
||||
if (jsonStr == '[DONE]' || jsonStr.contains('message_end')) {
|
||||
onStreamResponse(messageId, responseText, tempText, true);
|
||||
break;
|
||||
}
|
||||
try {
|
||||
final jsonData = json.decode(jsonStr);
|
||||
if (jsonData.containsKey('conversation_id') &&
|
||||
_cachedConversationId == null) {
|
||||
_cachedConversationId = jsonData['conversation_id'];
|
||||
}
|
||||
if (jsonData['event'].toString().contains('message')) {
|
||||
final textChunk =
|
||||
jsonData.containsKey('answer') ? jsonData['answer'] : '';
|
||||
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);
|
||||
}
|
||||
} catch (e) {
|
||||
print('解析 SSE 数据出错: $e, 原始数据: $jsonStr');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print('SSE 连接失败,状态码: ${_currentResponse!.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
// todo
|
||||
} finally {
|
||||
resetRequest();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> abort() async {
|
||||
_isAborted = true;
|
||||
resetRequest();
|
||||
}
|
||||
|
||||
void resetRequest() {
|
||||
_currentResponse?.detachSocket().then((socket) {
|
||||
socket.destroy();
|
||||
});
|
||||
_currentClient?.close(force: true);
|
||||
_currentClient = null;
|
||||
_currentResponse = null;
|
||||
}
|
||||
|
||||
int _getCompleteTextEndIndex(String buffer) {
|
||||
// 支持句号、问号、感叹号和换行符作为分割依据
|
||||
final sentenceEnders = RegExp(r'[。!?\n]');
|
||||
final matches = sentenceEnders.allMatches(buffer);
|
||||
return matches.isEmpty ? 0 : matches.last.end;
|
||||
}
|
||||
|
||||
// 新增:重置会话方法
|
||||
void resetSession() {
|
||||
_cachedUserId = null;
|
||||
_cachedConversationId = null;
|
||||
print('SSE会话已重置');
|
||||
}
|
||||
|
||||
String _generateRandomUserId(int length) {
|
||||
const chars =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
final random = Random();
|
||||
return String.fromCharCodes(Iterable.generate(
|
||||
length, (_) => chars.codeUnitAt(random.nextInt(chars.length))));
|
||||
}
|
||||
}
|
||||
27
lib/services/classification_service.dart
Normal file
27
lib/services/classification_service.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class TextClassificationService {
|
||||
Future<int> classifyText(String text) async {
|
||||
try {
|
||||
final uri = Uri.parse('http://143.64.185.20:18606/classify');
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({'text': text}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body)['category'];
|
||||
} else {
|
||||
print(
|
||||
'Classification failed: ${response.statusCode}, ${response.body}');
|
||||
return -1;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error during text classification: $e');
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
lib/services/command_service.dart
Normal file
126
lib/services/command_service.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import '../enums/vehicle_command_type.dart';
|
||||
import '../models/vehicle_status_info.dart';
|
||||
|
||||
/// 命令处理回调函数定义
|
||||
typedef CommandCallback = Future<(bool, Map<String, dynamic>? params)> Function(
|
||||
VehicleCommandType type, Map<String, dynamic>? params);
|
||||
|
||||
/// 命令处理器类 - 负责处理来自AI的车辆控制命令
|
||||
class CommandService {
|
||||
/// 保存主应用注册的回调函数
|
||||
static CommandCallback? onCommandReceived;
|
||||
|
||||
/// 注册命令处理回调
|
||||
static void registerCallback(CommandCallback callback) {
|
||||
onCommandReceived = callback;
|
||||
}
|
||||
|
||||
/// 执行车控命令
|
||||
/// 返回命令执行是否成功
|
||||
static Future<(bool, Map<String, dynamic>? params)> executeCommand(
|
||||
VehicleCommandType type,
|
||||
{Map<String, dynamic>? params}) async {
|
||||
if (onCommandReceived != null) {
|
||||
try {
|
||||
return await onCommandReceived!(type, params);
|
||||
} catch (e) {
|
||||
print('执行命令出错: $e');
|
||||
}
|
||||
} else {
|
||||
print('警告: 命令处理回调未注册');
|
||||
}
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
static bool checkState(
|
||||
VehicleCommandType type, Map<String, dynamic>? params) {
|
||||
// 获取车辆状态信息
|
||||
VehicleStatusInfo vehicleStatusInfo = VehicleStatusInfo();
|
||||
switch (type) {
|
||||
// 车辆开锁
|
||||
case VehicleCommandType.lock:
|
||||
return vehicleStatusInfo.leftFrontLockState &&
|
||||
vehicleStatusInfo.rightFrontLockState &&
|
||||
vehicleStatusInfo.leftRearLockState &&
|
||||
vehicleStatusInfo.rightRearLockState;
|
||||
// 车辆关锁
|
||||
case VehicleCommandType.unlock:
|
||||
return !(vehicleStatusInfo.leftFrontLockState &&
|
||||
vehicleStatusInfo.rightFrontLockState &&
|
||||
vehicleStatusInfo.leftRearLockState &&
|
||||
vehicleStatusInfo.rightRearLockState);
|
||||
// 打开窗户
|
||||
case VehicleCommandType.openWindow:
|
||||
return vehicleStatusInfo.leftFrontWindowState &&
|
||||
vehicleStatusInfo.rightFrontWindowState &&
|
||||
vehicleStatusInfo.leftRearWindowState &&
|
||||
vehicleStatusInfo.rightRearWindowState;
|
||||
// 关闭窗户
|
||||
case VehicleCommandType.closeWindow:
|
||||
return !(vehicleStatusInfo.leftFrontWindowState &&
|
||||
vehicleStatusInfo.rightFrontWindowState &&
|
||||
vehicleStatusInfo.leftRearWindowState &&
|
||||
vehicleStatusInfo.rightRearWindowState);
|
||||
// 预约空调
|
||||
case VehicleCommandType.appointAC:
|
||||
// todo
|
||||
return false;
|
||||
// 打开空调
|
||||
case VehicleCommandType.openAC:
|
||||
return vehicleStatusInfo.acState && vehicleStatusInfo.acSwitch;
|
||||
// 关闭空调
|
||||
case VehicleCommandType.closeAC:
|
||||
return !vehicleStatusInfo.acState && !vehicleStatusInfo.acSwitch;
|
||||
// 极速降温
|
||||
case VehicleCommandType.coolSharply:
|
||||
// todo
|
||||
return false;
|
||||
// 一键备车
|
||||
case VehicleCommandType.prepareCar:
|
||||
return false;
|
||||
// 一键融雪
|
||||
case VehicleCommandType.meltSnow:
|
||||
// todo
|
||||
return false;
|
||||
// 打开后备箱
|
||||
case VehicleCommandType.openTrunk:
|
||||
return vehicleStatusInfo.trunkState;
|
||||
// 关闭后备箱
|
||||
case VehicleCommandType.closeTrunk:
|
||||
return !vehicleStatusInfo.trunkState;
|
||||
// 车辆鸣笛
|
||||
case VehicleCommandType.honk:
|
||||
// todo
|
||||
return false;
|
||||
// 寻找车辆
|
||||
case VehicleCommandType.locateCar:
|
||||
// todo
|
||||
return false;
|
||||
// 修改空调温度
|
||||
case VehicleCommandType.changeACTemp:
|
||||
return vehicleStatusInfo.acState &&
|
||||
vehicleStatusInfo.acSwitch &&
|
||||
vehicleStatusInfo.acTemp == params?['temperature'];
|
||||
// 开启方向盘加热
|
||||
case VehicleCommandType.openWheelHeat:
|
||||
return vehicleStatusInfo.wheelHeat;
|
||||
// 关闭方向盘加热
|
||||
case VehicleCommandType.closeWheelHeat:
|
||||
return !vehicleStatusInfo.wheelHeat;
|
||||
// 开启主座椅加热
|
||||
case VehicleCommandType.openMainSeatHeat:
|
||||
return vehicleStatusInfo.mainSeatHeat;
|
||||
// 关闭主座椅加热
|
||||
case VehicleCommandType.closeMainSeatHeat:
|
||||
return !vehicleStatusInfo.mainSeatHeat;
|
||||
// 开启副座椅加热
|
||||
case VehicleCommandType.openMinorSeatHeat:
|
||||
return vehicleStatusInfo.minorSeatHeat;
|
||||
// 关闭副座椅加热
|
||||
case VehicleCommandType.closeMinorSeatHeat:
|
||||
return !vehicleStatusInfo.minorSeatHeat;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
lib/services/control_recognition_service.dart
Normal file
69
lib/services/control_recognition_service.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/vehicle_cmd.dart';
|
||||
import '../models/vehicle_cmd_response.dart';
|
||||
import 'vehicle_state_service.dart';
|
||||
|
||||
class VehicleCommandService {
|
||||
// final VehicleStateService vehicleStateService = VehicleStateService();
|
||||
|
||||
Future<VehicleCommandResponse?> getCommandFromText(String text) async {
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'http://143.64.185.20:18606/control');
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({'text': text}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
final tips = decoded['tips'];
|
||||
final commandList = decoded['commands'];
|
||||
List<VehicleCommand> vehicleCommandList = (commandList as List)
|
||||
.map<VehicleCommand>((item) => VehicleCommand.fromString(
|
||||
item['command'] as String,
|
||||
item['params'] as Map<String, dynamic>?,
|
||||
item['error'] ?? ''))
|
||||
.toList();
|
||||
return VehicleCommandResponse(tips: tips, commands: vehicleCommandList);
|
||||
} else {
|
||||
print(
|
||||
'Vehicle command query failed: ${response.statusCode}, ${response.body}');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error during vehicle command processing: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行车辆控制命令的方法
|
||||
Future<bool> executeCommand(VehicleCommand command) async {
|
||||
try {
|
||||
final uri = Uri.parse('http://143.64.185.20:18607/executeCommand');
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({
|
||||
'command': command.commandString, // 使用getter转换为字符串
|
||||
'params': command.params,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('命令执行成功: ${command.commandString}');
|
||||
return true;
|
||||
} else {
|
||||
print('命令执行失败: ${response.statusCode}, ${response.body}');
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
print('命令执行出错: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
lib/services/location_service.dart
Normal file
20
lib/services/location_service.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'dart:convert';
|
||||
import 'package:basic_intl/intl.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class LocationService {
|
||||
String lang = Intl.getCurrentLocale().startsWith('zh') ? 'zh' : 'en';
|
||||
Future<String> search(double lon, double lat) async {
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
'http://143.64.185.20:18606/location_info?lon=${lon}&lat=${lat}&lang=${lang}');
|
||||
final response = await http.get(uri);
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body)['address'];
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error during text classification: $e');
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
381
lib/services/message_service.dart
Normal file
381
lib/services/message_service.dart
Normal file
@@ -0,0 +1,381 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ai_chat_assistant/utils/common_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 '../services/tts_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 = [
|
||||
ChatMessage(
|
||||
id: Uuid().v1(),
|
||||
text: Intl.getCurrentLocale().startsWith('zh')
|
||||
? "您好,我是众众,请问有什么可以帮您?"
|
||||
: "Hi, I'm Zhongzhong, may I help you ? ",
|
||||
isUser: false,
|
||||
timestamp: DateTime.now(),
|
||||
status: MessageStatus.normal)
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
_ttsService.pushTextForStreamTTS(vehicleCommandResponse.tips!);
|
||||
_ttsService.markSSEStreamCompleted();
|
||||
}
|
||||
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,
|
||||
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,
|
||||
String completeText, 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);
|
||||
}
|
||||
} catch (e) {
|
||||
abortReply();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> abortReply() async {
|
||||
_isReplyAborted = true;
|
||||
_ttsService.stop();
|
||||
_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();
|
||||
}
|
||||
}
|
||||
33
lib/services/redis_service.dart
Normal file
33
lib/services/redis_service.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class RedisService {
|
||||
static final RedisService _instance = RedisService._internal();
|
||||
|
||||
factory RedisService() => _instance;
|
||||
|
||||
RedisService._internal();
|
||||
|
||||
Future<void> setKeyValue(String key, Object value) async {
|
||||
final uri = Uri.parse('http://143.64.185.20:18606/redis/set');
|
||||
await http.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode({'key': key, 'value': value}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> getValue(String key) async {
|
||||
try {
|
||||
final uri = Uri.parse('http://143.64.185.20:18606/redis/get?key=$key');
|
||||
final response = await http.get(uri);
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
608
lib/services/tts_service.dart
Normal file
608
lib/services/tts_service.dart
Normal file
@@ -0,0 +1,608 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'package:ai_chat_assistant/utils/common_util.dart';
|
||||
|
||||
// ...existing code...
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
|
||||
class LocalTtsService {
|
||||
final FlutterTts _flutterTts = FlutterTts();
|
||||
bool _isPlaying = false;
|
||||
|
||||
// TTS 播放队列 - 存储文本段
|
||||
final Queue<String> _ttsQueue = Queue<String>();
|
||||
|
||||
// TTS 事件回调
|
||||
VoidCallback? _onStartHandler;
|
||||
VoidCallback? _onCompletionHandler;
|
||||
Function(String)? _onErrorHandler;
|
||||
|
||||
// 有序播放队列 - 确保播放顺序与文本顺序一致
|
||||
List<TtsTask> _orderedTasks = []; // 有序任务队列
|
||||
int _nextTaskIndex = 0; // 下一个要处理的任务索引
|
||||
bool _isProcessingTasks = false; // 是否正在处理任务
|
||||
bool _streamCompleted = false; // 流是否完成
|
||||
|
||||
LocalTtsService() {
|
||||
_initializeTts();
|
||||
}
|
||||
|
||||
/// 初始化TTS引擎
|
||||
Future<void> _initializeTts() async {
|
||||
try {
|
||||
// 获取可用的TTS引擎列表
|
||||
await _printAvailableEngines();
|
||||
|
||||
// 推荐并设置最佳引擎
|
||||
// await setEngine("com.k2fsa.sherpa.onnx.tts.engine");
|
||||
|
||||
// 设置TTS参数
|
||||
await _flutterTts.setLanguage("en-US"); // 默认英文
|
||||
await _flutterTts.setSpeechRate(0.5); // 语速
|
||||
await _flutterTts.setVolume(1.0); // 音量
|
||||
await _flutterTts.setPitch(1.0); // 音调
|
||||
|
||||
// 设置TTS事件回调
|
||||
_flutterTts.setStartHandler(() {
|
||||
print('TTS开始播放');
|
||||
_isPlaying = true;
|
||||
_onStartHandler?.call();
|
||||
});
|
||||
|
||||
_flutterTts.setCompletionHandler(() {
|
||||
print('TTS播放完成');
|
||||
_isPlaying = false;
|
||||
_handleTtsPlaybackComplete();
|
||||
});
|
||||
|
||||
_flutterTts.setErrorHandler((msg) {
|
||||
print('TTS错误: $msg');
|
||||
_isPlaying = false;
|
||||
_onErrorHandler?.call(msg);
|
||||
_handleTtsPlaybackComplete(); // 错误时也要处理下一个任务
|
||||
});
|
||||
|
||||
print('TTS引擎初始化完成');
|
||||
} catch (e) {
|
||||
print('TTS引擎初始化失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置最佳TTS引擎
|
||||
// 已移除TtsEngineManager相关代码,避免未定义错误。
|
||||
|
||||
/// 打印可用的TTS引擎
|
||||
Future<void> _printAvailableEngines() async {
|
||||
try {
|
||||
// 获取可用的TTS引擎
|
||||
dynamic engines = await _flutterTts.getEngines;
|
||||
print('可用的TTS引擎: $engines');
|
||||
|
||||
// 获取可用的语言
|
||||
dynamic languages = await _flutterTts.getLanguages;
|
||||
print('支持的语言: $languages');
|
||||
|
||||
// 获取当前默认引擎
|
||||
dynamic defaultEngine = await _flutterTts.getDefaultEngine;
|
||||
print('当前默认引擎: $defaultEngine');
|
||||
} catch (e) {
|
||||
print('获取TTS引擎信息失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有队列和缓存
|
||||
void clearAll() {
|
||||
_ttsQueue.clear();
|
||||
_orderedTasks.clear();
|
||||
_nextTaskIndex = 0;
|
||||
_isProcessingTasks = false;
|
||||
_streamCompleted = false;
|
||||
_isPlaying = false; // 重置播放状态
|
||||
print('所有TTS队列和缓存已清空');
|
||||
}
|
||||
|
||||
/// 推送文本到TTS队列进行流式处理(保证顺序)
|
||||
void pushTextForStreamTTS(String text) {
|
||||
if (text.trim().isEmpty) {
|
||||
print('接收到空字符串,跳过推送到TTS队列');
|
||||
return;
|
||||
}
|
||||
|
||||
String cleanedText = CommonUtil.cleanText(text, true);
|
||||
if (cleanedText.isEmpty) {
|
||||
print('清理后的文本为空,跳过推送到TTS队列');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建有序任务
|
||||
int taskIndex = _orderedTasks.length;
|
||||
TtsTask task = TtsTask(
|
||||
index: taskIndex,
|
||||
text: cleanedText,
|
||||
status: TtsTaskStatus.ready,
|
||||
);
|
||||
|
||||
_orderedTasks.add(task);
|
||||
print('添加TTS任务 (索引: $taskIndex): $cleanedText');
|
||||
|
||||
// 尝试处理队列中的下一个任务
|
||||
_processNextReadyTask();
|
||||
}
|
||||
|
||||
/// 处理队列中下一个就绪的任务
|
||||
void _processNextReadyTask() {
|
||||
// 如果正在播放,则不处理下一个任务
|
||||
if (_isPlaying) {
|
||||
print('当前正在播放音频,等待播放完成');
|
||||
return;
|
||||
}
|
||||
|
||||
// 寻找下一个可以播放的任务
|
||||
while (_nextTaskIndex < _orderedTasks.length) {
|
||||
TtsTask task = _orderedTasks[_nextTaskIndex];
|
||||
if (task.status == TtsTaskStatus.ready) {
|
||||
print('按顺序播放TTS (索引: ${task.index}): ${task.text}');
|
||||
_playTts(task.text);
|
||||
return; // 等待当前音频播放完成
|
||||
} else {
|
||||
// 跳过已处理的任务
|
||||
_nextTaskIndex++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否所有任务都已处理完
|
||||
if (_streamCompleted && _nextTaskIndex >= _orderedTasks.length) {
|
||||
print('=== 所有TTS任务播放完成 ===');
|
||||
_isProcessingTasks = false;
|
||||
_onCompletionHandler?.call();
|
||||
}
|
||||
}
|
||||
|
||||
/// 播放TTS
|
||||
Future<void> _playTts(String text) async {
|
||||
if (_isPlaying) {
|
||||
print('已有TTS正在播放,跳过本次播放');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
print('开始播放TTS: $text');
|
||||
// 检测语言并设置
|
||||
bool isChinese = CommonUtil.containChinese(text);
|
||||
String targetLanguage = isChinese ? "zh-CN" : "en-US";
|
||||
// 检查语言是否可用,如果不可用则使用默认语言
|
||||
bool isLanguageOk = await isLanguageAvailable(targetLanguage);
|
||||
if (!isLanguageOk) {
|
||||
print('语言 $targetLanguage 不可用,使用默认语言');
|
||||
// 可以在这里设置备用语言或保持当前语言
|
||||
} else {
|
||||
await _flutterTts.setLanguage(targetLanguage);
|
||||
print('设置TTS语言为: $targetLanguage');
|
||||
}
|
||||
// 播放TTS
|
||||
await _flutterTts.speak(text);
|
||||
} catch (e) {
|
||||
print('播放TTS失败: $e');
|
||||
// 播放失败,重置状态并继续下一个
|
||||
_isPlaying = false;
|
||||
_handleTtsPlaybackComplete();
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理TTS播放完成
|
||||
void _handleTtsPlaybackComplete() {
|
||||
if (!_isPlaying) {
|
||||
print('TTS已停止播放,处理下一个任务');
|
||||
// 标记当前任务为已完成
|
||||
if (_nextTaskIndex < _orderedTasks.length &&
|
||||
_orderedTasks[_nextTaskIndex].status == TtsTaskStatus.ready) {
|
||||
_orderedTasks[_nextTaskIndex].status = TtsTaskStatus.completed;
|
||||
_nextTaskIndex++;
|
||||
}
|
||||
// 处理下一个任务
|
||||
_processNextReadyTask();
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记流完成
|
||||
void markSSEStreamCompleted() {
|
||||
print('=== 标记SSE流完成 ===');
|
||||
_streamCompleted = true;
|
||||
|
||||
// 尝试完成剩余任务
|
||||
_processNextReadyTask();
|
||||
}
|
||||
|
||||
/// 停止播放
|
||||
void stopSSEPlayback() {
|
||||
print("停止SSE播放");
|
||||
// 立即停止TTS播放,无论当前是否在播报
|
||||
_flutterTts.stop();
|
||||
_isPlaying = false;
|
||||
clearAll();
|
||||
}
|
||||
|
||||
// 设置事件处理器
|
||||
void setStartHandler(VoidCallback handler) {
|
||||
_onStartHandler = handler;
|
||||
}
|
||||
|
||||
void setCompletionHandler(VoidCallback handler) {
|
||||
_onCompletionHandler = handler;
|
||||
}
|
||||
|
||||
void setErrorHandler(Function(String) handler) {
|
||||
_onErrorHandler = handler;
|
||||
}
|
||||
|
||||
// 保留原有的播放逻辑用于完整文本处理
|
||||
Future<void> processCompleteText(String text) async {
|
||||
if (text.trim().isEmpty) {
|
||||
print("文本为空,跳过TTS处理");
|
||||
return;
|
||||
}
|
||||
|
||||
print("开始处理完整文本TTS,原始文本长度: ${text.length}字");
|
||||
|
||||
// 清理缓存和重置队列
|
||||
clearAll();
|
||||
|
||||
// 确保停止当前播放
|
||||
await _stopCurrentPlayback();
|
||||
|
||||
// 直接使用流式处理
|
||||
await _processCompleteTextAsStream(text);
|
||||
}
|
||||
|
||||
/// 将完整文本作为流式处理
|
||||
Future<void> _processCompleteTextAsStream(String text) async {
|
||||
// 清理markdown和异常字符
|
||||
String cleanedText = _cleanTextForTts(text);
|
||||
print("清理后文本长度: ${cleanedText.length}字");
|
||||
|
||||
if (cleanedText.trim().isEmpty) {
|
||||
print("清理后文本为空,跳过TTS");
|
||||
return;
|
||||
}
|
||||
|
||||
// 分割成句子并逐个处理
|
||||
List<String> sentences = _splitTextIntoSentences(cleanedText);
|
||||
print("=== 文本分段完成,共 ${sentences.length} 个句子 ===");
|
||||
|
||||
// 推送每个句子到流式TTS处理
|
||||
for (int i = 0; i < sentences.length; i++) {
|
||||
final sentence = sentences[i].trim();
|
||||
if (sentence.isEmpty) continue;
|
||||
|
||||
print("处理句子 ${i + 1}/${sentences.length}: $sentence");
|
||||
pushTextForStreamTTS(sentence);
|
||||
}
|
||||
|
||||
// 标记流完成
|
||||
markSSEStreamCompleted();
|
||||
}
|
||||
|
||||
// 停止当前播放的辅助方法
|
||||
Future<void> _stopCurrentPlayback() async {
|
||||
try {
|
||||
print("停止当前播放");
|
||||
|
||||
if (_isPlaying) {
|
||||
await _flutterTts.stop();
|
||||
_isPlaying = false;
|
||||
print("TTS.stop() 调用完成");
|
||||
}
|
||||
|
||||
// 短暂延迟确保播放器状态稳定
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
print("播放器状态稳定延迟完成");
|
||||
} catch (e) {
|
||||
print('停止当前播放错误: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 保留原有的方法用于兼容性
|
||||
List<String> _splitTextIntoSentences(String text) {
|
||||
List<String> result = [];
|
||||
|
||||
if (text.length <= 50) {
|
||||
result.add(text);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 支持中英文句子分割
|
||||
List<String> sentenceEnders = [
|
||||
'。', '!', '?', ';', '\n', // 中文
|
||||
'.', '!', '?', ';' // 英文
|
||||
];
|
||||
|
||||
String remainingText = text;
|
||||
|
||||
while (remainingText.isNotEmpty) {
|
||||
if (remainingText.length <= 50) {
|
||||
result.add(remainingText.trim());
|
||||
break;
|
||||
}
|
||||
|
||||
// 在50字以内查找最佳分割点
|
||||
String candidate = remainingText.substring(0, 50);
|
||||
|
||||
int lastSentenceEndIndex = -1;
|
||||
for (int i = candidate.length - 1; i >= 0; i--) {
|
||||
if (sentenceEnders.contains(candidate[i])) {
|
||||
lastSentenceEndIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastSentenceEndIndex != -1) {
|
||||
// 找到句子结束符,以此为分割点
|
||||
String chunk = candidate.substring(0, lastSentenceEndIndex + 1).trim();
|
||||
if (chunk.isNotEmpty) {
|
||||
result.add(chunk);
|
||||
}
|
||||
remainingText =
|
||||
remainingText.substring(lastSentenceEndIndex + 1).trim();
|
||||
} else {
|
||||
// 没找到句子结束符,寻找逗号分割点(中英文逗号)
|
||||
int lastCommaIndex = -1;
|
||||
for (int i = candidate.length - 1; i >= 20; i--) {
|
||||
if (candidate[i] == ',' || candidate[i] == ',') {
|
||||
lastCommaIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastCommaIndex != -1) {
|
||||
String chunk = candidate.substring(0, lastCommaIndex + 1).trim();
|
||||
if (chunk.isNotEmpty) {
|
||||
result.add(chunk);
|
||||
}
|
||||
remainingText = remainingText.substring(lastCommaIndex + 1).trim();
|
||||
} else {
|
||||
// 没找到合适的分割点,强制在50字处分割
|
||||
result.add(candidate.trim());
|
||||
remainingText = remainingText.substring(50).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.where((chunk) => chunk.trim().isNotEmpty).toList();
|
||||
}
|
||||
|
||||
String _cleanTextForTts(String text) {
|
||||
String cleanText = text
|
||||
// 修正markdown粗体格式,保留内容
|
||||
.replaceAllMapped(RegExp(r'\*\*(.*?)\*\*'),
|
||||
(m) => m.group(1) ?? '') // 粗体:**文字** -> 文字
|
||||
.replaceAllMapped(
|
||||
RegExp(r'\*(.*?)\*'), (m) => m.group(1) ?? '') // 斜体:*文字* -> 文字
|
||||
.replaceAllMapped(
|
||||
RegExp(r'`([^`]+)`'), (m) => m.group(1) ?? '') // 行内代码:`代码` -> 代码
|
||||
.replaceAll(RegExp(r'```[^`]*```', multiLine: true), '') // 代码块:完全移除
|
||||
.replaceAll(RegExp(r'```[\s\S]*?```'), '') // 代码块:完全移除(备用)
|
||||
.replaceAll(
|
||||
RegExp(r'^#{1,6}\s+(.*)$', multiLine: true), r'$1') // 标题:# 标题 -> 标题
|
||||
.replaceAll(
|
||||
RegExp(r'\[([^\]]+)\]\([^\)]+\)'), r'$1') // 链接:[文字](url) -> 文字
|
||||
.replaceAll(
|
||||
RegExp(r'^>\s*(.*)$', multiLine: true), r'$1') // 引用:> 文字 -> 文字
|
||||
.replaceAll(RegExp(r'^[-*+]\s+(.*)$', multiLine: true),
|
||||
r'$1') // 无序列表:- 项目 -> 项目
|
||||
.replaceAll(RegExp(r'^\d+\.\s+(.*)$', multiLine: true),
|
||||
r'$1') // 有序列表:1. 项目 -> 项目
|
||||
.replaceAll(RegExp(r'^\s*[-*]{3,}\s*$', multiLine: true), '') // 分隔线:移除
|
||||
.replaceAll(RegExp(r'\n\s*\n'), '\n') // 多个换行替换为单个换行
|
||||
.replaceAll(RegExp(r'\n'), ' ') // 换行替换为空格
|
||||
|
||||
// 移除代码相关的变量和符号(但保留正常文字中的这些字符)
|
||||
.replaceAll(RegExp(r'\$\d+(?=\s|$|[,。!?;:])'), '') // 移除独立的 $1, $2, $3 等
|
||||
.replaceAll(RegExp(r'\$\{[^}]*\}'), '') // 移除 ${variable} 格式
|
||||
.replaceAll(RegExp(r'(?<=\s|^)\$[a-zA-Z_]\w*(?=\s|$|[,。!?;:])'),
|
||||
'') // 移除独立的 $variable 格式
|
||||
|
||||
// 移除HTML相关内容
|
||||
.replaceAll(RegExp(r'\\[ntr]'), ' ') // 移除转义字符
|
||||
.replaceAll(RegExp(r'&[a-zA-Z0-9]+;'), ' ') // 移除HTML实体
|
||||
.replaceAll(RegExp(r'<[^>]*>'), '') // 秼除HTML标签
|
||||
|
||||
// 移除明显的代码结构(保留正常文字中的括号)
|
||||
.replaceAll(RegExp(r'\{[^}]*\}(?=\s|$)'), '') // 移除独立的花括号内容
|
||||
.replaceAll(
|
||||
RegExp(r'(?<=\s|^)@[a-zA-Z_]\w*(?=\s|$)'), '') // 移除独立的 @mentions
|
||||
|
||||
// 移除纯编程符号行,但保留文字中的标点
|
||||
.replaceAll(
|
||||
RegExp(r'^[#%&*+=|\\/<>^~`\s]*$', multiLine: true), '') // 移除纯符号行
|
||||
.replaceAll(
|
||||
RegExp(r'(?<=\s)[#%&*+=|\\/<>^~`]+(?=\s)'), ' ') // 移除词间的编程符号
|
||||
|
||||
// 移除表情符号
|
||||
.replaceAll(RegExp(r'[\u{1F600}-\u{1F64F}]', unicode: true), '') // 表情符号
|
||||
.replaceAll(
|
||||
RegExp(r'[\u{1F300}-\u{1F5FF}]', unicode: true), '') // 符号和象形文字
|
||||
.replaceAll(
|
||||
RegExp(r'[\u{1F680}-\u{1F6FF}]', unicode: true), '') // 运输和地图符号
|
||||
.replaceAll(RegExp(r'[\u{1F1E0}-\u{1F1FF}]', unicode: true), '') // 旗帜
|
||||
.replaceAll(RegExp(r'[\u{2600}-\u{26FF}]', unicode: true), '') // 杂项符号
|
||||
|
||||
// 移除特殊的非文字字符,但保留基本标点
|
||||
.replaceAll(
|
||||
RegExp(r'[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:,。!?;:、""'
|
||||
'()()[\]【】《》〈〉「」『』\-—_\n]'),
|
||||
' ');
|
||||
|
||||
// 清理多余的空格和标点
|
||||
cleanText = cleanText
|
||||
.replaceAll(RegExp(r'\s+'), ' ') // 多个空格替换为单个空格
|
||||
.replaceAll(RegExp(r'[。]{2,}'), '。') // 多个句号替换为单个
|
||||
.replaceAll(RegExp(r'[,]{2,}'), ',') // 多个逗号替换为单个
|
||||
.replaceAll(RegExp(r'[!]{2,}'), '!') // 多个感叹号替换为单个
|
||||
.replaceAll(RegExp(r'[?]{2,}'), '?') // 多个问号替换为单个
|
||||
.replaceAll(RegExp(r'^\s+|\s+$'), '') // 移除首尾空格
|
||||
.trim();
|
||||
|
||||
return cleanText;
|
||||
}
|
||||
|
||||
// 单次语音播放方法
|
||||
Future<bool> speak(String text) async {
|
||||
if (text.trim().isEmpty) return false;
|
||||
|
||||
try {
|
||||
await stop();
|
||||
await processCompleteText(text);
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('TTS 播放错误: $e');
|
||||
_onErrorHandler?.call('TTS 播放错误: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止播放
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
await _stopCurrentPlayback();
|
||||
clearAll();
|
||||
} catch (e) {
|
||||
print('停止音频播放错误: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pause() async {
|
||||
try {
|
||||
if (_isPlaying) {
|
||||
await _flutterTts.pause();
|
||||
}
|
||||
} catch (e) {
|
||||
print('暂停音频播放错误: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resume() async {
|
||||
try {
|
||||
// flutter_tts没有resume方法,需要重新播放当前文本
|
||||
// 这里可以根据需要实现
|
||||
print('TTS不支持恢复,需要重新播放');
|
||||
} catch (e) {
|
||||
print('恢复音频播放错误: $e');
|
||||
}
|
||||
}
|
||||
|
||||
bool get isPlaying => _isPlaying;
|
||||
|
||||
bool get hasQueuedItems => _orderedTasks.isNotEmpty;
|
||||
|
||||
int get queueLength => _orderedTasks.length;
|
||||
|
||||
// 获取当前播放状态
|
||||
bool get isCurrentlyPlaying => _isPlaying;
|
||||
|
||||
// 获取任务队列状态信息
|
||||
Map<String, dynamic> get sseQueueStatus => {
|
||||
'totalTasks': _orderedTasks.length,
|
||||
'completedTasks': _nextTaskIndex,
|
||||
'isProcessing': _isProcessingTasks,
|
||||
'streamCompleted': _streamCompleted,
|
||||
};
|
||||
|
||||
// 获取队列状态信息(保留兼容性)
|
||||
Map<String, dynamic> get queueStatus => {
|
||||
'totalUrls': 0,
|
||||
'currentIndex': 0,
|
||||
'isQueuePlaying': false,
|
||||
'isAllSegmentsProcessed': false,
|
||||
'expectedSegments': 0,
|
||||
};
|
||||
|
||||
// 获取有序任务数量(用于测试)
|
||||
int getOrderedTasksCount() => _orderedTasks.length;
|
||||
|
||||
// 获取下一个任务索引(用于测试)
|
||||
int getNextTaskIndex() => _nextTaskIndex;
|
||||
|
||||
// dispose方法
|
||||
void dispose() {
|
||||
clearAll();
|
||||
// flutter_tts会自动清理资源
|
||||
}
|
||||
|
||||
/// 获取可用的TTS引擎列表
|
||||
Future<List<dynamic>> getAvailableEngines() async {
|
||||
try {
|
||||
dynamic engines = await _flutterTts.getEngines;
|
||||
return engines ?? [];
|
||||
} catch (e) {
|
||||
print('获取TTS引擎列表失败: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置TTS引擎
|
||||
Future<bool> setEngine(String engineName) async {
|
||||
try {
|
||||
int result = await _flutterTts.setEngine(engineName);
|
||||
print('设置TTS引擎: $engineName, 结果: $result');
|
||||
return result == 1; // 1表示成功
|
||||
} catch (e) {
|
||||
print('设置TTS引擎失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前默认引擎
|
||||
Future<String?> getCurrentEngine() async {
|
||||
try {
|
||||
dynamic engine = await _flutterTts.getDefaultEngine;
|
||||
return engine?.toString();
|
||||
} catch (e) {
|
||||
print('获取当前TTS引擎失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取支持的语言列表
|
||||
Future<List<dynamic>> getSupportedLanguages() async {
|
||||
try {
|
||||
dynamic languages = await _flutterTts.getLanguages;
|
||||
return languages ?? [];
|
||||
} catch (e) {
|
||||
print('获取支持的语言列表失败: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查特定语言是否可用
|
||||
Future<bool> isLanguageAvailable(String language) async {
|
||||
try {
|
||||
dynamic result = await _flutterTts.isLanguageAvailable(language);
|
||||
return result == true;
|
||||
} catch (e) {
|
||||
print('检查语言可用性失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TTS任务类
|
||||
class TtsTask {
|
||||
final int index;
|
||||
final String text;
|
||||
TtsTaskStatus status;
|
||||
|
||||
TtsTask({
|
||||
required this.index,
|
||||
required this.text,
|
||||
required this.status,
|
||||
});
|
||||
}
|
||||
|
||||
/// TTS任务状态枚举
|
||||
enum TtsTaskStatus {
|
||||
ready, // 就绪
|
||||
completed, // 已完成
|
||||
failed, // 失败
|
||||
}
|
||||
98
lib/services/vehicle_state_service.dart
Normal file
98
lib/services/vehicle_state_service.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
// import 'redis_service.dart';
|
||||
// import 'package:flutter_ingeek_carkey/flutter_ingeek_carkey_platform_interface.dart';
|
||||
// import 'package:app_car/src/app_ingeek_mdk/ingeek_mdk_instance.dart';
|
||||
// import 'package:app_car/src/app_ingeek_mdk/ingeek_mdk_service.dart';
|
||||
//
|
||||
// class VehicleState {
|
||||
// String vin;
|
||||
// bool isStarted;
|
||||
// bool isUnlock;
|
||||
// bool isDoorUnlock;
|
||||
// bool isWindowOpen;
|
||||
// bool isTrunkOpen;
|
||||
// int power;
|
||||
// String mileage;
|
||||
// String innerTemperature;
|
||||
// bool climatisationOn;
|
||||
// String climatisationState;
|
||||
// double targetTemperature;
|
||||
// bool wheelHeatOn;
|
||||
// bool mainSeatHeatOn;
|
||||
// bool minorSeatHeatOn;
|
||||
//
|
||||
// VehicleState({
|
||||
// required this.vin,
|
||||
// required this.isStarted,
|
||||
// required this.isUnlock,
|
||||
// required this.isDoorUnlock,
|
||||
// required this.isWindowOpen,
|
||||
// required this.isTrunkOpen,
|
||||
// required this.power,
|
||||
// required this.mileage,
|
||||
// required this.innerTemperature,
|
||||
// required this.climatisationOn,
|
||||
// required this.climatisationState,
|
||||
// required this.targetTemperature,
|
||||
// required this.wheelHeatOn,
|
||||
// required this.mainSeatHeatOn,
|
||||
// required this.minorSeatHeatOn,
|
||||
// });
|
||||
//
|
||||
// Map<String, dynamic> toJson() => {
|
||||
// "vin": vin,
|
||||
// "isStarted": isStarted,
|
||||
// "isUnlock": isUnlock,
|
||||
// "isDoorUnlock": isDoorUnlock,
|
||||
// "isWindowOpen": isWindowOpen,
|
||||
// "isTrunkOpen": isTrunkOpen,
|
||||
// "power": power,
|
||||
// "mileage": mileage,
|
||||
// "innerTemperature": innerTemperature,
|
||||
// "climatisationOn": climatisationOn,
|
||||
// "climatisationState": climatisationState,
|
||||
// "targetTemperature": targetTemperature,
|
||||
// "wheelHeatOn": wheelHeatOn,
|
||||
// "mainSeatHeatOn": mainSeatHeatOn,
|
||||
// "minorSeatHeatOn": minorSeatHeatOn,
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// class VehicleStateService {
|
||||
// MDKObserverClient? _mdkClient;
|
||||
// String vin = '';
|
||||
//
|
||||
// static final VehicleStateService _instance = VehicleStateService._internal();
|
||||
//
|
||||
// factory VehicleStateService() => _instance;
|
||||
//
|
||||
// VehicleStateService._internal() {
|
||||
// _mdkClient = InGeekMdkInstance().addObserve(
|
||||
// (IGKVehicleStatusInfo data) {
|
||||
// vin = data.vin;
|
||||
// final vehicleState = VehicleState(
|
||||
// vin: data.vin,
|
||||
// isStarted: InGeekMdkInstance().getCarIsStart(),
|
||||
// isUnlock: InGeekMdkInstance().getMDKCarIsUnLock(),
|
||||
// isDoorUnlock: InGeekMdkInstance().getMDKDoorIsUnLock(),
|
||||
// isWindowOpen: InGeekMdkInstance().getMDKWindowIsOpen(),
|
||||
// isTrunkOpen: InGeekMdkInstance().getMDKTrunkIsOpen(),
|
||||
// power: InGeekMdkInstance().getMDKPower(),
|
||||
// mileage: InGeekMdkInstance().getMDKMileage(),
|
||||
// innerTemperature: InGeekMdkInstance().getMdkInnerTemperature(),
|
||||
// climatisationOn: InGeekMdkInstance().getMdkClimatizationOnoff(),
|
||||
// climatisationState: InGeekMdkInstance().getMdkClimatisationState(),
|
||||
// targetTemperature: InGeekMdkInstance().getMdkTargetTemperature(),
|
||||
// wheelHeatOn: InGeekMdkInstance().getMdkWheelHeatOn(),
|
||||
// mainSeatHeatOn: InGeekMdkInstance().getMdkMainSeatHeatOn(),
|
||||
// minorSeatHeatOn: InGeekMdkInstance().getMdkMinorSeatHeatOn());
|
||||
// RedisService().setKeyValue(data.vin, vehicleState);
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// void dispose() {
|
||||
// if (_mdkClient != null) {
|
||||
// InGeekMdkInstance().removeObserve(_mdkClient!);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
61
lib/services/voice_recognition_service.dart
Normal file
61
lib/services/voice_recognition_service.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
class VoiceRecognitionService {
|
||||
Future<String?> recognizeSpeech(List<int> audioBytes,
|
||||
{String lang = 'cn'}) async {
|
||||
try {
|
||||
print('准备发送OPUS音频数据,大小: ${audioBytes.length} 字节');
|
||||
final uri = Uri.parse('http://143.64.185.20:18606/voice');
|
||||
print('发送到: $uri');
|
||||
|
||||
final request = http.MultipartRequest('POST', uri);
|
||||
request.files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'audio',
|
||||
audioBytes,
|
||||
filename: 'record.wav',
|
||||
contentType: MediaType('audio', 'wav'),
|
||||
),
|
||||
);
|
||||
request.fields['lang'] = lang; // 根据参数设置语言
|
||||
|
||||
print('发送请求...');
|
||||
final streamResponse = await request.send();
|
||||
print('收到响应,状态码: ${streamResponse.statusCode}');
|
||||
|
||||
final response = await http.Response.fromStream(streamResponse);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('响应内容: ${response.body}');
|
||||
final text = _parseTextFromJson(response.body);
|
||||
if (text != null) {
|
||||
print('解析出文本: $text');
|
||||
return text; // 返回识别的文本
|
||||
} else {
|
||||
print('解析文本失败');
|
||||
}
|
||||
} else {
|
||||
print('请求失败,状态码: ${response.statusCode},响应: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('发送录音到服务器时出错: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 移到类内部作为私有方法
|
||||
String? _parseTextFromJson(String body) {
|
||||
try {
|
||||
final decoded = jsonDecode(body);
|
||||
if (decoded.containsKey('text')) {
|
||||
return decoded['text'] as String?;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('JSON解析错误: $e, 原始数据: $body');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user