tencent_asr_audio_probe.dart
4.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty) {
stderr.writeln('usage: dart run tool/tencent_asr_audio_probe.dart /tmp/test.wav');
exitCode = 2;
return;
}
const secretId = 'AKID9Vgy8FEC5qSxLcllVCmXzJWaVSI0Z2Nb';
const secretKey = 'c2c5MTOBo5d9XilhaLzVSG4DE7qRuCJF';
const appId = '1308375824';
final bytes = await File(args.first).readAsBytes();
final int dataStart = _wavDataOffset(bytes);
final List<int> pcm = bytes.sublist(dataStart);
stderr.writeln('pcmBytes=${pcm.length}, frames=${(pcm.length / 6400).ceil()}');
final int now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final String voiceId = 'audio_probe_${DateTime.now().microsecondsSinceEpoch}';
final Map<String, String> params = <String, String>{
'secretid': secretId,
'timestamp': '$now',
'expired': '${now + 86400}',
'nonce': '${(DateTime.now().microsecondsSinceEpoch % 999999999).abs() + 1}',
'engine_model_type': '16k_zh',
'voice_id': voiceId,
'voice_format': '1',
'needvad': '1',
'filter_modal': '1',
'filter_punc': '1',
'convert_num_mode': '1',
};
final sorted = params.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
final canonical = sorted.map((e) => '${e.key}=${e.value}').join('&');
final signText = 'asr.cloud.tencent.com/asr/v2/$appId?$canonical';
final digest = Hmac(sha1, utf8.encode(secretKey))
.convert(utf8.encode(signText))
.bytes;
final signature = base64.encode(digest);
final query = sorted
.map((e) => '${e.key}=${Uri.encodeQueryComponent(e.value)}')
.join('&');
final uri = Uri.parse(
'wss://asr.cloud.tencent.com/asr/v2/$appId?$query&signature=${Uri.encodeQueryComponent(signature)}');
final channel = WebSocketChannel.connect(uri);
final handshake = Completer<void>();
final done = Completer<void>();
final text = StringBuffer();
channel.stream.listen(
(dynamic m) {
final map = jsonDecode(m.toString()) as Map<String, dynamic>;
final code = map['code'] as int? ?? 0;
if (code != 0) {
stderr.writeln('ERROR: $m');
if (!done.isCompleted) done.complete();
if (!handshake.isCompleted) handshake.completeError('code=$code');
return;
}
if (!handshake.isCompleted) handshake.complete();
final result = map['result'] as Map<String, dynamic>?;
if (result != null) {
final sliceType = result['slice_type'] as int?;
final value = result['voice_text_str']?.toString() ?? '';
stderr.writeln('slice=$sliceType text=$value');
if (sliceType == 2) text.write(value);
}
if (map['final'] == 1 && !done.isCompleted) done.complete();
},
onError: (Object e) {
stderr.writeln('socket error: $e');
if (!done.isCompleted) done.complete();
},
onDone: () {
stderr.writeln('socket done');
if (!done.isCompleted) done.complete();
},
);
await channel.ready.timeout(const Duration(seconds: 10));
await handshake.future.timeout(const Duration(seconds: 10));
int offset = 0;
final stopwatch = Stopwatch()..start();
while (offset < pcm.length) {
final int end = (offset + 6400).clamp(0, pcm.length);
channel.sink.add(pcm.sublist(offset, end));
offset = end;
final int sleep = 200 - stopwatch.elapsedMilliseconds;
if (sleep > 0) await Future<void>.delayed(Duration(milliseconds: sleep));
stopwatch.reset();
}
channel.sink.add(jsonEncode(<String, String>{'type': 'end'}));
await done.future.timeout(const Duration(seconds: 15));
await channel.sink.close();
print('RESULT: ${text.toString().trim()}');
}
int _wavDataOffset(List<int> bytes) {
if (bytes.length > 12 &&
String.fromCharCodes(bytes.sublist(0, 4)) == 'RIFF' &&
String.fromCharCodes(bytes.sublist(8, 12)) == 'WAVE') {
int offset = 12;
while (offset + 8 <= bytes.length) {
final String id = String.fromCharCodes(bytes.sublist(offset, offset + 4));
final int size = _leInt(bytes, offset + 4);
if (id == 'data') return offset + 8;
offset += 8 + size + (size.isOdd ? 1 : 0);
}
throw StateError('data chunk not found');
}
return 0;
}
int _leInt(List<int> bytes, int start) {
return bytes[start] |
(bytes[start + 1] << 8) |
(bytes[start + 2] << 16) |
(bytes[start + 3] << 24);
}