選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

331 行
9.9 KiB

  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:math';
  4. import 'dart:typed_data';
  5. import 'package:demo001/tools/audio_tool.dart';
  6. import 'package:demo001/xunfei/recognition_result/recognition_result.dart';
  7. import 'package:demo001/xunfei/streamtrans_result/streamtrans_result.dart';
  8. import 'package:intl/intl.dart';
  9. import 'package:demo001/xunfei/utils.dart';
  10. import 'package:web_socket_channel/web_socket_channel.dart';
  11. //讯飞翻译
  12. class XunFeiTranslate {
  13. final int _chunkSize = 1280; // 每次发送的数据大小
  14. // 音量阈值
  15. final double _speakingThreshold = 50.0; // 开始说话的阈值
  16. final double _silenceThreshold = 30.0; // 结束说话的阈值
  17. final Duration _silenceDuration = Duration(seconds: 1); // 持续低于阈值的时间
  18. DateTime? _lastBelowThresholdTime; // 上次音量低于阈值的时间
  19. double _volume = 0; //当前原因
  20. Uint8List _buff = Uint8List(0); //音频缓存区
  21. bool _isrecord = false; //是否录音
  22. bool _isspeaking = false; //是否说话
  23. Timer? _timer;
  24. XunFeiTranslateTask? currtask;
  25. late Function(RecognitionResult) onRecognitionResult;
  26. late Function(StreamtransResult) onStreamtransResult;
  27. late Function(AudioModel) onAudioResult;
  28. final String appId;
  29. final String apiKey;
  30. final String apiSecret;
  31. final String host = "ws-api.xf-yun.com";
  32. final String requestUri = "/v1/private/simult_interpretation";
  33. //静态变量保存唯一实例
  34. static XunFeiTranslate? _instance;
  35. XunFeiTranslate._internal(
  36. {required this.appId,
  37. required this.apiKey,
  38. required this.apiSecret,
  39. required this.onRecognitionResult,
  40. required this.onStreamtransResult,
  41. required this.onAudioResult});
  42. //工厂构造函数
  43. factory XunFeiTranslate({
  44. required String appId,
  45. required String apiKey,
  46. required String apiSecret,
  47. required Function(RecognitionResult) onRecognitionResult,
  48. required Function(StreamtransResult) onStreamtransResult,
  49. required Function(AudioModel) onAudioResult,
  50. }) {
  51. _instance ??= XunFeiTranslate._internal(
  52. appId: appId,
  53. apiKey: apiKey,
  54. apiSecret: apiSecret,
  55. onRecognitionResult: onRecognitionResult,
  56. onStreamtransResult: onStreamtransResult,
  57. onAudioResult: onAudioResult,
  58. );
  59. return _instance!;
  60. }
  61. //获取链接地址
  62. String _geturl() {
  63. final now = DateTime.now();
  64. final date =
  65. DateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'").format(now.toUtc());
  66. final signatureOrigin =
  67. "host: $host\ndate: $date\nGET $requestUri HTTP/1.1";
  68. // 使用 HmacUtil 计算 HMAC-SHA256 签名
  69. final signature = XunfeiUtils.hmacSha256(apiSecret, signatureOrigin);
  70. final authorization = base64.encode(utf8.encode(
  71. "api_key=\"$apiKey\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"$signature\""));
  72. final queryParams = {
  73. "authorization": authorization,
  74. "date": date,
  75. "host": host,
  76. "serviceId": "simult_interpretation"
  77. };
  78. final wsUri =
  79. 'ws://$host$requestUri?${Uri(queryParameters: queryParams).query}';
  80. return wsUri;
  81. }
  82. //创建参数
  83. Map<String, dynamic> _createParams(int status, Uint8List audio) {
  84. final param = {
  85. "header": {
  86. "app_id": appId,
  87. "status": status,
  88. },
  89. "parameter": {
  90. "ist": {
  91. "accent": "mandarin",
  92. "domain": "ist_ed_open",
  93. "language": "zh_cn",
  94. "vto": 15000,
  95. "eos": 150000
  96. },
  97. "streamtrans": {"from": "cn", "to": "en"},
  98. "tts": {
  99. "vcn": "x2_catherine",
  100. "tts_results": {
  101. "encoding": "raw",
  102. "sample_rate": 16000,
  103. "channels": 1,
  104. "bit_depth": 16,
  105. "frame_size": 0
  106. }
  107. }
  108. },
  109. "payload": {
  110. "data": {
  111. "audio": base64.encode(audio),
  112. "encoding": "raw",
  113. "sample_rate": 16000,
  114. "seq": 1,
  115. "status": status
  116. }
  117. }
  118. };
  119. return param;
  120. }
  121. //开始同时翻译
  122. Future<void> starttranslate(Stream<Uint8List> stream) async {
  123. _isrecord = true;
  124. stream.listen((data) {
  125. if (_isrecord) {
  126. _buff = _appendToBuffer(data);
  127. }
  128. });
  129. _timer = Timer.periodic(Duration(milliseconds: 40), (timer) async {
  130. //每40毫秒读取一次数据
  131. var frame = _getAudioData();
  132. _volume = _calculateAmplitude(frame);
  133. var state = _checkSpeakingStatus();
  134. if (state == 1) {
  135. //开始说话
  136. currtask = XunFeiTranslateTask(_geturl(), _handleData);
  137. currtask?.sendaudio(_createParams(0, frame));
  138. print("发送第一帧---------------------------");
  139. } else if (state == 2) {
  140. //结束说话
  141. currtask?.sendaudio(_createParams(1, frame));
  142. } else if (state == 3) {
  143. //结束说话
  144. currtask?.sendaudio(_createParams(2, frame));
  145. print("发送最后一帧---------------------------");
  146. }
  147. });
  148. return;
  149. }
  150. //结束翻译
  151. Future<void> stoptranslate() async {
  152. _isrecord = false;
  153. _timer?.cancel();
  154. _timer = null;
  155. if (currtask != null) {
  156. var _frame = _getAudioData();
  157. currtask?.sendaudio(_createParams(2, _frame));
  158. print("发送最后一帧---------------------------");
  159. currtask = null;
  160. }
  161. _isspeaking = false;
  162. _lastBelowThresholdTime = null;
  163. _buff = Uint8List(0);
  164. return;
  165. }
  166. //写入音频数据到缓存区中
  167. Uint8List _appendToBuffer(Uint8List newData) {
  168. var newBuffer = Uint8List(_buff.length + newData.length);
  169. newBuffer.setAll(0, _buff);
  170. newBuffer.setAll(_buff.length, newData);
  171. return newBuffer;
  172. }
  173. //读取缓存区中一帧数据
  174. Uint8List _getAudioData() {
  175. if (_buff.length >= _chunkSize) {
  176. // 从缓冲区中读取1280字节的数据
  177. var data = _buff.sublist(0, _chunkSize);
  178. // 移除已读取的数据
  179. _buff = _buff.sublist(_chunkSize);
  180. return data;
  181. } else if (_buff.length >= 0) {
  182. return _buff;
  183. } else {
  184. // 如果数据不足,返回空数据
  185. return Uint8List(0);
  186. }
  187. }
  188. //当前音量计算
  189. double _calculateAmplitude(Uint8List data) {
  190. Int16List samples = Int16List(data.length ~/ 2);
  191. ByteData byteData = ByteData.view(data.buffer);
  192. for (int i = 0; i < samples.length; i++) {
  193. samples[i] = byteData.getInt16(i * 2, Endian.little);
  194. }
  195. double sum = 0;
  196. for (int sample in samples) {
  197. sum += (sample * sample);
  198. }
  199. double rms = sqrt(sum / samples.length);
  200. return rms;
  201. }
  202. // 检查说话状态
  203. int _checkSpeakingStatus() {
  204. if (_volume > _speakingThreshold && !_isspeaking) {
  205. // 音量高于阈值,表示开始说话
  206. _isspeaking = true;
  207. return 1;
  208. } else if (_volume < _silenceThreshold) {
  209. // 音量低于阈值
  210. if (_lastBelowThresholdTime == null) {
  211. // 记录第一次低于阈值的时间
  212. _lastBelowThresholdTime = DateTime.now();
  213. } else if (DateTime.now().difference(_lastBelowThresholdTime!) >
  214. _silenceDuration) {
  215. // 持续低于阈值超过指定时间,表示结束说话
  216. _isspeaking = false;
  217. return 3;
  218. }
  219. } else {
  220. // 音量恢复到阈值以上,重置计时器
  221. _lastBelowThresholdTime = null;
  222. }
  223. if (!_isspeaking) {
  224. return 0;
  225. } else {
  226. return 2;
  227. }
  228. }
  229. //处理返回结果
  230. void _handleData(dynamic json) {
  231. final status = json['header']['status'];
  232. final sid = json['header']['sid'];
  233. var payload = json['payload'];
  234. if (payload != null) {
  235. payload = json['payload'] as Map<String, dynamic>;
  236. //转文字的结果
  237. if (payload.containsKey('recognition_results')) {
  238. final model = RecognitionResult.fromJson(json);
  239. if (model.payload?.recognitionResults?.text == null ||
  240. model.payload?.recognitionResults?.text?.trim() == '') return;
  241. onRecognitionResult(model);
  242. //翻译好的结果
  243. } else if (payload.containsKey('streamtrans_results')) {
  244. final model = StreamtransResult.fromJson(json);
  245. if (model.payload?.streamtransResults?.text == null ||
  246. model.payload?.streamtransResults?.text?.trim() == '') return;
  247. onStreamtransResult(model);
  248. //合成语音
  249. } else if (payload.containsKey('tts_results')) {
  250. final bytes = base64Decode(payload['tts_results']['audio']);
  251. if (bytes.isEmpty) return;
  252. onAudioResult(AudioModel(status: status, sid: sid, data: bytes));
  253. }
  254. }
  255. if (status == 2) {}
  256. }
  257. }
  258. //讯飞翻译任务
  259. class XunFeiTranslateTask {
  260. late WebSocketChannel _channel;
  261. bool isconnected = false;
  262. late Function(dynamic) handle;
  263. XunFeiTranslateTask(String url, Function(dynamic) handle) {
  264. _channel = WebSocketChannel.connect(Uri.parse(url));
  265. _channel.stream.timeout(Duration(seconds: 10)); //设置超时时间
  266. _channel.stream.listen(
  267. (message) {
  268. onMessage(message);
  269. },
  270. onError: (error) {
  271. isconnected = false;
  272. print('连接失败: $error');
  273. },
  274. onDone: () {
  275. isconnected = false;
  276. print('WebSocket 连接已关闭');
  277. print('Close code: ${_channel?.closeCode}');
  278. print('Close reason: ${_channel?.closeReason}');
  279. },
  280. cancelOnError: true,
  281. );
  282. isconnected = true;
  283. handle = handle;
  284. }
  285. Future<void> sendaudio(Map<String, dynamic> data) async {
  286. if (isconnected) {
  287. _channel.sink.add(json.encode(data));
  288. }
  289. }
  290. Future<void> onMessage(String message) async {
  291. try {
  292. // print("收到的消息:$message");
  293. // 对结果进行解析
  294. var messageMap = json.decode(message);
  295. var status = messageMap["header"]["status"];
  296. handle(messageMap);
  297. if (status == 2) {
  298. print("任务已结束!------------------------------------------");
  299. _channel.sink.close();
  300. }
  301. } catch (e) {
  302. print("收到的消息 异常:$e");
  303. }
  304. return;
  305. }
  306. }