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 lastStartCode = 0; static int _seq = 0; static final Map>> _waiters = {}; static final Map _ttsHandlers = {}; static final Map> _ttsWaiters = {}; static final Map event)> _llmHandlers = {}; /// 听写结果。只在 [startListen] 成功之后、[stopListen] 之前有意义。 static void Function(Map event)? asrListener; /// 同声 AST 的识别、译文和合成音频。 static void Function(Map event)? astListener; static Future 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) { final missing = [ if (appId == null) 'app_id', if (apiKey == null) 'api_key', if (apiSecret == null) 'api_secret', ]; Logger.w(_tag, '密钥未下发,跳过 init: ${missing.join(",")}'); 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.w(_tag, 'init code=$code apiType=$apiType'); _ready = code == 0; return _ready; } /// 配置拉下来之后只做 init。不要在这里 startListen 或翻译: /// 新加坡节点连不上时 SDK 会在关闭 WebSocket 的线程里 SIGSEGV,启动即闪退。 static Future warmUp() async { await ensure(); } static void _onEvent(Map 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 == 'ast' || (kind == 'error' && event['where'] == 'ast')) { astListener?.call(event); } if (kind == 'tts' || (kind == 'error' && event['where'] == 'tts')) { _onTts(id, event); } if (kind == 'llm' || (kind == 'error' && event['where'] == 'llm')) { _llmHandlers[id]?.call(event); } } static void _onTts(String id, Map 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(); } } /// 同声:一条微软同传。发音人是短名,例如 JennyNeural。 static Future startAst({ required String lang, required String targetLang, required String speaker, }) async { if (!await ensure()) return false; 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'); // 同传网关认的是自己的 accessKeyId,不是 SparkChain 应用的 apiKey。 // 后台「同传单独」三项有值就用那一组,空才回退。 final ownKey = AppConfig.field('sparkchain', 'ast_api_key') != null; final astApp = AppConfig.field('sparkchain', 'ast_app_id') ?? appId ?? ''; final astKey = AppConfig.field('sparkchain', 'ast_api_key') ?? apiKey ?? ''; final astSecret = AppConfig.field('sparkchain', 'ast_api_secret') ?? apiSecret ?? ''; final code = await SparkChain.startAst( id: 'listen', lang: lang, targetLang: targetLang, speaker: speaker, astAppId: astApp, astApiKey: astKey, astApiSecret: astSecret, ); lastStartCode = code; listening = code == 0; Logger.w( _tag, 'AST 启动 code=$code RegionMSFT lang=$lang target_lang=$targetLang ' 'tts_target_lang=$targetLang tts_speaker=$speaker use_tts=true ' '同传密钥=${ownKey ? "单独字段" : "回退应用密钥"}', ); return listening; } /// 同声拼接:微软实时转写。四个固定参数加 language_name,不走星火听写。 static Future startSpliceListen(String locale) async { if (!await ensure()) return false; final code = await SparkChain.startSpliceListen(id: 'listen', locale: locale); lastStartCode = code; listening = code == 0; _writeFails = 0; Logger.w( _tag, 'RTASR 启动 code=$code RegionMSFT lang=msft accent=mandarin ' 'domain=ist_mul_msft dwa=wpgs language_name=$locale', ); return listening; } /// 微软区域实时转写,语种用界面语言的区域码。SDK 不自己开麦。 static Future startMsftListen(String locale) async { if (!await ensure()) return false; final code = await SparkChain.startMsftListen(id: 'listen', locale: locale); listening = code == 0; if (!listening) Logger.w(_tag, '微软听写启动失败 code=$code locale=$locale'); return listening; } /// 只听 [lang] 这一种。ga 走实时转写,其余走听写。SDK 不自己开麦。 static Future 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 var _writeFails = 0; /// 苹果实时转写文档:每 40ms 送 1280 字节。一次把积压全送进去,引擎会出错, /// 大约 10 秒后就是 18802。 static const _paceFrame = 1280; static final List _paceBuf = []; static Timer? _paceTimer; static void write(Uint8List pcm) { if (!listening || pcm.isEmpty) return; _paceBuf.addAll(pcm); const maxBytes = 32000 * 2; if (_paceBuf.length > maxBytes) { _paceBuf.removeRange(0, _paceBuf.length - maxBytes); } _paceTimer ??= Timer.periodic(const Duration(milliseconds: 40), (_) { _pumpPace(); }); } static void _pumpPace() { if (!listening || _paceBuf.isEmpty) { _paceTimer?.cancel(); _paceTimer = null; return; } final n = _paceBuf.length < _paceFrame ? _paceBuf.length : _paceFrame; final frame = Uint8List.fromList(_paceBuf.sublist(0, n)); _paceBuf.removeRange(0, n); unawaited(SparkChain.write(frame).then((code) { if (code != 0 && _writeFails < 3) { _writeFails++; Logger.w(_tag, 'write 失败 code=$code 字节=${frame.length}'); } })); } static void _stopPace() { _paceTimer?.cancel(); _paceTimer = null; _paceBuf.clear(); } static Future stopListen() async { listening = false; _stopPace(); asrListener = null; astListener = null; // 启动失败时 listening 仍是 false,但原生引擎可能已经占着一条没连上的会话。 // 不拆掉的话,下一次 start 会在这条会话上一直耗到超时。 try { await SparkChain.stopListen(); } catch (e) { Logger.w(_tag, 'stopListen: $e'); } } /// 微软区域神经语音。PCM 16 kHz,按到达顺序交给 [onPcm]。 static Future speakNeural({ required String voice, 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(); _ttsWaiters[id] = waiter; _ttsHandlers[id] = onPcm; final code = await SparkChain.speak( id: id, lang: 'msft', text: text, voice: voice, msft: true, ); if (code != 0) { _ttsHandlers.remove(id); _ttsWaiters.remove(id); throw StateError('speak $code'); } await waiter.future.timeout(const Duration(seconds: 30)); } static Future stopSpeak() async { try { await SparkChain.stopSpeak(); } catch (e) { Logger.w(_tag, 'stopSpeak: $e'); } } /// 端上文本对话。fresh 为 true 时清掉 SDK 里上一轮的上下文。 static Future chat({ required String query, required String system, required String tools, required bool fresh, }) async { if (!await ensure()) throw StateError('sparkchain not ready'); final id = 'llm-${++_seq}'; final waiter = Completer(); final text = StringBuffer(); var functionCall = ''; _llmHandlers[id] = (event) { if (event['kind'] == 'error') { if (!waiter.isCompleted) { waiter.completeError(StateError('${event['code']}')); } return; } text.write(event['text'] ?? ''); final call = (event['functionCall'] as String?) ?? ''; if (call.isNotEmpty) functionCall = call; if (event['final'] == true && !waiter.isCompleted) { waiter.complete(SparkLlmReply(text.toString(), functionCall)); } }; try { final code = await SparkChain.chat( id: id, query: query, system: system, tools: tools, fresh: fresh, ); if (code != 0) throw StateError('chat $code'); return await waiter.future.timeout(const Duration(seconds: 25)); } finally { _llmHandlers.remove(id); } } /// 合成一段。PCM 按到达顺序交给 [onPcm],整段结束或失败才返回。 static Future 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(); _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,不抛。 /// [prepared] 为真时语种码已经是 SDK 要的写法,不再收成 zh / en。 static Future translate({ required String from, required String to, required String text, bool prepared = false, }) async { if (!await ensure()) return null; final id = '${++_seq}'; final waiter = Completer>(); _waiters[id] = waiter; final src = prepared ? from : sparkChainShortCode(from); final dst = prepared ? to : sparkChainShortCode(to); try { final code = await SparkChain.translate( id: id, from: src, to: dst, text: text, ); if (code != 0) { Logger.w(_tag, 'translate 启动失败 code=$code'); return null; } final event = await waiter.future.timeout(const Duration(seconds: 20)); if (event['kind'] != 'its') { Logger.w(_tag, 'translate 失败 code=${event['code']} sid=${event['sid']}'); return null; } Logger.w(_tag, 'translate engine=${event['engine']}'); final out = (event['text'] as String?)?.trim() ?? ''; return out.isEmpty ? null : out; } on TimeoutException { Logger.w(_tag, 'translate 超时'); return null; } finally { _waiters.remove(id); } } } class SparkLlmReply { SparkLlmReply(this.text, this.functionCall); final String text; final String functionCall; }