5 changed files with 348 additions and 36 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue