Browse Source

Merge commit '18d354166c95b06b4bbeebdaae49bd8afe0039fb' into lxm

newdev_shunjiawei
lxm 2 years ago
parent
commit
32cc185f8a
  1. 5
      lib/data/services/asr_service.dart
  2. 2
      lib/data/services/speech_impl/xunfei_asr_service.dart
  3. 18
      lib/modules/meeting/controllers/meeting_details_controller.dart
  4. 20
      lib/modules/meeting/controllers/meeting_edit_controller.dart
  5. 84
      lib/modules/meeting/controllers/meeting_record_controller.dart
  6. 51
      lib/modules/meeting/views/meeting_details_view.dart
  7. 54
      lib/modules/meeting/views/meeting_edit_view.dart

5
lib/data/services/asr_service.dart

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
/// 语音识别服务接口 /// 语音识别服务接口
abstract class AsrService { abstract class AsrService {
@ -65,6 +66,9 @@ class RecognitionEvent {
/// 角色 /// 角色
final String role; final String role;
/// 原始音频
final Uint8List? audio;
/// 错误信息(仅在 error 和 canceled 类型中有效) /// 错误信息(仅在 error 和 canceled 类型中有效)
final String error; final String error;
@ -73,6 +77,7 @@ class RecognitionEvent {
this.text = '', this.text = '',
this.detectedLanguage = '', this.detectedLanguage = '',
this.role = '', this.role = '',
this.audio,
this.error = '', this.error = '',
}); });

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

