Browse Source

修改会议助手

newdev_shunjiawei
tanlongsheng 2 years ago
parent
commit
a11d3682f8
  1. 54
      lib/data/services/asr_service.dart
  2. 270
      lib/data/services/speech_impl/xunfei_asr_service.dart
  3. 6
      lib/modules/meeting/controllers/meeting_edit_controller.dart
  4. 50
      lib/modules/meeting/controllers/meeting_record_controller.dart
  5. 4
      lib/modules/meeting/views/meeting_edit_view.dart

54
lib/data/services/asr_service.dart

@ -4,28 +4,28 @@ import 'dart:async';
abstract class AsrService {
/// 支持的语言
List<String> get supportedLanguages;
/// 初始化语音识别服务
Future<bool> initialize({
List<String>? supportedLanguages,
});
/// 执行一次性语音识别
///
///
/// 返回识别的最终结果
Future<RecognitionEvent> recognizeOnce();
/// 开始连续语音识别
///
///
/// 返回一个包含识别事件的流
Future<Stream<RecognitionEvent>> startContinuousRecognition();
/// 停止连续语音识别
Future<bool> stopContinuousRecognition();
/// 检查连续识别是否活跃
bool isContinuousRecognitionActive();
/// 释放资源
Future<void> dispose();
}
@ -34,19 +34,19 @@ abstract class AsrService {
enum RecognitionEventType {
/// 最终识别结果
finalResult,
/// 中间识别结果(实时反馈)
intermediateResult,
/// 会话开始
sessionStarted,
/// 会话结束
sessionStopped,
/// 识别取消
canceled,
/// 识别错误
error,
}
@ -55,23 +55,27 @@ enum RecognitionEventType {
class RecognitionEvent {
/// 事件类型
final RecognitionEventType type;
/// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效)
final String text;
/// 检测到的语言
final String detectedLanguage;
/// 角色
final String role;
/// 错误信息(仅在 error 和 canceled 类型中有效)
final String error;
RecognitionEvent({
required this.type,
this.text = '',
this.detectedLanguage = '',
this.role = '',
this.error = '',
});
/// 创建最终结果事件的快捷构造函数
factory RecognitionEvent.finalResult({
required String text,
@ -83,7 +87,7 @@ class RecognitionEvent {
detectedLanguage: detectedLanguage,
);
}
/// 创建错误事件的快捷构造函数
factory RecognitionEvent.error(String errorMessage) {
return RecognitionEvent(
@ -91,15 +95,17 @@ class RecognitionEvent {
error: errorMessage,
);
}
/// 检查是否为最终结果
bool get isFinalResult => type == RecognitionEventType.finalResult;
/// 检查是否为错误
bool get isError => type == RecognitionEventType.error || type == RecognitionEventType.canceled;
bool get isError =>
type == RecognitionEventType.error ||
type == RecognitionEventType.canceled;
@override
String toString() {
return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}';
}
}
}

270
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -0,0 +1,270 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:record/record.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../../../core/utils/logger.dart';
import 'package:get/get.dart';
import '../asr_service.dart';
/// 讯飞 语音识别服务
///
class XunfeiAsrService extends GetxService implements AsrService {
final String baseUrl = 'wss://rtasr.xfyun.cn/v1/ws';
final int _chunkSize = 1280; // 每次发送的数据大小
final String appId = '43d87d22';
final String apiKey = '83f063755e8d342670002f50a4471d42';
AudioRecorder? recorder;
Timer? _timer;
WebSocketChannel? _channel;
bool isConnected = false; //是否链接
bool _isInitialized = false;
final List<String> _defaultSupportedLanguages = ['zh-CN'];
String _languageStr = 'cn';
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
@override
List<String> get supportedLanguages => _defaultSupportedLanguages;
Uint8List _buff = Uint8List(0); //音频缓存区
BytesBuilder _originaBytes = BytesBuilder(); //原始音频
bool isRecording = false;
@override
Future<bool> initialize({
List<String>? supportedLanguages,
}) async {
try {
final List<String> languages =
supportedLanguages ?? _defaultSupportedLanguages;
String languagetoStr = languages.first.split('-')[1].toLowerCase();
_languageStr = languagetoStr;
_isInitialized = true;
Logger.info('Azure 语音识别服务初始化成功');
return true;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e');
_isInitialized = false;
rethrow;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
if (!_isInitialized) {
await initialize();
}
try {
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
return RecognitionEvent.error(e.toString());
}
}
@override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) {
await initialize();
}
try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
await recorder?.dispose();
recorder = AudioRecorder();
Stream<Uint8List>? stream = await recorder?.startStream(
const RecordConfig(
sampleRate: 16000,
encoder: AudioEncoder.pcm16bits,
numChannels: 1,
),
);
isRecording = true;
stream?.listen((data) {
if (isRecording) {
_buff = _appendToBuffer(data);
_originaBytes.add(data);
}
});
await _tryConnecting();
_timer = Timer.periodic(const Duration(milliseconds: 40), (timer) async {
//每40毫秒读取一次数据
var frame = _getAudioData();
if (frame.isNotEmpty) {
_channel?.sink.add(frame);
}
});
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
rethrow;
}
}
//写入音频数据到缓存区中
Uint8List _appendToBuffer(Uint8List newData) {
var newBuffer = Uint8List(_buff.length + newData.length);
newBuffer.setAll(0, _buff);
newBuffer.setAll(_buff.length, newData);
return newBuffer;
}
//读取缓存区中一帧数据
Uint8List _getAudioData() {
if (_buff.length >= _chunkSize) {
// 从缓冲区中读取1280字节的数据
var data = _buff.sublist(0, _chunkSize);
// 移除已读取的数据
_buff = _buff.sublist(_chunkSize);
return data;
} else {
return _buff;
}
}
//尝试连接
Future<void> _tryConnecting() async {
try {
final url = _geturl();
_channel = WebSocketChannel.connect(Uri.parse(url));
_channel?.stream.timeout(const Duration(seconds: 10)); //设置超时时间
await _channel?.ready;
isConnected = true;
_channel?.stream.listen(
(message) {
_handle(message);
},
onError: (error) {
isConnected = false;
Logger.error('连接失败: $error');
},
onDone: () {
isConnected = false;
Logger.error('WebSocket 连接已关闭!');
},
cancelOnError: true,
);
} catch (e) {
Logger.error("初始化异常----------------$e");
}
}
//处理数据
void _handle(data) {
try {
final result = jsonDecode(data);
if (result['code'] == "0" && result['data'] != "") {
final data = jsonDecode(result['data']);
String msg = "";
String role = '';
List rtList = data['cn']['st']['rt'];
for (var rt in rtList) {
for (var ws in rt['ws']) {
for (var ws in ws['cw']) {
msg += ws['w'];
if (ws['rl'] != "0") {
role = ws['rl'];
}
}
}
}
Logger.info(
"识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role");
if (msg.isNotEmpty) {
if (data['cn']['st']['type'] == "0") {
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: msg,
role: role,
));
} else {
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: msg,
role: role,
));
}
}
}
} catch (e) {
Logger.error("接受消息异常 $e");
}
}
///获取链接地址
String _geturl() {
// 生成时间戳
String ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
// 计算 baseString
String baseString = md5.convert(utf8.encode(appId + ts)).toString();
// 计算 signa
var hmacSha1 = Hmac(sha1, utf8.encode(apiKey));
var signa = hmacSha1.convert(utf8.encode(baseString));
String signaBase64 = base64Encode(signa.bytes);
String url = "";
// 构建 WebSocket URL
url =
'$baseUrl?appid=$appId&ts=$ts&lang=$_languageStr&roleType=2&signa=${Uri.encodeComponent(signaBase64)}';
return url;
}
@override
Future<bool> stopContinuousRecognition() async {
try {
await recorder?.stop();
isRecording = false;
_timer?.cancel();
_buff = Uint8List(0);
_originaBytes = BytesBuilder();
if (isConnected) {
_channel?.sink.add(utf8.encode('{"end": true}')); //结束
}
_isContinuousRecognitionActive = false;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
rethrow;
}
}
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
@override
Future<void> dispose() async {
try {
await stopContinuousRecognition();
recorder?.dispose();
_channel?.sink.close();
_eventStreamController?.close();
_eventStreamController = null;
_isInitialized = false;
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
_isInitialized = false;
}
}
}

