|
|
|
@ -1,32 +1,42 @@ |
|
|
|
import 'dart:convert'; |
|
|
|
import 'dart:math'; |
|
|
|
import 'dart:typed_data'; |
|
|
|
import '../../data/models/appconfig.dart'; |
|
|
|
import 'package:get_storage/get_storage.dart'; |
|
|
|
|
|
|
|
import 'package:http/http.dart' as http; |
|
|
|
import 'package:crypto/crypto.dart'; |
|
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
|
|
|
|
import '../../data/models/appconfig.dart'; |
|
|
|
import '../../core/utils/logger.dart'; |
|
|
|
import 'language_manager.dart'; |
|
|
|
import '../../modules/picture_translation/models/picture_translation_models.dart'; |
|
|
|
|
|
|
|
//// 火山引擎机器翻译服务 |
|
|
|
/// 提供文本翻译功能 |
|
|
|
/// 提供图片翻译功能 |
|
|
|
/// 提供文本语种检测功能 |
|
|
|
class VolcanoTranslationService extends GetxService { |
|
|
|
// API 配置 |
|
|
|
late String _baseUrl; |
|
|
|
final String _endpoint = '/'; // 修改为根路径 |
|
|
|
late String _accessKey; |
|
|
|
late String _secretKey; |
|
|
|
late String _region; |
|
|
|
late String _service; |
|
|
|
late final String _accessKey; |
|
|
|
late final String _secretKey; |
|
|
|
late final String _region; |
|
|
|
late final String _service; |
|
|
|
|
|
|
|
// 初始化标志 |
|
|
|
bool _isInitialized = false; |
|
|
|
|
|
|
|
// 语言管理器 |
|
|
|
final LanguageManager _languageManager = Get.find<LanguageManager>(); |
|
|
|
// final GetStorage _storage = GetStorage(); |
|
|
|
|
|
|
|
// 文本语种检测频率控制与缓存 |
|
|
|
DateTime? _lastDetectTime; // 上次检测时间 |
|
|
|
String? _lastDetectTextKey; // 上次检测的文本键 |
|
|
|
final Duration _detectMinInterval = |
|
|
|
const Duration(milliseconds: 800); // 最小检测间隔,避免过频请求 |
|
|
|
final Map<String, String> _detectCache = {}; // 文本键 -> 检测到的短代码 缓存 |
|
|
|
|
|
|
|
// 构造函数 |
|
|
|
VolcanoTranslationService(); |
|
|
|
@override |
|
|
|
@ -46,25 +56,13 @@ class VolcanoTranslationService extends GetxService { |
|
|
|
return true; |
|
|
|
} |
|
|
|
try { |
|
|
|
// 优先使用后端 AppConfig,其次回退到 .env |
|
|
|
final bool appConfigReady = AppConfig.isInitialized(); |
|
|
|
|
|
|
|
final accessKeyFromConfig = appConfigReady |
|
|
|
? AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') |
|
|
|
: null; |
|
|
|
final secretKeyFromConfig = appConfigReady |
|
|
|
? AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') |
|
|
|
: null; |
|
|
|
final regionFromConfig = |
|
|
|
appConfigReady ? AppConfig.env('VOLCANO_TRANSLATION_REGION') : null; |
|
|
|
|
|
|
|
_accessKey = accessKeyFromConfig ?? |
|
|
|
_accessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? |
|
|
|
dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? |
|
|
|
''; |
|
|
|
_secretKey = secretKeyFromConfig ?? |
|
|
|
_secretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? |
|
|
|
dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? |
|
|
|
''; |
|
|
|
_region = regionFromConfig ?? |
|
|
|
_region = AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? |
|
|
|
dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? |
|
|
|
'cn-north-1'; |
|
|
|
|
|
|
|
@ -288,6 +286,136 @@ class VolcanoTranslationService extends GetxService { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 文本语种检测 |
|
|
|
/// 返回值为 ASR 代码(例如 `zh-CN` / `en-US`),若检测失败返回 null。 |
|
|
|
/// 频率控制:同一文本键在最小间隔内复用缓存结果,降低请求频率。 |
|
|
|
Future<String?> detectTextLanguage({ |
|
|
|
required String text, |
|
|
|
}) async { |
|
|
|
if (!_isInitialized) { |
|
|
|
Logger.error('语种检测服务未初始化'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 构建文本键用于频率控制与缓存:去除首尾空格并截取前 256 字符 |
|
|
|
final normalized = text.trim(); |
|
|
|
if (normalized.isEmpty) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
final textKey = |
|
|
|
normalized.length > 256 ? normalized.substring(0, 256) : normalized; |
|
|
|
|
|
|
|
// 命中缓存直接返回,避免重复请求 |
|
|
|
final cachedShortCode = _detectCache[textKey]; |
|
|
|
if (cachedShortCode != null && cachedShortCode.isNotEmpty) { |
|
|
|
final cachedAsr = |
|
|
|
_languageManager.getAsrCodeByShortCode(cachedShortCode); |
|
|
|
if (cachedAsr != null && cachedAsr.isNotEmpty) { |
|
|
|
return cachedAsr; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 频率控制:同一文本键在最小时间间隔内直接复用上次结果或跳过 |
|
|
|
final now = DateTime.now(); |
|
|
|
if (_lastDetectTime != null && _lastDetectTextKey == textKey) { |
|
|
|
final delta = now.difference(_lastDetectTime!); |
|
|
|
if (delta < _detectMinInterval) { |
|
|
|
Logger.info('语种检测过于频繁,已短路本次请求'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
_lastDetectTime = now; |
|
|
|
_lastDetectTextKey = textKey; |
|
|
|
|
|
|
|
// 构建请求体 |
|
|
|
final requestBody = { |
|
|
|
'TextList': [normalized], |
|
|
|
}; |
|
|
|
|
|
|
|
// 查询参数(官方文档:Action=LangDetect, Version=2020-06-01) |
|
|
|
final queryParams = { |
|
|
|
'Action': 'LangDetect', |
|
|
|
'Version': '2020-06-01', |
|
|
|
'Region': _region, |
|
|
|
'Service': _service, |
|
|
|
}; |
|
|
|
|
|
|
|
// 生成签名与头部 |
|
|
|
final headers = _generateSignature('POST', requestBody, queryParams); |
|
|
|
|
|
|
|
// 拼接请求 URL |
|
|
|
final queryString = Uri(queryParameters: queryParams).query; |
|
|
|
final url = '$_baseUrl/?$queryString'; |
|
|
|
|
|
|
|
// 发送请求,增加网络超时保护 |
|
|
|
final response = await http |
|
|
|
.post( |
|
|
|
Uri.parse(url), |
|
|
|
headers: headers, |
|
|
|
body: json.encode(requestBody), |
|
|
|
) |
|
|
|
.timeout(const Duration(seconds: 8)); |
|
|
|
|
|
|
|
if (response.statusCode == 200) { |
|
|
|
final responseData = json.decode(response.body); |
|
|
|
|
|
|
|
// 有效性校验:必须存在 DetectedLanguageList 且包含 Language/Confidence |
|
|
|
if (responseData is! Map || |
|
|
|
responseData['DetectedLanguageList'] is! List || |
|
|
|
(responseData['DetectedLanguageList'] as List).isEmpty) { |
|
|
|
Logger.error('语种检测响应无有效结果: ${response.body}'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
// 选择置信度最高的结果 |
|
|
|
String? bestShortCode; |
|
|
|
double bestConfidence = -1; |
|
|
|
for (final item in (responseData['DetectedLanguageList'] as List)) { |
|
|
|
if (item is Map) { |
|
|
|
final lang = item['Language']?.toString(); |
|
|
|
final conf = (item['Confidence'] is num) |
|
|
|
? (item['Confidence'] as num).toDouble() |
|
|
|
: null; |
|
|
|
if (lang != null && lang.isNotEmpty && conf != null) { |
|
|
|
if (conf > bestConfidence) { |
|
|
|
bestConfidence = conf; |
|
|
|
bestShortCode = lang; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 置信度阈值校验:过低则视为失败 |
|
|
|
if (bestShortCode == null || bestConfidence < 0.6) { |
|
|
|
Logger.info('语种检测置信度不足: code=$bestShortCode, conf=$bestConfidence'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
// 映射为 ASR 代码 |
|
|
|
final asrCode = _languageManager.getAsrCodeByShortCode(bestShortCode); |
|
|
|
if (asrCode == null || asrCode.isEmpty) { |
|
|
|
Logger.error('语种检测返回短代码无法映射为ASR: $bestShortCode'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
// 写入缓存(短代码缓存,便于后续复用) |
|
|
|
_detectCache[textKey] = bestShortCode; |
|
|
|
|
|
|
|
Logger.info( |
|
|
|
'lxm---语种检测成功: $bestShortCode (conf=$bestConfidence) -> $asrCode'); |
|
|
|
return asrCode; |
|
|
|
} |
|
|
|
|
|
|
|
Logger.error( |
|
|
|
'语种检测HTTP错误: status=${response.statusCode}, body=${response.body}'); |
|
|
|
return null; |
|
|
|
} catch (e) { |
|
|
|
Logger.error('语种检测请求异常: $e'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 图片翻译(整图识别+回贴翻译) |
|
|
|
Future<ImageTranslationResult?> translateImage({ |
|
|
|
required Uint8List imageBytes, |
|
|
|
@ -345,7 +473,8 @@ class VolcanoTranslationService extends GetxService { |
|
|
|
responseData['ResponseMetadata'] is Map && |
|
|
|
(responseData['ResponseMetadata']['Error'] != null)) { |
|
|
|
final err = responseData['ResponseMetadata']['Error']; |
|
|
|
Logger.error('图片翻译错误: code=${err['Code']}, message=${err['Message']}'); |
|
|
|
Logger.error( |
|
|
|
'图片翻译错误: code=${err['Code']}, message=${err['Message']}'); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
|