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.
303 lines
8.1 KiB
303 lines
8.1 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(String filePath) {
|
|
// 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();
|
|
}
|
|
}
|
|
|