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.1 KiB
247 lines
9.1 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.cred('mt_azure', 'subscription_key', 'AZURE_TRANSLATION_KEY') ?? '';
|
|
_region = AppConfig.cred('mt_azure', 'region', 'AZURE_TRANSLATION_REGION') ?? '';
|
|
_endpoint =
|
|
AppConfig.cred('mt_azure', 'endpoint', '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';
|
|
}
|
|
}
|
|
|