tencent_asr_audio_probe.dart 4.41 KB
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);
}