Browse Source

加入面对面翻译功能,优化ui

weicu
fdp 1 year ago
parent
commit
c15894fb60
  1. 6
      lib/data/services/speech_impl/azure_tts_service.dart
  2. 2
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  3. 4
      lib/data/services/tts_service.dart
  4. 62
      lib/modules/explore/views/explore_view.dart
  5. 1057
      lib/modules/meeting/views/meeting_view.dart
  6. 61
      lib/modules/translation/controllers/translation_controller.dart
  7. 20
      lib/modules/translation/views/translation_list_view.dart
  8. 8
      lib/modules/translation/views/translation_view.dart
  9. 13
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  10. 12
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  11. 1
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

6
lib/data/services/speech_impl/azure_tts_service.dart

@ -415,10 +415,12 @@ class AzureTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> setAudioOutputDevice() async { Future<bool> setAudioOutputDevice(int type) async {
try { try {
// 开始合成,传递参数 // 开始合成,传递参数
final result = await _channel.invokeMethod('setAudioOutputDevice'); final result = await _channel.invokeMethod('setAudioOutputDevice', {
'type': type,
});
return result; return result;
} catch (e) { } catch (e) {
Logger.error('音频输出设备类型失败: ${e.toString()}'); Logger.error('音频输出设备类型失败: ${e.toString()}');

2
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -392,7 +392,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
} }
@override @override
Future<bool> setAudioOutputDevice() { Future<bool> setAudioOutputDevice(int type) {
// TODO: implement setAudioOutputDevice // TODO: implement setAudioOutputDevice
throw UnimplementedError(); throw UnimplementedError();
} }

4
lib/data/services/tts_service.dart

@ -33,7 +33,9 @@ abstract class TtsService {
/// 释放资源 /// 释放资源
Future<void> dispose(); Future<void> dispose();
Future<bool> setAudioOutputDevice();
/// 切换音频输出设备
Future<bool> setAudioOutputDevice(int type);
} }
/// TTS事件类型 /// TTS事件类型

62
lib/modules/explore/views/explore_view.dart

@ -126,37 +126,37 @@ class ExploreView extends GetView<ExploreController> {
], ],
), ),
// 搜索框 // // 搜索框
SizedBox(height: 16.h), // SizedBox(height: 16.h),
Container( // Container(
decoration: BoxDecoration( // decoration: BoxDecoration(
color: // color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], // isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r), // borderRadius: BorderRadius.circular(12.r),
), // ),
child: TextField( // child: TextField(
controller: controller.searchController, // controller: controller.searchController,
decoration: InputDecoration( // decoration: InputDecoration(
hintText: 'searchAi'.tr, // 搜索AI功能... // hintText: 'searchAi'.tr, // 搜索AI功能...
hintStyle: TextStyle( // hintStyle: TextStyle(
fontSize: 14.sp, // fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[400], // color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
), // ),
prefixIcon: Icon( // prefixIcon: Icon(
Icons.search, // Icons.search,
size: 20.sp, // size: 20.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[400], // color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
), // ),
border: InputBorder.none, // border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 12.h), // contentPadding: EdgeInsets.symmetric(vertical: 12.h),
), // ),
textInputAction: TextInputAction.search, // textInputAction: TextInputAction.search,
onSubmitted: (_) => controller.search(), // onSubmitted: (_) => controller.search(),
style: TextStyle( // style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, // color: isDarkMode ? Colors.white : Colors.black,
), // ),
), // ),
), // ),
], ],
), ),
); );

1057
lib/modules/meeting/views/meeting_view.dart

File diff suppressed because it is too large

61
lib/modules/translation/controllers/translation_controller.dart