6
lib/modules/meeting/controllers/meeting_edit_controller.dart

@ -62,13 +62,13 @@ class MeetingEditController extends GetxController
void confirm() async {
String content = contentController.text;
String firstContent =
content.length >= 20 ? content.substring(0, 20) : content;
content.length >= 30 ? content.substring(0, 30) : content;
String title = titleController.text.isNotEmpty
? titleController.text
: meetingDetailsController.meetingData.value.title;
Map<String, Object?> data = {
'title': title,
'firstContent': firstContent,
'firstContent': firstContent.replaceAll("\n", ""),
'seconds': meetingDetailsController.seconds,
'content': contentController.text,
};
@ -81,7 +81,7 @@ class MeetingEditController extends GetxController
final dataMap =
Map<String, dynamic>.from(meetingController.dataList[index]);
dataMap['title'] = title;
dataMap['content'] = firstContent;
dataMap['content'] = firstContent.replaceAll("\n", "");
dataMap['seconds'] = meetingDetailsController.seconds;
meetingController.dataList[index] = dataMap;
}

50
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/speech_impl/xunfei_asr_service.dart';
import 'meeting_controller.dart';
class MeetingRecordController extends GetxController
@ -31,7 +32,8 @@ class MeetingRecordController extends GetxController
void onInit() {
super.onInit();
lottieController = AnimationController(vsync: this);
_asrService = Get.find<AsrService>();
Get.put(XunfeiAsrService());
_asrService = Get.find<XunfeiAsrService>();
final Map<String, dynamic> args = Get.arguments ?? {};
title = args['title'];
creationTime = DateTime.now().millisecondsSinceEpoch;
@ -41,7 +43,7 @@ class MeetingRecordController extends GetxController
void onClose() {
lottieController.dispose();
timer?.cancel();
_asrService.stopContinuousRecognition();
_asrService.dispose();
_recognitionSubscription?.cancel();
super.onClose();
}
@ -82,7 +84,6 @@ class MeetingRecordController extends GetxController
//结束录音
Future<void> _stopRecorder() async {
await _asrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
}
// 处理语音识别事件
@ -92,7 +93,7 @@ class MeetingRecordController extends GetxController
intermediateContent.value = event.text;
break;
case RecognitionEventType.finalResult:
finalContent.value += event.text;
finalContent.value += _spliceText(event);
intermediateContent.value = '';
break;
default:
@ -101,14 +102,49 @@ class MeetingRecordController extends GetxController
}
}
String _spliceText(RecognitionEvent event) {
String eventText = event.text;
String text = '';
if (finalContent.value.isNotEmpty) {
if (event.role != '') {
final punctuations = {
',',
'。',
'!',
'?',
';',
':',
',',
'.',
':',
';',
'!',
'?'
};
final firstChar = eventText[0];
if (punctuations.contains(firstChar)) {
text += '$firstChar\n';
eventText = eventText.substring(1);
} else {
text += '\n';
}
text += '说话人${event.role}:';
}
} else {
text += '说话人${event.role}:';
}
text += eventText;
return text;
}
// 完成
void complete() async {
String firstContent = finalContent.value.length >= 20
? finalContent.value.substring(0, 20)
String firstContent = finalContent.value.length >= 30
? finalContent.value.substring(0, 30)
: finalContent.value;
Map<String, Object?> data = {
'title': title,
'firstContent': firstContent,
'firstContent': firstContent.replaceAll("\n", ""),
'seconds': seconds.value,
'creationTime': creationTime,
'audio': [],

4
lib/modules/meeting/views/meeting_edit_view.dart

@ -237,14 +237,14 @@ class MeetingEditView extends GetView<MeetingEditController> {
controller: controller.contentController,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
fontSize: 14.sp,
),
maxLines: 50,
decoration: InputDecoration(
hintText: '编辑记录内容',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 16.sp,
fontSize: 14.sp,
),
fillColor: Colors.transparent,
border: InputBorder.none,

Loading…
Cancel
Save