import 'dart:math'; import 'package:dio/dio.dart'; import '../../core/utils/logger.dart'; import '../models/appconfig.dart'; import 'ai_vendor_lock.dart'; /// 微软文本翻译。同声 / 面对面 / 音视频里扩展语种走这条,不经服务端阿里。 /// /// 密钥与通话翻译同一套:`mt_azure` 的 subscription_key / region。 /// 请求形状与原生 `MicrosoftTranslationServiceImpl` 一致。 class MicrosoftTextTranslation { static const defaultEndpoint = 'https://api.cognitive.microsofttranslator.com'; final Dio _dio = Dio(BaseOptions( connectTimeout: const Duration(seconds: 10), sendTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )); Future translate({ required String text, required String from, required String to, CancelToken? cancelToken, }) async { final src = text.trim(); if (src.isEmpty) return null; if (aiOnlyIflytek) { Logger.w('MsTrans', '测试服只允许科大讯飞,不调用微软翻译'); return null; } final key = AppConfig.cred('mt_azure', 'subscription_key', 'AZURE_TRANSLATION_KEY'); if (key == null || key.isEmpty) { Logger.w('MsTrans', '微软翻译密钥未下发,跳过 $from→$to'); return null; } final region = AppConfig.cred('mt_azure', 'region', 'AZURE_TRANSLATION_REGION'); final endpoint = AppConfig.cred('mt_azure', 'endpoint', 'AZURE_TRANSLATION_ENDPOINT') ?? defaultEndpoint; final headers = { 'Ocp-Apim-Subscription-Key': key, 'Content-Type': 'application/json', 'X-ClientTraceId': _traceId(), }; if (region != null && region.isNotEmpty) { headers['Ocp-Apim-Subscription-Region'] = region; } try { final query = { 'api-version': '3.0', 'to': to, }; if (from.isNotEmpty) query['from'] = from; final response = await _dio.post( '${endpoint.replaceAll(RegExp(r'/+$'), '')}/translate', queryParameters: query, data: [ {'text': src}, ], options: Options(headers: headers), cancelToken: cancelToken, ); final out = microsoftTranslationText(response.data); if (out == null) { Logger.w('MsTrans', '微软翻译返回空: $from→$to'); } return out; } on DioException catch (e) { if (e.type == DioExceptionType.cancel) return null; Logger.w('MsTrans', '微软翻译失败($from→$to): $e'); return null; } catch (e) { Logger.w('MsTrans', '微软翻译失败($from→$to): $e'); return null; } } } /// 阿里机器翻译盖不住的语种:繁体会被收成简体,乌克兰语和克罗地亚语直接被拒。 bool microsoftMtRequired(String from, String to) { bool hit(String raw) { switch (raw.trim().toLowerCase()) { case 'zh-hant': case 'zh-hant-hk': case 'zh-tw': case 'zh-hk': case 'uk': case 'uk-ua': case 'hr': case 'hr-hr': return true; default: return false; } } return hit(from) || hit(to); } /// 收成微软翻译认的码。空串留给自动检测,请求里不带 from。 String microsoftMtCode(String raw) { final code = raw.trim(); if (code.isEmpty) return ''; switch (code.toLowerCase()) { case 'zh-hant': case 'zh-hant-hk': case 'zh-tw': case 'zh-hk': return 'zh-Hant'; case 'zh': case 'zh-cn': case 'zh-hans': return 'zh-Hans'; case 'uk': case 'uk-ua': return 'uk'; case 'hr': case 'hr-hr': return 'hr'; default: final dash = code.indexOf('-'); if (dash > 0) return code.substring(0, dash).toLowerCase(); return code.toLowerCase(); } } /// 解析微软翻译 3.0 的响应。失败或空译文返回 null。 String? microsoftTranslationText(dynamic data) { if (data is! List || data.isEmpty) return null; final first = data.first; if (first is! Map) return null; final translations = first['translations']; if (translations is! List || translations.isEmpty) return null; final item = translations.first; if (item is! Map) return null; final text = item['text']; if (text is! String) return null; final out = text.trim(); return out.isEmpty ? null : out; } String _traceId() { final rand = Random(); String hex(int n) => List.generate(n, (_) => rand.nextInt(16).toRadixString(16)).join(); return '${hex(8)}-${hex(4)}-4${hex(3)}-${hex(4)}-${hex(12)}'; }