80 changed files with 3802 additions and 1663 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,168 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_intro/flutter_intro.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:lottie/lottie.dart'; |
|||
|
|||
class IntroOverlayBuilder { |
|||
static Widget homeBuilder( |
|||
StepWidgetParams params, |
|||
bool isWakeup, { |
|||
Function? onNext, |
|||
}) { |
|||
return Column( |
|||
children: [ |
|||
isWakeup |
|||
? _AnimatedLottieWidget() |
|||
: SizedBox( |
|||
width: 200.w, |
|||
height: 200.w, |
|||
child: Lottie.asset('assets/lottie/intro_foriday.json'), |
|||
), |
|||
Padding( |
|||
padding: EdgeInsets.only(bottom: 20.w), |
|||
child: Text( |
|||
isWakeup ? '我在呢~' : '对着麦克风说:“小言小言”', |
|||
style: TextStyle( |
|||
fontSize: 15.sp, |
|||
color: Colors.white, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
), |
|||
if (onNext != null) |
|||
_button( |
|||
'下一步', |
|||
() { |
|||
params.onNext!(); |
|||
onNext(); |
|||
}, |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
static Widget aiBuilder( |
|||
StepWidgetParams params, |
|||
String text, { |
|||
String buttonText = '下一步', |
|||
bool alignment = true, |
|||
Function? onNext, |
|||
Function? onFinish, |
|||
}) { |
|||
return Column( |
|||
crossAxisAlignment: |
|||
alignment ? CrossAxisAlignment.start : CrossAxisAlignment.end, |
|||
children: [ |
|||
Padding( |
|||
padding: EdgeInsets.only(bottom: 10.w), |
|||
child: Text( |
|||
text, |
|||
style: TextStyle( |
|||
fontSize: 15.sp, |
|||
color: Colors.white, |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
), |
|||
Row( |
|||
children: [ |
|||
if (onNext != null) |
|||
_button( |
|||
buttonText, |
|||
() { |
|||
params.onNext!(); |
|||
onNext(); |
|||
}, |
|||
), |
|||
if (onFinish != null) |
|||
_button( |
|||
buttonText, |
|||
() { |
|||
params.onFinish(); |
|||
onFinish(); |
|||
}, |
|||
), |
|||
], |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
static Widget _button(String text, Function() onTap) { |
|||
return GestureDetector( |
|||
onTap: onTap, |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(20.r), |
|||
border: Border.all(color: Colors.white), |
|||
), |
|||
child: Text( |
|||
text, |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: Colors.white, |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
// 新增的动画Widget |
|||
class _AnimatedLottieWidget extends StatefulWidget { |
|||
@override |
|||
_AnimatedLottieWidgetState createState() => _AnimatedLottieWidgetState(); |
|||
} |
|||
|
|||
class _AnimatedLottieWidgetState extends State<_AnimatedLottieWidget> |
|||
with SingleTickerProviderStateMixin { |
|||
late AnimationController _animationController; |
|||
late Animation<double> _scaleAnimation; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
|
|||
// 创建动画控制器,持续时间1.5秒 |
|||
_animationController = AnimationController( |
|||
duration: const Duration(milliseconds: 1000), |
|||
vsync: this, |
|||
); |
|||
|
|||
// 创建缩放动画,从0.8到1.2再回到1.0 |
|||
_scaleAnimation = Tween<double>( |
|||
begin: 0.8, |
|||
end: 1.2, |
|||
).animate(CurvedAnimation( |
|||
parent: _animationController, |
|||
curve: Curves.elasticInOut, |
|||
)); |
|||
|
|||
// 启动动画并设置为循环 |
|||
_animationController.repeat(reverse: true); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_animationController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return AnimatedBuilder( |
|||
animation: _scaleAnimation, |
|||
builder: (context, child) { |
|||
return Transform.scale( |
|||
scale: _scaleAnimation.value, |
|||
child: SizedBox( |
|||
width: 200.w, |
|||
height: 200.w, |
|||
child: Lottie.asset('assets/lottie/intro_foriday.json'), |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
import 'package:flutter/material.dart'; |
|||
|
|||
class SelectLocationMap extends StatefulWidget { |
|||
const SelectLocationMap({super.key}); |
|||
|
|||
@override |
|||
State<SelectLocationMap> createState() => _SelectLocationMapState(); |
|||
} |
|||
|
|||
class _SelectLocationMapState extends State<SelectLocationMap> { |
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return const Placeholder(); |
|||
} |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
import 'dart:async'; |
|||
|
|||
import 'package:ble_service/ble_service.dart'; |
|||
import 'package:flutter_intro/flutter_intro.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:get_storage/get_storage.dart'; |
|||
|
|||
class NoviceGuideService extends GetxService { |
|||
final GetStorage _storage = GetStorage(); |
|||
|
|||
final BleService _bleService = BleService(); |
|||
StreamSubscription<dynamic>? _statusSubscription; |
|||
|
|||
bool isHomeGuide = true; |
|||
bool isAIGuide = true; |
|||
|
|||
RxBool isWakeup = false.obs; |
|||
|
|||
int aiStep = 0; |
|||
|
|||
List aiStepsList = [ |
|||
{ |
|||
'start': '对小言说:“今天天气怎么样”', |
|||
'input': '今天天气怎么样', |
|||
'send': '小言会为你实时为你播报天气', |
|||
'end': '小言能实时为你播报天气,试试其他指令吧', |
|||
}, |
|||
{ |
|||
'start': '对小言说:“请播放一组摇滚乐”', |
|||
'input': '请播放一组摇滚乐', |
|||
'send': '小言会为你搜寻到一组音乐', |
|||
'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧', |
|||
}, |
|||
{ |
|||
'start': '对小言说:“导航到深圳机场”', |
|||
'input': '导航到深圳机场', |
|||
'send': '小言会为你规划路线', |
|||
'end': '小言已成功为您导航到深圳机场,试试其他指令吧', |
|||
}, |
|||
{ |
|||
'start': '对小言说:“明天下午3点创建会议提醒”', |
|||
'input': '明天下午3点创建会议提醒', |
|||
'send': '小言会为你创建会议提醒', |
|||
'end': '小言已成功为您创建会议提醒,试试其他指令吧', |
|||
}, |
|||
{ |
|||
'start': '对小言说:“拨打妈妈的电话”', |
|||
'input': '拨打妈妈的电话', |
|||
'send': '小言会为你拨打电话', |
|||
'end': '小言已成功为您拨打电话,试试其他指令吧', |
|||
}, |
|||
{ |
|||
'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”', |
|||
'input': '给爸爸发短信说晚上回家吃饭’', |
|||
'send': '小言会为你发送短信', |
|||
'end': '短信已发送,语音操控更便捷', |
|||
}, |
|||
]; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
// isHomeGuide = _storage.read('home_guide') ?? false; |
|||
// isAIGuide = _storage.read('ai_guide') ?? false; |
|||
} |
|||
|
|||
@override |
|||
void onClose() { |
|||
closeWakeupListen(); |
|||
super.onClose(); |
|||
} |
|||
|
|||
void startHomeGuide() async { |
|||
if (!isHomeGuide) { |
|||
await Future.delayed(const Duration(milliseconds: 200)); |
|||
isHomeGuide = true; |
|||
_storage.write('home_guide', true); |
|||
Intro.of(Get.context!).start(group: 'home'); |
|||
} |
|||
} |
|||
|
|||
void startAIGuide() async { |
|||
if (!isAIGuide) { |
|||
await Future.delayed(const Duration(milliseconds: 200)); |
|||
isAIGuide = true; |
|||
_storage.write('ai_guide', true); |
|||
Intro.of(Get.context!).start(group: 'ai'); |
|||
} |
|||
} |
|||
|
|||
void startWakeupListen() { |
|||
_bleService.setWakeup(false); |
|||
_statusSubscription = _bleService.onStatusUpdated.listen((event) { |
|||
if (event is Map && event['type'] == 'wakeup') { |
|||
isWakeup.value = true; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
void closeWakeupListen() { |
|||
_bleService.setWakeup(true); |
|||
_statusSubscription?.cancel(); |
|||
} |
|||
} |
|||
@ -1,218 +1,213 @@ |
|||
import 'dart:async'; |
|||
import 'dart:io'; |
|||
import '../../../data/models/appconfig.dart'; |
|||
import '../../../data/models/appconfig_model.dart'; |
|||
import 'package:app_tracking_transparency/app_tracking_transparency.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:get_storage/get_storage.dart'; |
|||
|
|||
import '../../../data/models/appconfig.dart'; |
|||
import '../../../data/models/appconfig_model.dart'; |
|||
import '../../../data/models/user_Info.dart'; |
|||
import '../../../routes/app_routes.dart'; |
|||
import '../../../core/themes/theme_service.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
import '../../../data/services/version_update_service.dart'; |
|||
import '../../../modules/common/widgets/version_update_dialog.dart'; |
|||
import '../../../modules/common/widgets/privacy_policy_dialog.dart'; |
|||
import '../../common/webview_page.dart'; |
|||
import '../../../core/utils/logger.dart'; |
|||
|
|||
class SplashController extends GetxController { |
|||
final GetStorage _storage = GetStorage(); |
|||
static const String _tag = 'SplashCtrl'; |
|||
|
|||
// 进度条动画控制 |
|||
final _loadingProgress = 0.0.obs; |
|||
double get loadingProgress => _loadingProgress.value; |
|||
|
|||
// 资源加载状态 |
|||
final _isResourceLoaded = false.obs; |
|||
bool get isResourceLoaded => _isResourceLoaded.value; |
|||
|
|||
final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策 |
|||
|
|||
// 版本号 |
|||
//final String version = 'V1.0.0'; |
|||
|
|||
// 主题服务 |
|||
final ThemeService _themeService = ThemeService(); |
|||
// 使用一个反应式变量来决定是否可以导航,替代所有布尔标记 |
|||
final _canNavigate = false.obs; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
|
|||
// 首先检查隐私政策状态 |
|||
final hasAcceptedPrivacy = _storage.read('hasAcceptedPrivacy') ?? false; |
|||
WidgetsBinding.instance.addPostFrameCallback((_) { |
|||
if (!hasAcceptedPrivacy) { |
|||
// 优先显示隐私政策弹窗,暂停其他初始化 |
|||
_showPrivacyDialog(); |
|||
} else { |
|||
// 已同意隐私政策,正常启动流程 |
|||
_startNormalFlow(); |
|||
} |
|||
}); |
|||
// 监听 _canNavigate 状态,一旦为 true,立即且仅执行一次导航 |
|||
once(_canNavigate, (_) => _navigateToTargetPage()); |
|||
// 启动整个初始化流程 |
|||
_initializeApp(); |
|||
} |
|||
|
|||
void _startNormalFlow() { |
|||
_startLoadingAnimation(); |
|||
_loadAppResources(); |
|||
} |
|||
/// 启动并协调所有初始化任务 |
|||
Future<void> _initializeApp() async { |
|||
// 步骤 1: 检查并等待隐私政策同意 |
|||
final hasAccepted = await _checkAndRequestPrivacy(); |
|||
if (!hasAccepted) { |
|||
// 如果用户拒绝隐私政策,应用会退出,后续代码不执行 |
|||
return; |
|||
} |
|||
|
|||
void _onPrivacyAccepted() { |
|||
_storage.write('hasAcceptedPrivacy', true); |
|||
Get.back(); // 关闭弹窗 |
|||
_startNormalFlow(); // 开始正常流程 |
|||
} |
|||
// 步骤 2: 并行执行所有启动任务,并等待它们全部完成 |
|||
// Future.wait 会等待数组中所有的 Future 都完成后才继续 |
|||
await Future.wait([ |
|||
_startLoadingAnimation(), // 任务1: 最小动画时长 |
|||
_performParallelTasks(), // 任务2: 其他所有后台任务 |
|||
]); |
|||
|
|||
void _onPrivacyRejected() { |
|||
exit(0); // 退出应用 |
|||
// 步骤 3: 所有任务(动画和后台)都已完成,触发导航 |
|||
_canNavigate.value = true; |
|||
} |
|||
|
|||
// 显示隐私政策弹窗 |
|||
void _showPrivacyDialog() { |
|||
PrivacyPolicyDialog.show( |
|||
onAccept: () { |
|||
_onPrivacyAccepted(); |
|||
_hasAcceptedPrivacy.value = true; |
|||
}, |
|||
onReject: _onPrivacyRejected, |
|||
canDismiss: false, // 禁止通过返回键关闭弹窗 |
|||
); |
|||
/// 检查隐私政策,如果未同意,则显示弹窗并等待用户操作 |
|||
/// 返回用户是否同意 |
|||
Future<bool> _checkAndRequestPrivacy() async { |
|||
if (_storage.read<bool>('hasAcceptedPrivacy') ?? false) { |
|||
return true; |
|||
} |
|||
|
|||
final completer = Completer<bool>(); |
|||
WidgetsBinding.instance.addPostFrameCallback((_) { |
|||
PrivacyPolicyDialog.show( |
|||
onAccept: () { |
|||
_storage.write('hasAcceptedPrivacy', true); |
|||
Get.back(); // 关闭弹窗 |
|||
if (!completer.isCompleted) completer.complete(true); |
|||
}, |
|||
onReject: () { |
|||
if (!completer.isCompleted) completer.complete(false); |
|||
exit(0); // 退出应用 |
|||
}, |
|||
canDismiss: false, |
|||
); |
|||
}); |
|||
|
|||
return completer.future; |
|||
} |
|||
|
|||
// 初始化地图SDK |
|||
// void _initMapSDK() { |
|||
// try { |
|||
// AmapFlutter().initSdk( |
|||
// iosKey: "a4a1394fe817c2f86a424b897b4a9af4", |
|||
// androidKey: "78e7946dd602778df82c6ae143a88184", |
|||
// ); |
|||
// } catch (e) { |
|||
// print('地图SDK初始化失败: $e'); |
|||
// } |
|||
// } |
|||
|
|||
// 进度条动画(持续2秒) |
|||
void _startLoadingAnimation() { |
|||
const totalDuration = 1500; // 动画总时长增加到1.5秒 |
|||
const steps = totalDuration ~/ 30; // 30的间隔,更平滑的动画 |
|||
/// 动画任务:返回一个在动画结束时完成的 Future |
|||
Future<void> _startLoadingAnimation() { |
|||
const totalDuration = 1500; // 动画总时长1.5秒 |
|||
const steps = totalDuration ~/ 30; |
|||
const stepValue = 1.0 / steps; |
|||
final completer = Completer<void>(); |
|||
|
|||
Timer.periodic(const Duration(milliseconds: 30), (timer) { |
|||
if (_loadingProgress.value < 1.0) { |
|||
_loadingProgress.value += stepValue; |
|||
} else { |
|||
_loadingProgress.value = 1.0; |
|||
timer.cancel(); |
|||
_checkNavigateCondition(); |
|||
if (!completer.isCompleted) completer.complete(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// 异步加载应用资源 |
|||
Future<void> _loadAppResources() async { |
|||
try { |
|||
// 模拟资源加载(如API配置、本地缓存等) |
|||
await Future.delayed(const Duration(milliseconds: 1500)); |
|||
return completer.future; |
|||
} |
|||
|
|||
// 实际资源加载代码 |
|||
// await _loadImages(); |
|||
// await _initDatabase(); |
|||
/// 将所有并行的后台任务打包成一个 Future |
|||
Future<void> _performParallelTasks() async { |
|||
// 同样使用 Future.wait,确保此函数在所有子任务完成后才结束 |
|||
await Future.wait([ |
|||
_requestTrackingPermissionAsync(), |
|||
_validateTokenAsync(), |
|||
_performVersionCheckAsync(), |
|||
]); |
|||
Logger.d(_tag, '所有后台任务已完成'); |
|||
} |
|||
|
|||
// 检查版本更新(在后台进行,不阻塞启动流程) |
|||
//_checkVersionUpdate(); |
|||
/// 异步请求追踪权限 |
|||
Future<void> _requestTrackingPermissionAsync() async { |
|||
try { |
|||
await AppTrackingTransparency.requestTrackingAuthorization(); |
|||
Logger.d(_tag, '用户追踪权限请求完成'); |
|||
} catch (e) { |
|||
Logger.e(_tag, '用户追踪权限请求失败: $e'); |
|||
// 即使失败,也视为完成,不阻塞主流程 |
|||
} |
|||
} |
|||
|
|||
_isResourceLoaded.value = true; |
|||
_checkNavigateCondition(); |
|||
/// 异步验证Token和加载应用配置 |
|||
Future<void> _validateTokenAsync() async { |
|||
try { |
|||
final token = _storage.read('logintoken'); |
|||
if (token != null && token.isNotEmpty) { |
|||
final isLoggedIn = await getLoginToken(); |
|||
if (isLoggedIn) { |
|||
// 只有登录成功,才获取App配置 |
|||
await getAppConfig(); |
|||
} else { |
|||
await User.clearUserSession(); // |
|||
} |
|||
} |
|||
} catch (e) { |
|||
Get.snackbar('错误', '资源加载失败,请检查网络连接'); |
|||
_isResourceLoaded.value = true; // 允许用户继续使用 |
|||
print('资源加载错误: $e'); |
|||
Logger.e(_tag, 'Token验证或配置加载失败: $e'); |
|||
await User.clearUserSession(); |
|||
} |
|||
} |
|||
|
|||
// 检查跳转条件 |
|||
void _checkNavigateCondition() { |
|||
if (_loadingProgress.value >= 1.0 && _isResourceLoaded.value) { |
|||
_navigateToLogin(); |
|||
/// 异步执行版本检查 |
|||
Future<void> _performVersionCheckAsync() async { |
|||
try { |
|||
final versionService = VersionUpdateService.to; |
|||
final updateInfo = await versionService.checkForUpdate(); |
|||
|
|||
if (updateInfo.hasUpdate) { |
|||
Logger.i(_tag, '发现新版本: ${updateInfo.latestVersion}'); |
|||
final completer = Completer<void>(); |
|||
// 在主线程中显示更新对话框 |
|||
WidgetsBinding.instance.addPostFrameCallback((_) { |
|||
// 使用 whenComplete 确保无论弹窗如何关闭,Future 都会完成 |
|||
VersionUpdateDialog.show(updateInfo).whenComplete(() { |
|||
if (!completer.isCompleted) completer.complete(); |
|||
}); |
|||
}); |
|||
// 等待弹窗交互结束 |
|||
await completer.future; |
|||
} else { |
|||
Logger.i(_tag, '当前已是最新版本'); |
|||
} |
|||
} catch (e) { |
|||
Logger.e(_tag, '版本检查失败: $e'); |
|||
// 即使检查失败,也视为任务完成 |
|||
} |
|||
} |
|||
|
|||
// 获取登录Token并跳转 |
|||
Future<void> _navigateToLogin() async { |
|||
bool isLoggedIn = false; |
|||
/// 根据最终状态跳转到目标页面 |
|||
void _navigateToTargetPage() { |
|||
// 导航时直接检查最终的数据状态,而不是依赖中间变量 |
|||
final token = _storage.read('logintoken'); |
|||
print('getLoginToken:进来了${token}'); |
|||
if (token != null && token.isNotEmpty) { |
|||
isLoggedIn = await getLoginToken(); |
|||
} |
|||
if (isLoggedIn) { |
|||
await getAppConfig(); |
|||
final isAppConfigInitialized = AppConfig.isInitialized(); |
|||
// 检查登录状态和配置初始化状态 |
|||
if (token != null && token.isNotEmpty && isAppConfigInitialized) { |
|||
Get.offAllNamed(Routes.pairing); |
|||
} else { |
|||
Get.offAllNamed(Routes.login); // 跳转到登录页 |
|||
Get.offAllNamed(Routes.login); |
|||
} |
|||
} |
|||
|
|||
/// 异步获取登录Token |
|||
Future<bool> getLoginToken() async { |
|||
try { |
|||
print('getLoginToken:进来了'); |
|||
//if (User.getname() != "") { |
|||
final response = await Api.tokenlogin(); |
|||
print('getLoginToken: $response'); |
|||
if (response == null) return false; |
|||
|
|||
if (response['user'] != '') { |
|||
print('getLoginToken:进来了'); |
|||
// 根据API设计调整 |
|||
if (response?['user'] != null && response['user'] != '') { |
|||
final user = User.fromJson(response); |
|||
|
|||
// 存储用户信息(示例) |
|||
await _storage.write('user_info', user.toJson()); |
|||
return true; |
|||
} |
|||
//return true; |
|||
//} |
|||
return false; |
|||
} catch (e) { |
|||
print('Token获取失败: $e'); |
|||
Logger.e(_tag, 'Token获取失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
Future<bool> getAppConfig() async { |
|||
try { |
|||
print('getAppConfig:进来了'); |
|||
//if (User.getname() != "") { |
|||
final response = await Api.getappconfig(); |
|||
print('getAppConfig: $response'); |
|||
if (response == null) return false; |
|||
// 根据API设计调整 |
|||
final resp = UserGetAppConfigResp.fromJson(response); |
|||
print('getAppConfig:进来了'); |
|||
// 存储用户信息(示例) |
|||
// _storage.write('ENV', resp.env); |
|||
// _storage.write('AGENTS', resp.agents); |
|||
// _storage.write('MCPS', resp.mcps); |
|||
AppConfig.initialize(resp); |
|||
return true; |
|||
} catch (e) { |
|||
print('getAppConfig 取失败: $e'); |
|||
Logger.e(_tag, 'getAppConfig 获取失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 检查版本更新(后台执行,不阻塞启动流程) |
|||
// void _checkVersionUpdate() { |
|||
// // 延迟执行版本检查,避免影响启动速度 |
|||
// Timer(const Duration(seconds: 1), () async { |
|||
// try { |
|||
// final versionService = VersionUpdateService.to; |
|||
// final updateInfo = await versionService.checkForUpdate(); |
|||
|
|||
// if (updateInfo.hasUpdate) { |
|||
// // 显示更新对话框 |
|||
// await VersionUpdateDialog.show(updateInfo); |
|||
// } |
|||
// } catch (e) { |
|||
// print('版本检查失败: $e'); |
|||
// // 版本检查失败不影响用户使用,静默处理 |
|||
// } |
|||
// }); |
|||
// } |
|||
} |
|||
|
|||
Binary file not shown.
@ -0,0 +1,214 @@ |
|||
import Foundation |
|||
import NaturalLanguage |
|||
|
|||
/** |
|||
* 语言检测工具类 |
|||
* 使用iOS自带的自然语言处理框架检测文本语种 |
|||
*/ |
|||
public class LanguageDetector { |
|||
|
|||
/** |
|||
* 检测文本的主要语言 |
|||
* |
|||
* @param text 需要检测的文本 |
|||
* @return 语言代码(如:"zh-Hans", "en", "ja"等),如果检测失败返回nil |
|||
*/ |
|||
public static func detectLanguage(from text: String) -> String? { |
|||
// 检查输入文本是否为空 |
|||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { |
|||
return nil |
|||
} |
|||
|
|||
// 使用NLLanguageRecognizer进行语言检测 |
|||
let recognizer = NLLanguageRecognizer() |
|||
recognizer.processString(text) |
|||
|
|||
// 获取最可能的语言 |
|||
guard let dominantLanguage = recognizer.dominantLanguage else { |
|||
return nil |
|||
} |
|||
|
|||
return dominantLanguage.rawValue |
|||
} |
|||
|
|||
/** |
|||
* 检测文本的语言并返回置信度 |
|||
* |
|||
* @param text 需要检测的文本 |
|||
* @return 语言代码和置信度的字典,按置信度降序排列 |
|||
*/ |
|||
public static func detectLanguageWithConfidence(from text: String) -> [(language: String, confidence: Double)] { |
|||
// 检查输入文本是否为空 |
|||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { |
|||
return [] |
|||
} |
|||
|
|||
let recognizer = NLLanguageRecognizer() |
|||
recognizer.processString(text) |
|||
|
|||
// 获取语言假设及其置信度 |
|||
let languageHypotheses = recognizer.languageHypotheses(withMaximum: 5) |
|||
|
|||
// 转换为元组数组并按置信度排序 |
|||
return languageHypotheses.map { (language: $0.key.rawValue, confidence: $0.value) } |
|||
.sorted { $0.confidence > $1.confidence } |
|||
} |
|||
|
|||
/** |
|||
* 将语言代码转换为人类可读的语言名称 |
|||
* |
|||
* @param languageCode 语言代码(如:"zh-Hans", "en"等) |
|||
* @return 语言的本地化名称 |
|||
*/ |
|||
public static func getLanguageName(from languageCode: String) -> String { |
|||
let locale = Locale.current |
|||
return locale.localizedString(forLanguageCode: languageCode) ?? languageCode |
|||
} |
|||
|
|||
/** |
|||
* 检测文本语言并返回Azure语音服务支持的语言代码格式 |
|||
* |
|||
* @param text 需要检测的文本 |
|||
* @return Azure支持的语言代码格式(如:"zh-CN", "en-US"等) |
|||
*/ |
|||
public static func detectLanguageForAzure(from text: String) -> String? { |
|||
guard let detectedLanguage = detectLanguage(from: text) else { |
|||
return nil |
|||
} |
|||
|
|||
// 将iOS语言代码转换为Azure语音服务支持的格式 |
|||
return convertToAzureLanguageCode(detectedLanguage) |
|||
} |
|||
|
|||
/** |
|||
* 将iOS语言代码转换为Azure语音服务支持的语言代码 |
|||
* |
|||
* @param iosLanguageCode iOS检测到的语言代码 |
|||
* @return Azure支持的语言代码 |
|||
*/ |
|||
public static func convertToAzureLanguageCode(_ iosLanguageCode: String) -> String { |
|||
let languageMapping: [String: String] = [ |
|||
// 中文 |
|||
"zh-Hans": "zh-CN", // 简体中文 |
|||
"zh-Hant": "zh-TW", // 繁体中文 |
|||
"zh-HK": "zh-HK", // 香港中文 |
|||
|
|||
// 英语系 |
|||
"en": "en-US", // 英语(美国) |
|||
"en-GB": "en-US", // 英语(英国) |
|||
"en-AU": "en-US", // 英语(澳大利亚) |
|||
"en-CA": "en-US", // 英语(加拿大) |
|||
"en-IN": "en-US", // 英语(印度) |
|||
"en-IE": "en-US", // 英语(爱尔兰) |
|||
"en-NZ": "en-US", // 英语(新西兰) |
|||
"en-ZA": "en-US", // 英语(南非) |
|||
|
|||
// 亚洲语言 |
|||
"ja": "ja-JP", // 日语 |
|||
"ko": "ko-KR", // 韩语 |
|||
"hi": "hi-IN", // 印地语 |
|||
"th": "th-TH", // 泰语 |
|||
"vi": "vi-VN", // 越南语 |
|||
"id": "id-ID", // 印尼语 |
|||
"ms": "ms-MY", // 马来语 |
|||
"tl": "fil-PH", // 菲律宾语 |
|||
"my": "my-MM", // 缅甸语 |
|||
"km": "km-KH", // 高棉语(柬埔寨) |
|||
"lo": "lo-LA", // 老挝语 |
|||
"si": "si-LK", // 僧伽罗语(斯里兰卡) |
|||
"ta": "ta-IN", // 泰米尔语 |
|||
"te": "te-IN", // 泰卢固语 |
|||
"bn": "bn-IN", // 孟加拉语 |
|||
"gu": "gu-IN", // 古吉拉特语 |
|||
"kn": "kn-IN", // 卡纳达语 |
|||
"ml": "ml-IN", // 马拉雅拉姆语 |
|||
"mr": "mr-IN", // 马拉地语 |
|||
"pa": "pa-IN", // 旁遮普语 |
|||
"ur": "ur-PK", // 乌尔都语 |
|||
|
|||
// 欧洲语言 |
|||
"fr": "fr-FR", // 法语(法国) |
|||
"fr-CA": "fr-CA", // 法语(加拿大) |
|||
"fr-BE": "fr-BE", // 法语(比利时) |
|||
"fr-CH": "fr-CH", // 法语(瑞士) |
|||
"de": "de-DE", // 德语(德国) |
|||
"de-AT": "de-AT", // 德语(奥地利) |
|||
"de-CH": "de-CH", // 德语(瑞士) |
|||
"es": "es-ES", // 西班牙语(西班牙) |
|||
"es-MX": "es-MX", // 西班牙语(墨西哥) |
|||
"es-AR": "es-AR", // 西班牙语(阿根廷) |
|||
"es-CO": "es-CO", // 西班牙语(哥伦比亚) |
|||
"es-CL": "es-CL", // 西班牙语(智利) |
|||
"es-PE": "es-PE", // 西班牙语(秘鲁) |
|||
"es-VE": "es-VE", // 西班牙语(委内瑞拉) |
|||
"it": "it-IT", // 意大利语 |
|||
"pt": "pt-BR", // 葡萄牙语(巴西) |
|||
"pt-PT": "pt-PT", // 葡萄牙语(葡萄牙) |
|||
"ru": "ru-RU", // 俄语 |
|||
"nl": "nl-NL", // 荷兰语 |
|||
"nl-BE": "nl-BE", // 荷兰语(比利时) |
|||
"sv": "sv-SE", // 瑞典语 |
|||
"da": "da-DK", // 丹麦语 |
|||
"no": "nb-NO", // 挪威语 |
|||
"fi": "fi-FI", // 芬兰语 |
|||
"is": "is-IS", // 冰岛语 |
|||
"pl": "pl-PL", // 波兰语 |
|||
"cs": "cs-CZ", // 捷克语 |
|||
"sk": "sk-SK", // 斯洛伐克语 |
|||
"hu": "hu-HU", // 匈牙利语 |
|||
"ro": "ro-RO", // 罗马尼亚语 |
|||
"bg": "bg-BG", // 保加利亚语 |
|||
"hr": "hr-HR", // 克罗地亚语 |
|||
"sr": "sr-RS", // 塞尔维亚语 |
|||
"sl": "sl-SI", // 斯洛文尼亚语 |
|||
"et": "et-EE", // 爱沙尼亚语 |
|||
"lv": "lv-LV", // 拉脱维亚语 |
|||
"lt": "lt-LT", // 立陶宛语 |
|||
"uk": "uk-UA", // 乌克兰语 |
|||
"be": "be-BY", // 白俄罗斯语 |
|||
"mk": "mk-MK", // 马其顿语 |
|||
"sq": "sq-AL", // 阿尔巴尼亚语 |
|||
"mt": "mt-MT", // 马耳他语 |
|||
"ga": "ga-IE", // 爱尔兰语 |
|||
"cy": "cy-GB", // 威尔士语 |
|||
"eu": "eu-ES", // 巴斯克语 |
|||
"ca": "ca-ES", // 加泰罗尼亚语 |
|||
"gl": "gl-ES", // 加利西亚语 |
|||
|
|||
// 中东和非洲语言 |
|||
"ar": "ar-SA", // 阿拉伯语(沙特阿拉伯) |
|||
"ar-EG": "ar-EG", // 阿拉伯语(埃及) |
|||
"ar-AE": "ar-AE", // 阿拉伯语(阿联酋) |
|||
"ar-BH": "ar-BH", // 阿拉伯语(巴林) |
|||
"ar-DZ": "ar-DZ", // 阿拉伯语(阿尔及利亚) |
|||
"ar-IQ": "ar-IQ", // 阿拉伯语(伊拉克) |
|||
"ar-JO": "ar-JO", // 阿拉伯语(约旦) |
|||
"ar-KW": "ar-KW", // 阿拉伯语(科威特) |
|||
"ar-LB": "ar-LB", // 阿拉伯语(黎巴嫩) |
|||
"ar-LY": "ar-LY", // 阿拉伯语(利比亚) |
|||
"ar-MA": "ar-MA", // 阿拉伯语(摩洛哥) |
|||
"ar-OM": "ar-OM", // 阿拉伯语(阿曼) |
|||
"ar-QA": "ar-QA", // 阿拉伯语(卡塔尔) |
|||
"ar-SY": "ar-SY", // 阿拉伯语(叙利亚) |
|||
"ar-TN": "ar-TN", // 阿拉伯语(突尼斯) |
|||
"ar-YE": "ar-YE", // 阿拉伯语(也门) |
|||
"he": "he-IL", // 希伯来语 |
|||
"fa": "fa-IR", // 波斯语(伊朗) |
|||
"tr": "tr-TR", // 土耳其语 |
|||
"az": "az-AZ", // 阿塞拜疆语 |
|||
"ka": "ka-GE", // 格鲁吉亚语 |
|||
"hy": "hy-AM", // 亚美尼亚语 |
|||
"sw": "sw-KE", // 斯瓦希里语 |
|||
"am": "am-ET", // 阿姆哈拉语(埃塞俄比亚) |
|||
"zu": "zu-ZA", // 祖鲁语(南非) |
|||
"af": "af-ZA", // 南非荷兰语 |
|||
|
|||
// 其他语言 |
|||
"el": "el-GR", // 希腊语 |
|||
"eo": "eo", // 世界语 |
|||
"la": "la", // 拉丁语 |
|||
] |
|||
|
|||
return languageMapping[iosLanguageCode] ?? iosLanguageCode |
|||
} |
|||
} |
|||
Loading…
Reference in new issue