14 changed files with 312 additions and 45 deletions
@ -0,0 +1,170 @@ |
|||
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<LanguageManager>(); |
|||
|
|||
/// shortCode -> 微软 translationCode 缓存(由 getAzureTranslationLanguageSpecs 构建) |
|||
final Map<String, String> _shortCodeToTranslationCode = {}; |
|||
|
|||
@override |
|||
Future<void> onInit() async { |
|||
await initialize(); |
|||
super.onInit(); |
|||
} |
|||
|
|||
Future<bool> 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<String?> 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 = <String, String>{ |
|||
'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; |
|||
} |
|||
} |
|||
|
|||
String _newTraceId() { |
|||
final now = DateTime.now().microsecondsSinceEpoch; |
|||
return 'ms-trans-$now'; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue