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.
 
 
 
 
 
 

174 lines
5.5 KiB

import 'dart:async';
import 'dart:typed_data';
import 'package:sparkchain/sparkchain.dart';
import '../../core/utils/logger.dart';
import '../models/appconfig.dart';
/// 端上讯飞翻译。没配密钥就返回 null,调用方继续走原来的服务端翻译。
class SparkChainSession {
SparkChainSession._();
static const _tag = 'SparkChain';
static bool _ready = false;
static bool _eventsAttached = false;
static bool listening = false;
static int _seq = 0;
static final Map<String, Completer<Map<String, dynamic>>> _waiters = {};
static final Map<String, void Function(Uint8List pcm, bool isFinal)> _ttsHandlers = {};
static final Map<String, Completer<void>> _ttsWaiters = {};
/// 听写结果。只在 [startListen] 成功之后、[stopListen] 之前有意义。
static void Function(Map<String, dynamic> event)? asrListener;
static Future<bool> ensure() async {
if (_ready) return true;
final appId = AppConfig.cred('sparkchain', 'app_id', 'SPARKCHAIN_APP_ID');
final apiKey = AppConfig.cred('sparkchain', 'api_key', 'SPARKCHAIN_API_KEY');
final apiSecret =
AppConfig.cred('sparkchain', 'api_secret', 'SPARKCHAIN_API_SECRET');
if (appId == null || apiKey == null || apiSecret == null) {
return false;
}
if (!_eventsAttached) {
_eventsAttached = true;
SparkChain.events.listen(_onEvent);
}
final apiType =
int.tryParse(AppConfig.cred('sparkchain', 'api_type', 'SPARKCHAIN_API_TYPE') ?? '') ??
1;
final code = await SparkChain.init(
appId: appId,
apiKey: apiKey,
apiSecret: apiSecret,
apiType: apiType,
);
Logger.i(_tag, 'init code=$code apiType=$apiType');
_ready = code == 0;
return _ready;
}
static void _onEvent(Map<String, dynamic> event) {
final id = event['id']?.toString() ?? '';
final kind = event['kind'];
if (kind == 'its' || (kind == 'error' && event['where'] == 'its')) {
final waiter = _waiters[id];
if (waiter != null && !waiter.isCompleted) waiter.complete(event);
}
if (kind == 'asr' || (kind == 'error' && event['where'] == 'asr')) {
asrListener?.call(event);
}
if (kind == 'tts' || (kind == 'error' && event['where'] == 'tts')) {
_onTts(id, event);
}
}
static void _onTts(String id, Map<String, dynamic> event) {
if (event['kind'] == 'error') {
_ttsHandlers.remove(id);
final waiter = _ttsWaiters.remove(id);
if (waiter != null && !waiter.isCompleted) {
waiter.completeError(StateError('${event['code']}'));
}
return;
}
final raw = event['pcm'];
final pcm = raw is Uint8List ? raw : Uint8List(0);
_ttsHandlers[id]?.call(pcm, event['final'] == true);
if (event['final'] == true) {
_ttsHandlers.remove(id);
final waiter = _ttsWaiters.remove(id);
if (waiter != null && !waiter.isCompleted) waiter.complete();
}
}
/// 只听 [lang] 这一种。ga 走实时转写,其余走听写。SDK 不自己开麦。
static Future<bool> startListen(String lang) async {
if (!await ensure()) return false;
final code = await SparkChain.startListen(id: 'listen', lang: sparkChainShortCode(lang));
listening = code == 0;
if (!listening) {
Logger.w(_tag, 'startListen 失败 code=$code');
}
return listening;
}
static void write(Uint8List pcm) {
if (!listening || pcm.isEmpty) return;
unawaited(SparkChain.write(pcm));
}
static Future<void> stopListen() async {
if (!listening) return;
listening = false;
asrListener = null;
try {
await SparkChain.stopListen();
} catch (e) {
Logger.w(_tag, 'stopListen: $e');
}
}
/// 合成一段。PCM 按到达顺序交给 [onPcm],整段结束或失败才返回。
static Future<void> speak({
required String lang,
required String text,
required void Function(Uint8List pcm, bool isFinal) onPcm,
}) async {
if (!await ensure()) {
throw StateError('sparkchain not ready');
}
final id = 'tts-${++_seq}';
final waiter = Completer<void>();
_ttsWaiters[id] = waiter;
_ttsHandlers[id] = onPcm;
final code = await SparkChain.speak(
id: id,
lang: sparkChainShortCode(lang),
text: text,
);
if (code != 0) {
_ttsHandlers.remove(id);
_ttsWaiters.remove(id);
throw StateError('speak $code');
}
await waiter.future.timeout(const Duration(seconds: 20));
}
/// 短码 zh / en / ga。失败返回 null,不抛。
static Future<String?> translate({
required String from,
required String to,
required String text,
}) async {
if (!await ensure()) return null;
final id = '${++_seq}';
final waiter = Completer<Map<String, dynamic>>();
_waiters[id] = waiter;
try {
final code = await SparkChain.translate(
id: id,
from: sparkChainShortCode(from),
to: sparkChainShortCode(to),
text: text,
);
if (code != 0) {
Logger.w(_tag, 'translate 启动失败 code=$code');
return null;
}
final event = await waiter.future.timeout(const Duration(seconds: 12));
if (event['kind'] != 'its') {
Logger.w(_tag, 'translate 失败 code=${event['code']} sid=${event['sid']}');
return null;
}
final out = (event['text'] as String?)?.trim() ?? '';
return out.isEmpty ? null : out;
} on TimeoutException {
Logger.w(_tag, 'translate 超时');
return null;
} finally {
_waiters.remove(id);
}
}
}