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.
 
 
 
 
 
 

152 lines
4.5 KiB

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<String?> 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 = <String, String>{
'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 = <String, String>{
'api-version': '3.0',
'to': to,
};
if (from.isNotEmpty) query['from'] = from;
final response = await _dio.post<dynamic>(
'${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)}';
}