@ -17,6 +17,13 @@ import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart'; import '../../../data/services/asr_service.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
// 切换音频输出设备的枚举
enum AudioOutputDevice {
defaultDevice, // 默认(如有耳机优先耳机,否则扬声器)
headphones, // 强制耳机
speaker, // 强制扬声器
}
class TranslationController extends GetxController { class TranslationController extends GetxController {
// 服务 // 服务
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
@ -55,6 +62,7 @@ class TranslationController extends GetxController {
final appDocDir = getApplicationDocumentsDirectory(); final appDocDir = getApplicationDocumentsDirectory();
var appDir; var appDir;
var dir; var dir;
Future<void> changeTranslationMode(String mode) async { Future<void> changeTranslationMode(String mode) async {
currentMode.value = mode; currentMode.value = mode;
currentModeTitle.value = _getModeTitle(mode); currentModeTitle.value = _getModeTitle(mode);
@ -104,7 +112,7 @@ class TranslationController extends GetxController {
//_setupEventListeners(); //_setupEventListeners();
// 加载上次保存的语言设置 // 加载上次保存的语言设置
_loadSavedLanguages(); _loadSavedLanguages();
_requestStoragePermission();
// 初始化语言代码 // 初始化语言代码
final sourceAsrCode = final sourceAsrCode =
_languageManager.getAsrCodeByChineseName(sourceLanguage.value); _languageManager.getAsrCodeByChineseName(sourceLanguage.value);
@ -175,25 +183,27 @@ class TranslationController extends GetxController {
} }
// 请求存储权限 // 请求存储权限
void _requestStoragePermission() async { Future<bool> _requestStoragePermission() async {
// final BuildContext context = Get.overlayContext!; // final BuildContext context = Get.overlayContext!;
try {
PermissionUtil.instance final bool granted = await PermissionUtil.instance.requestPermission(
.requestPermission(
// context: context,
permissionType: Permission.storage, permissionType: Permission.storage,
permissionName: "存储", permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
) );
.then((granted) {
if (granted) { if (granted) {
Logger.d("Permission", "用户授予了存储权限"); Logger.d("Permission", "用户授予了存储权限");
return true;
} else { } else {
Logger.d("Permission", "用户未授予存储权限"); Logger.d("Permission", "用户未授予存储权限");
// 可以提示用户无法进行文件操作 return false;
}
} catch (e) {
Logger.e("Permission", "请求存储权限时出错: $e");
return false;
} }
});
} }
// /// 设置事件监听器 // /// 设置事件监听器
// void _setupEventListeners() { // void _setupEventListeners() {
@ -224,6 +234,11 @@ class TranslationController extends GetxController {
await _asrService.initialize( await _asrService.initialize(
supportedLanguages: asrSupportedLanguages, supportedLanguages: asrSupportedLanguages,
); );
final bool storage = await _requestStoragePermission();
if (!storage) {
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
return;
}
appDir = await appDocDir; // 先 await 获取目录 appDir = await appDocDir; // 先 await 获取目录
dir = Directory("${appDir.path}/TranslatAudio"); dir = Directory("${appDir.path}/TranslatAudio");
if (!await dir.exists()) { if (!await dir.exists()) {
@ -266,10 +281,10 @@ class TranslationController extends GetxController {
Future<void> startRecognition() async { Future<void> startRecognition() async {
Logger.info('开始语音识别'); Logger.info('开始语音识别');
// 等待权限请求完成并获取结果 // 等待权限请求完成并获取结果
final bool granted = await _requestRecordPermission(); final bool record = await _requestRecordPermission();
// 如果权限被拒绝,直接返回 // 如果权限被拒绝,直接返回
if (!granted) { if (!record) {
Logger.d("Permission", "录音权限被拒绝,无法开始录音"); Logger.d("Permission", "录音权限被拒绝,无法开始录音");
return; return;
} }
@ -328,6 +343,11 @@ class TranslationController extends GetxController {
await _asrService.startContinuousRecognition(_audioSourceType); await _asrService.startContinuousRecognition(_audioSourceType);
isRecognizing.value = true; isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) { if (isRecording.value && isRecognizing.value) {
final bool storage = await _requestStoragePermission();
if (!storage) {
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
return;
}
final formattedTime = final formattedTime =
DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
await _asrService.enableRecord( await _asrService.enableRecord(
@ -516,6 +536,20 @@ class TranslationController extends GetxController {
detectedSourceLanguageCode; detectedSourceLanguageCode;
translationHistory[index].targetLanguageCode = translationHistory[index].targetLanguageCode =
detectedTargetLanguageCode; detectedTargetLanguageCode;
print("判断是否是面对面模式且语言一致: ${detectedSourceLanguageCode}");
// 判断是否是面对面模式且语言一致
final bool isFaceToFaceSameLang =
detectedSourceLanguageCode == sourceLanguageCode.value &&
currentMode.value == "faceToFace";
// 设置音频输出设备
var isSuccess = await _ttsService.setAudioOutputDevice(
isFaceToFaceSameLang
? AudioOutputDevice.speaker.index
: AudioOutputDevice.headphones.index,
);
if (isSuccess) {
print('设置音频输出设备成功');
translationHistory.refresh(); translationHistory.refresh();
saveTranslationHistory(); saveTranslationHistory();
// 更新后滚动到底部 // 更新后滚动到底部
@ -526,6 +560,9 @@ class TranslationController extends GetxController {
// Logger.info('播放翻译文本: ${translationResult}'); // Logger.info('播放翻译文本: ${translationResult}');
await playTranslatedText(translationResult); await playTranslatedText(translationResult);
} }
} else {
print('设置音频输出设备失败');
}
} else { } else {
Logger.info( Logger.info(
'未找到匹配的历史项 ${sourceText} ${timestamp.toIso8601String() ?? "无时间戳"}'); '未找到匹配的历史项 ${sourceText} ${timestamp.toIso8601String() ?? "无时间戳"}');

20
lib/modules/translation/views/translation_list_view.dart

@ -278,15 +278,16 @@ class TranslationListView extends GetView<TranslationListController> {
), ),
_popupMenuEntry( _popupMenuEntry(
Icon( Icon(
Icons.delete_outline, Icons.smart_button,
size: 20.w, size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red, color: isDarkMode ? Colors.white : Colors.black87,
), ),
'删除', '智能转写',
isDarkMode ? Colors.red[700]! : Colors.red, isDarkMode ? Colors.white : Colors.black87,
() { () {
file.delete(); Get.toNamed(Routes.meeting, arguments: {
controller.loadTranslatAudio(); // 刷新列表 'filePath': file.path,
});
}, },
), ),
_popupMenuEntry( _popupMenuEntry(
@ -295,12 +296,11 @@ class TranslationListView extends GetView<TranslationListController> {
size: 20.w, size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red, color: isDarkMode ? Colors.red[700] : Colors.red,
), ),
'导入', '删除',
isDarkMode ? Colors.red[700]! : Colors.red, isDarkMode ? Colors.red[700]! : Colors.red,
() { () {
Get.toNamed(Routes.meeting, arguments: { file.delete();
'filePath': file.path, controller.loadTranslatAudio(); // 刷新列表
});
}, },
), ),
], ],

8
lib/modules/translation/views/translation_view.dart

@ -110,7 +110,9 @@ class TranslationView extends GetView<TranslationController> {
// TTS toggle button // TTS toggle button
Obx(() => IconButton( Obx(() => IconButton(
icon: Icon( icon: Icon(
controller.isRecording.value ? Icons.mic : Icons.mic_off, controller.isRecording.value
? Icons.mic
: Icons.fiber_manual_record,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp, size: 24.sp,
), ),
@ -122,7 +124,7 @@ class TranslationView extends GetView<TranslationController> {
// 历史记录按钮 // 历史记录按钮
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.history, Icons.folder,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp, size: 24.sp,
), ),
@ -134,7 +136,7 @@ class TranslationView extends GetView<TranslationController> {
// 返回时不自动开始翻译,只需重新设置滚动监听器 // 返回时不自动开始翻译,只需重新设置滚动监听器
controller.setupScrollListener(); controller.setupScrollListener();
}, },
tooltip: '查看历史记录', tooltip: '查看录音文件',
), ),
], ],
bottom: PreferredSize( bottom: PreferredSize(

13
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -147,13 +147,14 @@ class AzureAsrHelper(private val context: Context) {
} }
// 录音文件类 // 录音文件类
recordfile = RecordFile; recordfile = RecordFile;
//进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请
// 创建识别器 // 创建识别器
val setupSuccess = setupRecognizer() // val setupSuccess = setupRecognizer()
if (setupSuccess) { // if (setupSuccess) {
// 优化:初始化完成后进行预热 // // 优化:初始化完成后进行预热
warmupRecognizer() // warmupRecognizer()
} // }
return setupSuccess return true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "初始化失败: ${e.message}") Log.e(tag, "初始化失败: ${e.message}")
return false return false

12
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -502,7 +502,19 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
result.success(true) result.success(true)
} }
"setAudioOutputDevice" -> { "setAudioOutputDevice" -> {
val type = call.argument<Int>("type")
if (type == 0) {
// 默认(如果有耳机选耳机,否则使用系统扬声器)
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT)
} else if (type == 1) {
// 强制使用耳机
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES)
} else if (type == 2) {
// 强制使用扬声器
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER)
}
result.success(true) result.success(true)
} }

1
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -213,6 +213,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
override fun setAudioOutputDevice(device: AudioOutputDevice) { override fun setAudioOutputDevice(device: AudioOutputDevice) {
// Azure TTS使用系统默认的音频路由 // Azure TTS使用系统默认的音频路由
// 音频输出设备的控制需要通过Android的AudioManager实现 // 音频输出设备的控制需要通过Android的AudioManager实现
FileLogger.e(TAG, "音频输出设备类型device=: ${device}")
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
audioManager?.let { manager -> audioManager?.let { manager ->
when (device) { when (device) {

Loading…
Cancel
Save