You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
9.3 KiB

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.toString()}');
_isInitialized = false;
rethrow;
}
}
@override
Future<RecognitionEvent> recognizeOnce() async {
if (!_isInitialized) {
await initialize();
}
try {
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString());
}
}
// @override
// Future<Stream<RecognitionEvent>> startContinuousRecognition(
// bool audioSourceType) 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.toString()}');
// 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.toString()}');
},
onDone: () {
isConnected = false;
Logger.error('WebSocket 连接已关闭!');
},
cancelOnError: true,
);
} catch (e) {
Logger.error("初始化异常----------------${e.toString()}");
}
}
//处理数据
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,
audio: _originaBytes.toBytes(),
));
_originaBytes.clear();
} else {
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult,
text: msg,
role: role,
));
}
}
}
} catch (e) {
Logger.error("接受消息异常 ${e.toString()}");
}
}
///获取链接地址
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.toString()}');
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.toString()}');
_isInitialized = false;
}
}
@override
Future<bool> enableRecord(bool audioSourceType, String filePath,
{bool acceptAudioData = false}) {
// TODO: implement enableRecord
throw UnimplementedError();
}
@override
Future<bool> stopRecord(bool isSave) {
// TODO: implement stopRecord
throw UnimplementedError();
}
@override
Future<bool> pauseRecord() {
// TODO: implement pauseRecord
throw UnimplementedError();
}
@override
Future<bool> moveFile(String sourcePath, String destPath) {
// TODO: implement moveFile
throw UnimplementedError();
}
@override
Future<bool> renameFile(String filePath, String newName) {
// TODO: implement renameFile
throw UnimplementedError();
}
@override
Future<void> disableBluetoothAudio() {
// TODO: implement disableBluetoothAudio
throw UnimplementedError();
}
@override
Future<void> restoreOriginalAudioState() {
// TODO: implement restoreOriginalAudioState
throw UnimplementedError();
}
@override
Future<Stream<RecognitionEvent>> recognizeCallback() {
// TODO: implement recognizeCallback
throw UnimplementedError();
}
@override
Future<bool> startContinuousRecognition(
bool audioSourceType, {
bool isRemoveFirstPunctuation = true,
}) {
// TODO: implement startContinuousRecognition
throw UnimplementedError();
}
@override
Future<bool> setAudioConfig({int sampleRate = 16000, int channels = 1}) {
// TODO: implement setAudioConfig
throw UnimplementedError();
}
@override
Future<bool> resumeRecord() {
// TODO: implement resumeRecord
throw UnimplementedError();
}
@override
Stream<Uint8List>? getAudioDataStream() {
// TODO: implement getAudioDataStream
throw UnimplementedError();
}
}