@ -192,7 +192,9 @@ class XunfeiAsrService extends GetxService implements AsrService {
type: RecognitionEventType.finalResult, type: RecognitionEventType.finalResult,
text: msg, text: msg,
role: role, role: role,
audio: _originaBytes.toBytes(),
)); ));
_originaBytes.clear();
} else { } else {
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult, type: RecognitionEventType.intermediateResult,

18
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -1,13 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/speech_impl/volcano_tts_api_service.dart';
import '../model/meeting_details_model.dart'; import '../model/meeting_details_model.dart';
import '../model/meeting_model.dart'; import '../model/meeting_model.dart';
class MeetingDetailsController extends GetxController class MeetingDetailsController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
late AnimationController lottieController; late AnimationController lottieController;
final AudioPlayer audioPlayer = AudioPlayer();
int id = 0; int id = 0;
@ -31,6 +34,7 @@ class MeetingDetailsController extends GetxController
@override @override
void onClose() { void onClose() {
lottieController.dispose(); lottieController.dispose();
audioPlayer.dispose();
super.onClose(); super.onClose();
} }
@ -38,16 +42,26 @@ class MeetingDetailsController extends GetxController
final data = await SqfliteApi.getMeetingDetails(id); final data = await SqfliteApi.getMeetingDetails(id);
meetingData.value = MeetingModel.fromJson(data); meetingData.value = MeetingModel.fromJson(data);
dataDetails.value = MeetingDetailsModel.fromJson(data['details']); dataDetails.value = MeetingDetailsModel.fromJson(data['details']);
seconds = meetingData.value.seconds; audioPlayer.setAudioSource(BytesAudioSource(dataDetails.value.audio));
audioPlayer.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
await audioPlayer.seek(Duration.zero);
audioPlayer.pause();
lottieController.stop();
isPlay.value = false;
}
});
isLoading.value = false; isLoading.value = false;
} }
void start() { void start() async {
if (isPlay.value) { if (isPlay.value) {
lottieController.stop(); lottieController.stop();
audioPlayer.pause();
isPlay.value = false; isPlay.value = false;
} else { } else {
lottieController.repeat(); lottieController.repeat();
audioPlayer.play();
isPlay.value = true; isPlay.value = true;
} }
} }

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

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/speech_impl/volcano_tts_api_service.dart';
import 'meeting_controller.dart'; import 'meeting_controller.dart';
import 'meeting_details_controller.dart'; import 'meeting_details_controller.dart';
@ -16,6 +18,8 @@ class MeetingEditController extends GetxController
late TextEditingController titleController; late TextEditingController titleController;
late TextEditingController contentController; late TextEditingController contentController;
final AudioPlayer audioPlayer = AudioPlayer();
int creationTime = 0; int creationTime = 0;
int id = 0; int id = 0;
@ -36,6 +40,7 @@ class MeetingEditController extends GetxController
lottieController.dispose(); lottieController.dispose();
titleController.dispose(); titleController.dispose();
contentController.dispose(); contentController.dispose();
audioPlayer.dispose();
super.onClose(); super.onClose();
} }
@ -47,14 +52,27 @@ class MeetingEditController extends GetxController
text: meetingDetailsController.dataDetails.value.content, text: meetingDetailsController.dataDetails.value.content,
); );
creationTime = meetingDetailsController.meetingData.value.creationtime; creationTime = meetingDetailsController.meetingData.value.creationtime;
audioPlayer.setAudioSource(
BytesAudioSource(meetingDetailsController.dataDetails.value.audio));
audioPlayer.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
await audioPlayer.seek(Duration.zero);
audioPlayer.pause();
lottieController.stop();
isPlay.value = false;
}
});
} }
void start() { void start() async {
if (isPlay.value) { if (isPlay.value) {
lottieController.stop(); lottieController.stop();
audioPlayer.pause();
isPlay.value = false; isPlay.value = false;
} else { } else {
lottieController.repeat(); lottieController.repeat();
await audioPlayer.seek(Duration.zero);
audioPlayer.play();
isPlay.value = true; isPlay.value = true;
} }
} }

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

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -28,6 +29,8 @@ class MeetingRecordController extends GetxController
StreamSubscription? _recognitionSubscription; StreamSubscription? _recognitionSubscription;
BytesBuilder _originaBytes = BytesBuilder(); //原始音频
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -45,6 +48,7 @@ class MeetingRecordController extends GetxController
timer?.cancel(); timer?.cancel();
_asrService.dispose(); _asrService.dispose();
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_originaBytes = BytesBuilder();
super.onClose(); super.onClose();
} }
@ -95,6 +99,7 @@ class MeetingRecordController extends GetxController
case RecognitionEventType.finalResult: case RecognitionEventType.finalResult:
finalContent.value += _spliceText(event); finalContent.value += _spliceText(event);
intermediateContent.value = ''; intermediateContent.value = '';
_originaBytes.add(event.audio!);
break; break;
default: default:
// 不做任何处理 // 不做任何处理
@ -142,12 +147,20 @@ class MeetingRecordController extends GetxController
String firstContent = finalContent.value.length >= 30 String firstContent = finalContent.value.length >= 30
? finalContent.value.substring(0, 30) ? finalContent.value.substring(0, 30)
: finalContent.value; : finalContent.value;
Uint8List audio = _originaBytes.toBytes();
Uint8List wavHeader = generateWavHeader(
sampleRate: 16000,
numberOfChannels: 1,
bitsPerSample: 16,
dataLength: audio.length,
);
Uint8List wavData = Uint8List.fromList([...wavHeader, ...audio]);
Map<String, Object?> data = { Map<String, Object?> data = {
'title': title, 'title': title,
'firstContent': firstContent.replaceAll("\n", ""), 'firstContent': firstContent.replaceAll("\n", ""),
'seconds': seconds.value, 'seconds': seconds.value,
'creationTime': creationTime, 'creationTime': creationTime,
'audio': [], 'audio': wavData,
'content': finalContent.value, 'content': finalContent.value,
}; };
final result = await SqfliteApi.insertMeeting(data); final result = await SqfliteApi.insertMeeting(data);
@ -162,4 +175,73 @@ class MeetingRecordController extends GetxController
Get.back(); Get.back();
} }
} }
Uint8List generateWavHeader({
required int sampleRate,
required int numberOfChannels,
required int bitsPerSample,
required int dataLength,
}) {
final byteRate = sampleRate * numberOfChannels * (bitsPerSample ~/ 8);
final blockAlign = numberOfChannels * (bitsPerSample ~/ 8);
final riffSize = 36 + dataLength;
final header = Uint8List(44);
var pos = 0;
// RIFF 块
header.setRange(pos, pos + 4, [0x52, 0x49, 0x46, 0x46]); // 'RIFF'
pos += 4;
// 写入 RIFF 大小(小端)
header.setRange(pos, pos + 4, [
riffSize & 0xff,
(riffSize >> 8) & 0xff,
(riffSize >> 16) & 0xff,
(riffSize >> 24) & 0xff,
]);
pos += 4;
// WAVE 标识
header.setRange(pos, pos + 4, [0x57, 0x41, 0x56, 0x45]); // 'WAVE'
pos += 4;
// fmt 子块
header.setRange(pos, pos + 4, [0x66, 0x6d, 0x74, 0x20]); // 'fmt '
pos += 4;
header.setRange(pos, pos + 4, [16, 0, 0, 0]); // 块大小
pos += 4;
header.setRange(pos, pos + 2, [1, 0]); // PCM 格式
pos += 2;
header.setRange(pos, pos + 2, [numberOfChannels, 0]); // 声道数
pos += 2;
// 采样率(小端)
header.setRange(pos, pos + 4, [
sampleRate & 0xff,
(sampleRate >> 8) & 0xff,
(sampleRate >> 16) & 0xff,
(sampleRate >> 24) & 0xff,
]);
pos += 4;
// 字节率(小端)
header.setRange(pos, pos + 4, [
byteRate & 0xff,
(byteRate >> 8) & 0xff,
(byteRate >> 16) & 0xff,
(byteRate >> 24) & 0xff,
]);
pos += 4;
// 块对齐
header.setRange(pos, pos + 2, [blockAlign, 0]);
pos += 2;
// 位深度
header.setRange(pos, pos + 2, [bitsPerSample, 0]);
pos += 2;
// data 子块
header.setRange(pos, pos + 4, [0x64, 0x61, 0x74, 0x61]); // 'data'
pos += 4;
// 数据大小(小端)
header.setRange(pos, pos + 4, [
dataLength & 0xff,
(dataLength >> 8) & 0xff,
(dataLength >> 16) & 0xff,
(dataLength >> 24) & 0xff,
]);
return header;
}
} }

