import 'dart:convert'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import '../../core/utils/logger.dart'; import '../../data/models/appconfig.dart'; import 'language_configs/azure_language_config.dart'; import 'language_manager.dart'; /// 微软 Azure Translator REST 翻译服务 /// 端点:https://api.cognitive.microsofttranslator.com /// 文档:https://learn.microsoft.com/azure/ai-services/translator/reference/v3-0-translate class MicrosoftTranslationService extends GetxService { static const String _defaultEndpoint = 'https://api.cognitive.microsofttranslator.com'; static const String _apiPath = '/translate'; static const String _apiVersion = '3.0'; late final String _subscriptionKey; late final String _region; late final String _endpoint; bool _isInitialized = false; bool get isInitialized => _isInitialized; final LanguageManager _languageManager = Get.find(); /// shortCode -> 微软 translationCode 缓存(由 getAzureTranslationLanguageSpecs 构建) final Map _shortCodeToTranslationCode = {}; @override Future onInit() async { await initialize(); super.onInit(); } Future initialize() async { if (_isInitialized) return true; try { _subscriptionKey = AppConfig.env('AZURE_TRANSLATION_KEY') ?? ''; _region = AppConfig.env('AZURE_TRANSLATION_REGION') ?? ''; _endpoint = AppConfig.env('AZURE_TRANSLATION_ENDPOINT') ?? _defaultEndpoint; if (_subscriptionKey.isEmpty) { print( '🟢[MS-TRANS-DBG] AZURE_TRANSLATION_KEY 未配置,服务无法初始化'); Logger.error('微软翻译服务初始化失败:AZURE_TRANSLATION_KEY 为空'); return false; } for (final spec in getAzureTranslationLanguageSpecs()) { final short = spec['shortCode']; final code = spec['translationCode']; if (short != null && code != null) { _shortCodeToTranslationCode[short] = code; } } _isInitialized = true; print( '🟢[MS-TRANS-DBG] 微软翻译服务初始化成功: endpoint=$_endpoint, region=$_region, keyLen=${_subscriptionKey.length}, specCount=${_shortCodeToTranslationCode.length}'); Logger.info('微软翻译服务初始化成功 region=$_region'); return true; } catch (e) { print('🟢[MS-TRANS-DBG] 微软翻译服务初始化异常: $e'); Logger.error('微软翻译服务初始化异常: $e'); return false; } } /// 把 ASR 代码(如 `zh-CN`)映射为微软 Translator 语言码(如 `zh-Hans`) /// 找不到返回 null String? _toTranslationCode(String asrCode) { if (asrCode.isEmpty) return null; final short = _languageManager.getShortCodeByAsrCode(asrCode); if (short == null) return null; return _shortCodeToTranslationCode[short]; } /// 文本翻译 /// [text] 待翻译文本 /// [sourceLanguageCode] 源语言 ASR 码(传空串则让微软自动检测) /// [targetLanguageCode] 目标语言 ASR 码 Future translateText({ required String text, required String sourceLanguageCode, required String targetLanguageCode, }) async { if (!_isInitialized) { print('🟢[MS-TRANS-DBG] 未初始化,返回 null'); Logger.error('微软翻译服务未初始化'); return null; } if (text.isEmpty) return null; try { final String? targetCode = _toTranslationCode(targetLanguageCode); if (targetCode == null) { print( '🟢[MS-TRANS-DBG] 目标语言码映射失败: targetAsr=$targetLanguageCode,返回 null'); Logger.error('微软翻译失败:目标语言 $targetLanguageCode 不在映射表'); return null; } final String? sourceCode = _toTranslationCode(sourceLanguageCode); final queryParams = { 'api-version': _apiVersion, 'to': targetCode, }; if (sourceCode != null && sourceCode.isNotEmpty) { queryParams['from'] = sourceCode; } final uri = Uri.parse('$_endpoint$_apiPath') .replace(queryParameters: queryParams); final body = json.encode([ {'text': text} ]); final headers = { 'Content-Type': 'application/json', 'Ocp-Apim-Subscription-Key': _subscriptionKey, 'Ocp-Apim-Subscription-Region': _region, 'X-ClientTraceId': _newTraceId(), }; print( '🟢[MS-TRANS-DBG] 发起翻译请求: uri=$uri, from=$sourceCode, to=$targetCode, text=$text'); final response = await http.post(uri, headers: headers, body: body).timeout( const Duration(seconds: 10), onTimeout: () => http.Response('{"error":"timeout"}', 408), ); print( '🟢[MS-TRANS-DBG] 翻译响应: status=${response.statusCode}, bodyLen=${response.body.length}, body=${response.body.length > 500 ? response.body.substring(0, 500) : response.body}'); if (response.statusCode != 200) { Logger.error( '微软翻译 HTTP 错误: status=${response.statusCode}, body=${response.body}'); return null; } final decoded = json.decode(response.body); if (decoded is! List || decoded.isEmpty) { print('🟢[MS-TRANS-DBG] 响应非 List 或为空,解析失败'); return null; } final first = decoded.first; if (first is! Map) return null; final translations = first['translations']; if (translations is! List || translations.isEmpty) return null; final translated = translations.first; if (translated is! Map) return null; final result = translated['text']; if (result is! String) return null; print('🟢[MS-TRANS-DBG] 翻译成功: $result'); return result; } catch (e, st) { print('🟢[MS-TRANS-DBG] 翻译请求异常: $e\n$st'); Logger.error('微软翻译请求异常: $e'); return null; } } // ===== 语种检测(Azure Translator /detect)===== // 与原火山实现保持同一契约:返回 **ASR 代码**(如 zh-CN), // 带同文本缓存与最小请求间隔,避免自动检测场景下频繁打接口。 DateTime? _lastDetectTime; String? _lastDetectTextKey; final Map _detectCache = {}; static const Duration _detectMinInterval = Duration(milliseconds: 800); Future detectTextLanguage({required String text}) async { if (!_isInitialized) { Logger.error('语种检测服务未初始化'); return null; } final normalized = text.trim(); if (normalized.isEmpty) return null; final textKey = normalized.length > 256 ? normalized.substring(0, 256) : normalized; // 命中缓存直接返回 final cachedShort = _detectCache[textKey]; if (cachedShort != null && cachedShort.isNotEmpty) { final cachedAsr = _languageManager.getAsrCodeByShortCode(cachedShort); if (cachedAsr != null && cachedAsr.isNotEmpty) return cachedAsr; } // 频率控制:同一文本在最小间隔内不重复请求 final now = DateTime.now(); if (_lastDetectTime != null && _lastDetectTextKey == textKey) { if (now.difference(_lastDetectTime!) < _detectMinInterval) { Logger.info('语种检测过于频繁,已短路本次请求'); return null; } } _lastDetectTime = now; _lastDetectTextKey = textKey; try { final uri = Uri.parse('$_endpoint/detect') .replace(queryParameters: {'api-version': _apiVersion}); final response = await http .post(uri, headers: { 'Content-Type': 'application/json', 'Ocp-Apim-Subscription-Key': _subscriptionKey, 'Ocp-Apim-Subscription-Region': _region, 'X-ClientTraceId': _newTraceId(), }, body: json.encode([ {'text': textKey} ])) .timeout(const Duration(seconds: 10), onTimeout: () => http.Response('{"error":"timeout"}', 408)); if (response.statusCode != 200) { Logger.error( '微软语种检测 HTTP 错误: status=${response.statusCode}, body=${response.body}'); return null; } final decoded = json.decode(response.body); if (decoded is! List || decoded.isEmpty) return null; final first = decoded.first; if (first is! Map) return null; final short = first['language']; if (short is! String || short.isEmpty) return null; // Azure 返回的是短代码(en / zh-Hans / ja …),统一收敛到项目的 shortCode final normalizedShort = short.split('-').first.toLowerCase(); _detectCache[textKey] = normalizedShort; final asr = _languageManager.getAsrCodeByShortCode(normalizedShort); Logger.info('微软语种检测: $short -> short=$normalizedShort, asr=$asr'); return asr; } catch (e) { Logger.error('微软语种检测异常: $e'); return null; } } String _newTraceId() { final now = DateTime.now().microsecondsSinceEpoch; return 'ms-trans-$now'; } }