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.
 
 
 
 
 
 

247 lines
9.0 KiB

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;
}
}
// ===== 语种检测(Azure Translator /detect)=====
// 与原火山实现保持同一契约:返回 **ASR 代码**(如 zh-CN),
// 带同文本缓存与最小请求间隔,避免自动检测场景下频繁打接口。
DateTime? _lastDetectTime;
String? _lastDetectTextKey;
final Map<String, String> _detectCache = {};
static const Duration _detectMinInterval = Duration(milliseconds: 800);
Future<String?> 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';
}
}