51
lib/modules/meeting/views/meeting_details_view.dart

@ -139,12 +139,22 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Text( StreamBuilder<Duration>(
'00:00:00', stream: controller.audioPlayer.positionStream,
style: TextStyle( builder: (context, snapshot) {
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], final position = snapshot.data ?? Duration.zero;
fontSize: 12.sp, return Text(
), DateUtil.formatDateMs(
position.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
), ),
GestureDetector( GestureDetector(
onTap: controller.start, onTap: controller.start,
@ -164,12 +174,23 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
), ),
), ),
), ),
Text( StreamBuilder<Duration?>(
'00:00:00', stream: controller.audioPlayer.durationStream,
style: TextStyle( builder: (context, snapshot) {
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], final duration = snapshot.data ?? Duration.zero;
fontSize: 12.sp, controller.seconds = duration.inSeconds;
), return Text(
DateUtil.formatDateMs(
duration.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
), ),
], ],
), ),
@ -202,10 +223,8 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: SingleChildScrollView( child: SingleChildScrollView(
reverse: true, reverse: true,
child: Obx( child: Text(
() => Text( controller.dataDetails.value.content,
controller.dataDetails.value.content,
),
), ),
), ),
), ),

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

@ -174,12 +174,22 @@ class MeetingEditView extends GetView<MeetingEditController> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Text( StreamBuilder<Duration>(
'00:00:00', stream: controller.audioPlayer.positionStream,
style: TextStyle( builder: (context, snapshot) {
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], final position = snapshot.data ?? Duration.zero;
fontSize: 12.sp, return Text(
), DateUtil.formatDateMs(
position.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
), ),
GestureDetector( GestureDetector(
onTap: controller.start, onTap: controller.start,
@ -192,19 +202,31 @@ class MeetingEditView extends GetView<MeetingEditController> {
12.w, 12.w,
), ),
), ),
child: Icon( child: Obx(
controller.isPlay.value ? Icons.pause : Icons.play_arrow, () => Icon(
color: Colors.white, controller.isPlay.value ? Icons.pause : Icons.play_arrow,
size: 25.w, color: Colors.white,
size: 25.w,
),
), ),
), ),
), ),
Text( StreamBuilder<Duration?>(
'00:00:00', stream: controller.audioPlayer.durationStream,
style: TextStyle( builder: (context, snapshot) {
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], final duration = snapshot.data ?? Duration.zero;
fontSize: 12.sp, return Text(
), DateUtil.formatDateMs(
duration.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
);
},
), ),
], ],
), ),

Loading…
Cancel
Save