Browse Source

summary

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
4a2d5dfba3
  1. 2
      android/app/build.gradle.kts
  2. 376
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  3. 526
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoAsrHelper.kt
  4. 438
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt
  5. 8
      lib/core/routes/app_pages.dart
  6. 101
      lib/data/providers/agent_provider.dart
  7. 11
      lib/data/services/azure_asr_service.dart
  8. 2
      lib/data/services/volcano_tts_api_service.dart
  9. 14
      lib/modules/translation/bindings/translation_binding.dart
  10. 9
      lib/modules/translation/bindings/translation_detail_binding.dart
  11. 11
      lib/modules/translation/bindings/translation_history_binding.dart
  12. 231
      lib/modules/translation/controllers/translation_controller.dart
  13. 125
      lib/modules/translation/controllers/translation_detail_controller.dart
  14. 129
      lib/modules/translation/controllers/translation_history_controller.dart
  15. 491
      lib/modules/translation/views/translation_detail_view.dart
  16. 244
      lib/modules/translation/views/translation_history_view.dart
  17. 229
      lib/modules/translation/views/translation_view.dart
  18. 4
      linux/flutter/generated_plugin_registrant.cc
  19. 1
      linux/flutter/generated_plugins.cmake
  20. 1
      pubspec.yaml
  21. 6
      windows/flutter/generated_plugin_registrant.cc
  22. 2
      windows/flutter/generated_plugins.cmake

2
android/app/build.gradle.kts

@ -59,7 +59,7 @@ dependencies {
// implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8") // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")
// 添加火山语音识别SDK依赖 // 添加火山语音识别SDK依赖
implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") // implementation("com.bytedance.speechengine:speechengine_tob:0.0.5")
} }
flutter { flutter {

376
android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt

@ -13,31 +13,19 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
class MainActivity: AudioServiceActivity() { class MainActivity: AudioServiceActivity() {
private val SPEECH_RECOGNITION_CHANNEL = "com.example.deep_voice/volcano_asr"
private val SPEECH_RECOGNITION_EVENT_CHANNEL = "com.example.deep_voice/volcano_events"
private val VOLCANO_TTS_CHANNEL = "com.example.deep_voice/volcano_tts"
private val VOLCANO_TTS_EVENT_CHANNEL = "com.example.deep_voice/volcano_tts_events"
private val AZURE_ASR_CHANNEL = "com.example.deep_voice/azure_asr" private val AZURE_ASR_CHANNEL = "com.example.deep_voice/azure_asr"
private val AZURE_ASR_EVENT_CHANNEL = "com.example.deep_voice/azure_asr_events" private val AZURE_ASR_EVENT_CHANNEL = "com.example.deep_voice/azure_asr_events"
private val TAG = "MainActivity" private val TAG = "MainActivity"
private lateinit var speechHelper: VolcanoAsrHelper
private lateinit var volcanoTtsHelper: VolcanoTtsHelper
private lateinit var azureAsrHelper: AzureAsrHelper private lateinit var azureAsrHelper: AzureAsrHelper
private var eventSink: EventChannel.EventSink? = null
private var azureEventSink: EventChannel.EventSink? = null private var azureEventSink: EventChannel.EventSink? = null
private var ttsEventSink: EventChannel.EventSink? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Initialize speechHelper with context
speechHelper = VolcanoAsrHelper(applicationContext)
// Initialize volcanoTtsHelper with context
volcanoTtsHelper = VolcanoTtsHelper(applicationContext)
// 初始化 Azure ASR 助手 // 初始化 Azure ASR 助手
azureAsrHelper = AzureAsrHelper() azureAsrHelper = AzureAsrHelper()
} }
@ -45,327 +33,6 @@ class MainActivity: AudioServiceActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
// 设置语音识别方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SPEECH_RECOGNITION_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey")
val serviceRegion = call.argument<String>("serviceRegion")
val resourceId = call.argument<String>("resourceId")
val cluster = call.argument<String>("cluster") ?: "cn-beijing"
if (subscriptionKey == null || serviceRegion == null) {
result.error("INVALID_ARGUMENTS", "subscriptionKey and serviceRegion are required", null)
return@setMethodCallHandler
}
try {
Log.d(TAG, "初始化语音识别服务,APP_ID: ${subscriptionKey.take(3)}***,APP_KEY: ${serviceRegion.take(5)}***")
Log.d(TAG, "APP_ID长度: ${subscriptionKey.length}, APP_KEY长度: ${serviceRegion.length}")
Log.d(TAG, "使用大模型流式识别SDK配置")
Log.d(TAG, "资源ID: ${resourceId ?: subscriptionKey}")
Log.d(TAG, "集群区域: $cluster")
val success = speechHelper.initialize(subscriptionKey, serviceRegion, cluster, resourceId)
Log.d(TAG, "语音识别服务初始化${if (success) "成功" else "失败"}")
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "初始化语音识别服务失败", e)
result.error("INITIALIZATION_ERROR", e.message, null)
}
}
"recognizeOnce" -> {
try {
speechHelper.recognizeOnce(object : VolcanoAsrHelper.RecognizeCallback {
override fun onResult(text: String) {
result.success(text)
}
override fun onError(error: String) {
result.error("RECOGNITION_ERROR", error, null)
}
})
} catch (e: Exception) {
result.error("RECOGNITION_ERROR", e.message, null)
}
}
"startContinuousRecognition" -> {
try {
if (speechHelper.isContinuousRecognitionActive()) {
result.error("ALREADY_ACTIVE", "连续识别已经在进行中", null)
return@setMethodCallHandler
}
val success = speechHelper.startContinuousRecognition(object : VolcanoAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String) {
sendEvent(mapOf(
"eventType" to "finalResult",
"text" to text
))
}
override fun onRecognizing(recognizing: String) {
sendEvent(mapOf(
"eventType" to "intermediateResult",
"text" to recognizing
))
}
override fun onSessionStarted() {
sendEvent(mapOf(
"eventType" to "sessionStarted"
))
}
override fun onSessionStopped() {
sendEvent(mapOf(
"eventType" to "sessionStopped"
))
}
override fun onCanceled(reason: String, errorDetails: String) {
sendEvent(mapOf(
"eventType" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
))
}
override fun onError(error: String) {
sendEvent(mapOf(
"eventType" to "error",
"error" to error
))
}
})
result.success(success)
} catch (e: Exception) {
result.error("RECOGNITION_ERROR", e.message, null)
}
}
"stopContinuousRecognition" -> {
try {
if (!speechHelper.isContinuousRecognitionActive()) {
result.error("NOT_ACTIVE", "连续识别未在进行中", null)
return@setMethodCallHandler
}
val success = speechHelper.stopContinuousRecognition(object : VolcanoAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String) {}
override fun onRecognizing(recognizing: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {
sendEvent(mapOf(
"eventType" to "sessionStopped"
))
}
override fun onCanceled(reason: String, errorDetails: String) {}
override fun onError(error: String) {
result.error("STOP_ERROR", error, null)
}
})
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", e.message, null)
}
}
"dispose" -> {
try {
speechHelper.dispose()
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
}
else -> {
result.notImplemented()
}
}
}
// 设置火山语音合成方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOLCANO_TTS_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val appId = call.argument<String>("appId")
val token = call.argument<String>("token")
val cluster = call.argument<String>("cluster")
if (appId == null || token == null || cluster == null) {
result.error("INVALID_ARGUMENTS", "appId, token and cluster are required", null)
return@setMethodCallHandler
}
try {
val success = volcanoTtsHelper.initialize(appId, token, cluster)
// 设置播放状态回调
volcanoTtsHelper.setPlaybackCallback(object : VolcanoTtsHelper.PlaybackCallback {
override fun onPlaybackStarted() {
sendTtsEvent(mapOf(
"eventType" to "playbackStarted"
))
}
override fun onPlaybackCompleted() {
sendTtsEvent(mapOf(
"eventType" to "playbackCompleted"
))
}
override fun onPlaybackError(error: String) {
sendTtsEvent(mapOf(
"eventType" to "playbackError",
"error" to error
))
}
// 实现合成开始回调
override fun onSynthesisBegin() {
sendTtsEvent(mapOf(
"eventType" to "synthesisBegin"
))
}
// 实现合成结束回调
override fun onSynthesisEnd() {
sendTtsEvent(mapOf(
"eventType" to "synthesisEnd"
))
}
})
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
}
"synthesize" -> {
val text = call.argument<String>("text")
val voiceType = call.argument<String>("voiceType")
if (text == null || voiceType == null) {
result.error("INVALID_ARGUMENTS", "text and voiceType are required", null)
return@setMethodCallHandler
}
// Since synthesize method was removed, we'll use speakText instead
// and inform the caller that we no longer support direct audio data return
try {
val success = volcanoTtsHelper.speakText(text, voiceType)
if (success) {
result.success(ByteArray(0)) // Return empty byte array as placeholder
} else {
result.error("TTS_ERROR", "Failed to synthesize text", null)
}
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
}
"synthesizeSync" -> {
val text = call.argument<String>("text")
val voiceType = call.argument<String>("voiceType")
if (text == null || voiceType == null) {
result.error("INVALID_ARGUMENTS", "text and voiceType are required", null)
return@setMethodCallHandler
}
// Since synthesizeSync method was removed, we'll use speakText instead
// and inform the caller that we no longer support direct audio data return
try {
val success = volcanoTtsHelper.speakText(text, voiceType)
if (success) {
result.success(ByteArray(0)) // Return empty byte array as placeholder
} else {
result.error("TTS_ERROR", "Failed to synthesize text", null)
}
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
}
"speakText" -> {
val text = call.argument<String>("text")
val voiceType = call.argument<String>("voiceType")
if (text == null || voiceType == null) {
result.error("INVALID_ARGUMENTS", "text and voiceType are required", null)
return@setMethodCallHandler
}
try {
Log.d(TAG, "准备播放文本: $text")
// 先停止当前播放
try {
volcanoTtsHelper.stopSpeaking()
// 添加短暂延迟,确保引擎状态已重置
Thread.sleep(100)
} catch (e: Exception) {
Log.e(TAG, "停止当前播放失败: ${e.message}")
// 继续尝试新的播放
}
val success = volcanoTtsHelper.speakText(text, voiceType)
Log.d(TAG, if (success) "成功开始播放" else "播放失败")
result.success(success)
} catch (e: Exception) {
Log.e(TAG, "播放文本失败", e)
result.error("SPEAK_ERROR", e.message, null)
}
}
"stopSpeaking" -> {
try {
val success = volcanoTtsHelper.stopSpeaking()
result.success(success)
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
}
"setSpeed" -> {
val speed = call.argument<Double>("speed")
if (speed == null) {
result.error("INVALID_ARGUMENTS", "speed is required", null)
return@setMethodCallHandler
}
try {
val success = volcanoTtsHelper.setSpeed(speed.toFloat())
result.success(success)
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
}
"setVolume" -> {
val volume = call.argument<Double>("volume")
if (volume == null) {
result.error("INVALID_ARGUMENTS", "volume is required", null)
return@setMethodCallHandler
}
try {
val success = volcanoTtsHelper.setVolume(volume.toFloat())
result.success(success)
} catch (e: Exception) {
result.error("TTS_ERROR", e.message, null)
}
}
"dispose" -> {
try {
volcanoTtsHelper.dispose()
result.success(true)
} catch (e: Exception) {
result.error("DISPOSE_ERROR", e.message, null)
}
}
else -> {
result.notImplemented()
}
}
}
// 设置 Azure ASR 方法通道 // 设置 Azure ASR 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result -> MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result ->
@ -542,31 +209,7 @@ class MainActivity: AudioServiceActivity() {
} }
} }
// 设置语音识别事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, SPEECH_RECOGNITION_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
}
)
// 设置 TTS 事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, VOLCANO_TTS_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
}
override fun onCancel(arguments: Any?) {
ttsEventSink = null
}
}
)
// 设置 Azure ASR 事件通道 // 设置 Azure ASR 事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler( EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler(
@ -582,17 +225,6 @@ class MainActivity: AudioServiceActivity() {
) )
} }
private fun sendEvent(event: Map<String, Any?>) {
runOnUiThread {
eventSink?.success(event)
}
}
private fun sendTtsEvent(event: Map<String, Any?>) {
runOnUiThread {
ttsEventSink?.success(event)
}
}
private fun sendAzureEvent(event: Map<String, Any?>) { private fun sendAzureEvent(event: Map<String, Any?>) {
runOnUiThread { runOnUiThread {
@ -601,8 +233,6 @@ class MainActivity: AudioServiceActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
speechHelper.dispose()
volcanoTtsHelper.dispose()
azureAsrHelper.dispose() azureAsrHelper.dispose()
super.onDestroy() super.onDestroy()
} }

526
android/app/src/main/kotlin/com/example/deep_voice/VolcanoAsrHelper.kt

@ -1,526 +0,0 @@
package com.example.deep_voice
import android.content.Context
import android.util.Log
import com.bytedance.speech.speechengine.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator
import org.json.JSONException
import org.json.JSONObject
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* 火山语音识别Helper类
*
* 该类封装了火山语音SDK的语音识别功能,提供简单的接口供Flutter调用
*/
class VolcanoAsrHelper(context: Context) : SpeechEngine.SpeechListener {
private val TAG = "VolcanoAsrHelper"
private var mSpeechEngine: SpeechEngine? = null
private var mSpeechEngineHandler: Long = -1
private var isInitialized = false
private var applicationContext: Context? = context.applicationContext
// 存储应用ID和密钥,以便在错误处理中使用
private var appId: String = ""
private var appKey: String = ""
private var cluster: String = "" // 移除默认值
// 同步识别相关变量
private var mRecognitionLatch: CountDownLatch? = null
private var mRecognitionResult: String? = null
private var mRecognitionError: String? = null
// 当前回调
private var mCurrentCallback: RecognizeCallback? = null
private var mCurrentContinuousCallback: ContinuousRecognizeCallback? = null
// 是否正在进行连续识别
private var mIsContinuousRecognitionActive = false
/**
* 初始化语音识别引擎
* @param appId 应用ID
* @param token 应用密钥/令牌
* @param cluster 集群区域
* @param resourceId 资源ID,用于大模型流式识别API鉴权
* @return 初始化是否成功
*/
fun initialize(appId: String, token: String, cluster: String, resourceId: String? = null): Boolean {
try {
// 设置基本参数
this.appId = appId
this.appKey = token
this.cluster = cluster
Log.d(TAG, "初始化参数: APP_ID=${appId}, APP_KEY长度=${token.length}, 集群区域=${cluster}")
Log.d(TAG, "资源ID: ${resourceId ?: appId}")
// 确保应用上下文已设置
if (applicationContext == null) {
Log.e(TAG, "应用上下文未设置,请先调用setContext方法")
return false
}
// 准备环境
SpeechEngineGenerator.PrepareEnvironment(applicationContext, null)
// 获取语音引擎实例
mSpeechEngine = SpeechEngineGenerator.getInstance()
mSpeechEngineHandler = mSpeechEngine?.createEngine() ?: -1
if (mSpeechEngineHandler == -1L) {
Log.e(TAG, "创建语音引擎失败")
return false
}
// 设置上下文
mSpeechEngine?.setContext(applicationContext)
// 设置引擎类型为ASR
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING,
SpeechEngineDefines.ASR_ENGINE
)
// 设置日志级别
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING,
SpeechEngineDefines.LOG_LEVEL_DEBUG // 设置为DEBUG级别以获取更多日志信息
)
// 设置用户ID (必需)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_UID_STRING,
"deep_voice_user"
)
// 设置设备ID (可选)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING,
"deep_voice_device"
)
// 设置授权信息 - APP_ID
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING,
appId
)
// 使用大模型流式识别SDK (API v3)
Log.d(TAG, "使用大模型流式识别SDK配置 (API v3)")
// 设置API路径为大模型流式识别API
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_URI_STRING,
"/api/v3/sauc/bigmodel"
)
// 设置授权信息 - APP_TOKEN (不需要Bearer前缀)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING,
token
)
// 设置资源ID,用于大模型流式识别API鉴权
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING,
resourceId
)
// 设置协议类型为SEED
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT,
SpeechEngineDefines.PROTOCOL_TYPE_SEED
)
// 设置ASR请求参数
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_REQ_PARAMS_STRING,
"{\"force_to_speech_time\":0, \"end_window_size\":800}"
)
// 设置网络配置
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING,
"wss://openspeech.bytedance.com"
)
// 设置集群区域 - 这对于WebSocket连接非常重要
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_CLUSTER_STRING,
cluster
)
// 设置音频来源为内置录音机
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_RECORDER_TYPE_STRING,
SpeechEngineDefines.RECORDER_TYPE_RECORDER
)
// 启用音量获取
mSpeechEngine?.setOptionBoolean(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ENABLE_GET_VOLUME_BOOL,
true
)
// 设置最大录音时长为60秒
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_VAD_MAX_SPEECH_DURATION_INT,
60000
)
// 控制识别效果
mSpeechEngine?.setOptionBoolean(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_ENABLE_DDC_BOOL,
true
)
mSpeechEngine?.setOptionBoolean(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_SHOW_NLU_PUNC_BOOL,
true
)
// 设置识别结果形式为全量返回(适合一句话识别)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING,
SpeechEngineDefines.ASR_RESULT_TYPE_SINGLE
)
// 记录所有配置参数,便于调试
Log.d(TAG, "配置参数汇总:")
Log.d(TAG, "APP_ID: $appId")
Log.d(TAG, "APP_TOKEN: ${token.take(5)}***")
Log.d(TAG, "CLUSTER: $cluster")
Log.d(TAG, "RESOURCE_ID: ${resourceId ?: appId}")
Log.d(TAG, "API_PATH: ${if (resourceId != null) "/api/v3/sauc/bigmodel" else "/api/v2/asr"}")
// 初始化引擎
val ret = mSpeechEngine?.initEngine(mSpeechEngineHandler) ?: -1
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "初始化引擎失败: $ret")
return false
}
// 设置监听器
mSpeechEngine?.setListener(this)
isInitialized = true
Log.d(TAG, "火山语音识别引擎初始化成功")
return true
} catch (e: Exception) {
Log.e(TAG, "火山语音识别引擎初始化失败: ${e.message}")
e.printStackTrace()
return false
}
}
/**
* 设置应用上下文
*/
fun setContext(context: Context) {
applicationContext = context.applicationContext
}
/**
* 一次性识别
*
* @param callback 回调接口,用于返回结果或错误
*/
fun recognizeOnce(callback: RecognizeCallback) {
if (!isInitialized) {
callback.onError("语音识别引擎尚未初始化")
return
}
try {
Log.d(TAG, "开始一次性识别")
// 保存回调以便在onMessage中使用
mCurrentCallback = callback
// 设置识别结果形式为全量返回(适合一句话识别)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING,
SpeechEngineDefines.ASR_RESULT_TYPE_FULL
)
// 先停止引擎,避免SDK内部异步线程带来的问题
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "")
// 启动引擎
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
callback.onError("启动引擎失败: $ret")
}
} catch (e: Exception) {
Log.e(TAG, "语音识别异常: ${e.message}")
callback.onError("语音识别异常: ${e.message}")
}
}
/**
* 开始连续识别
*
* @param callback 回调接口,用于返回结果或错误
*/
fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (!isInitialized) {
callback.onError("语音识别引擎尚未初始化")
return false
}
if (mIsContinuousRecognitionActive) {
callback.onError("连续识别已经在进行中")
return false
}
try {
Log.d(TAG, "开始连续识别")
// 保存回调以便在onMessage中使用
mCurrentContinuousCallback = callback
// 设置识别结果形式为增量返回(适合连续识别)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ASR_RESULT_TYPE_STRING,
SpeechEngineDefines.ASR_RESULT_TYPE_SINGLE
)
// 先停止引擎,避免SDK内部异步线程带来的问题
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "")
// 启动引擎
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
callback.onError("启动引擎失败: $ret")
return false
}
mIsContinuousRecognitionActive = true
return true
} catch (e: Exception) {
Log.e(TAG, "开始连续识别异常: ${e.message}")
callback.onError("开始连续识别异常: ${e.message}")
return false
}
}
/**
* 停止连续识别
*
* @param callback 回调接口,用于返回结果或错误
*/
fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (!isInitialized) {
callback.onError("语音识别引擎尚未初始化")
return false
}
if (!mIsContinuousRecognitionActive) {
callback.onError("连续识别未在进行中")
return false
}
try {
Log.d(TAG, "停止连续识别")
// 音频输入完成
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_FINISH_TALKING, "")
// 停止引擎
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
callback.onError("停止引擎失败: $ret")
return false
}
mIsContinuousRecognitionActive = false
return true
} catch (e: Exception) {
Log.e(TAG, "停止连续识别异常: ${e.message}")
callback.onError("停止连续识别异常: ${e.message}")
return false
}
}
/**
* 检查连续识别是否活跃
*/
fun isContinuousRecognitionActive(): Boolean {
return mIsContinuousRecognitionActive
}
/**
* 释放资源
*/
fun dispose() {
try {
if (mSpeechEngineHandler != -1L) {
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
mSpeechEngine?.destroyEngine(mSpeechEngineHandler)
mSpeechEngineHandler = -1
}
mSpeechEngine = null
isInitialized = false
mIsContinuousRecognitionActive = false
Log.d(TAG, "火山语音识别引擎已释放")
} catch (e: Exception) {
Log.e(TAG, "释放火山语音识别引擎失败: ${e.message}")
}
}
// 实现SpeechListener接口
override fun onSpeechMessage(type: Int, data: ByteArray, len: Int) {
val stdData = String(data)
when (type) {
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> {
Log.d(TAG, "引擎启动成功: $stdData")
mCurrentContinuousCallback?.onSessionStarted()
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> {
Log.d(TAG, "引擎已停止: $stdData")
mIsContinuousRecognitionActive = false
mCurrentContinuousCallback?.onSessionStopped()
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> {
Log.e(TAG, "引擎错误: $stdData")
try {
val errorJson = JSONObject(stdData)
val errorCode = errorJson.optInt("err_code", -1)
val errorMsg = errorJson.optString("err_msg", "未知错误")
// 检查是否是资源授权错误
if (errorCode == 1001 && (errorMsg.contains("requested resource not granted") ||
errorMsg.contains("requested grant not found"))) {
Log.e(TAG, "资源授权错误: 应用可能未开通语音识别服务或密钥无效")
// 记录更多调试信息,但隐藏敏感信息
Log.d(TAG, "APP_ID长度: ${appId.length}")
Log.d(TAG, "APP_KEY长度: ${appKey.length}")
val errorMessage = "资源授权错误: 请确保应用已开通语音识别服务并且密钥有效 (错误码: $errorCode)"
if (mIsContinuousRecognitionActive) {
mCurrentContinuousCallback?.onError(errorMessage)
} else {
mCurrentCallback?.onError(errorMessage)
}
} else {
// 其他错误
if (mIsContinuousRecognitionActive) {
mCurrentContinuousCallback?.onError("引擎错误: $errorMsg (错误码: $errorCode)")
} else {
mCurrentCallback?.onError("引擎错误: $errorMsg (错误码: $errorCode)")
}
}
mRecognitionError = "引擎错误: $stdData"
mRecognitionLatch?.countDown()
} catch (e: JSONException) {
Log.e(TAG, "解析错误信息失败: ${e.message}")
if (mIsContinuousRecognitionActive) {
mCurrentContinuousCallback?.onError("引擎错误: $stdData")
} else {
mCurrentCallback?.onError("引擎错误: $stdData")
}
}
}
SpeechEngineDefines.MESSAGE_TYPE_PARTIAL_RESULT -> {
Log.d(TAG, "中间识别结果: $stdData")
processRecognitionResult(stdData, false)
}
SpeechEngineDefines.MESSAGE_TYPE_FINAL_RESULT -> {
Log.d(TAG, "最终识别结果: $stdData")
processRecognitionResult(stdData, true)
}
SpeechEngineDefines.MESSAGE_TYPE_VOLUME_LEVEL -> {
// 音量级别,可用于显示波形
// Log.d(TAG, "音量级别: $stdData")
}
}
}
/**
* 处理识别结果
*/
private fun processRecognitionResult(resultData: String, isFinal: Boolean) {
try {
val resultJson = JSONObject(resultData)
if (!resultJson.has("result")) {
return
}
val resultArray = resultJson.getJSONArray("result")
if (resultArray.length() == 0) {
return
}
val resultObj = resultArray.getJSONObject(0)
val text = resultObj.optString("text", "")
if (text.isEmpty()) {
return
}
if (mIsContinuousRecognitionActive) {
if (isFinal) {
mCurrentContinuousCallback?.onResult(text)
} else {
mCurrentContinuousCallback?.onRecognizing(text)
}
} else {
if (isFinal) {
mCurrentCallback?.onResult(text)
mRecognitionResult = text
mRecognitionLatch?.countDown()
}
}
} catch (e: JSONException) {
Log.e(TAG, "解析识别结果失败: ${e.message}")
}
}
/**
* 一次性识别回调接口
*/
interface RecognizeCallback {
fun onResult(text: String)
fun onError(error: String)
}
/**
* 连续识别回调接口
*/
interface ContinuousRecognizeCallback {
fun onResult(text: String)
fun onRecognizing(recognizing: String)
fun onSessionStarted()
fun onSessionStopped()
fun onCanceled(reason: String, errorDetails: String)
fun onError(error: String)
}
}

438
android/app/src/main/kotlin/com/example/deep_voice/VolcanoTtsHelper.kt

@ -1,438 +0,0 @@
package com.example.deep_voice
import android.content.Context
import android.util.Log
import com.bytedance.speech.speechengine.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator
/**
* 火山语音合成Helper类
*
* 该类封装了火山语音SDK的TTS功能,提供简单的接口供Flutter调用
* 实现基于官方文档的标准接入流程
*/
class VolcanoTtsHelper(context: Context) : SpeechEngine.SpeechListener {
private val TAG = "VolcanoTtsHelper"
private var mSpeechEngine: SpeechEngine? = null
private var mSpeechEngineHandler: Long = -1
private var isInitialized = false
private var applicationContext: Context? = context.applicationContext
// 播放状态回调
private var mPlaybackCallback: PlaybackCallback? = null
/**
* 初始化TTS引擎
*
* @param appId 火山语音服务AppID
* @param token 火山语音服务Token
* @param cluster 火山语音服务集群
*/
fun initialize(appId: String, token: String, cluster: String): Boolean {
try {
// 确保应用上下文已设置
if (applicationContext == null) {
Log.e(TAG, "应用上下文未设置")
return false
}
Log.d(TAG, "初始化火山语音TTS引擎,APP_ID: ${appId.take(3)}***,Token长度: ${token.length},集群区域: $cluster")
// 1. 准备环境
SpeechEngineGenerator.PrepareEnvironment(applicationContext, null)
// 2. 创建引擎实例
mSpeechEngine = SpeechEngineGenerator.getInstance()
mSpeechEngineHandler = mSpeechEngine?.createEngine() ?: -1
if (mSpeechEngineHandler == -1L) {
Log.e(TAG, "创建语音引擎失败")
return false
}
// 设置上下文
mSpeechEngine?.setContext(applicationContext)
// 3. 参数配置
// 3.1 引擎类型
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_ENGINE_NAME_STRING,
SpeechEngineDefines.TTS_ENGINE
)
// 3.2 日志级别
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_LOG_LEVEL_STRING,
SpeechEngineDefines.LOG_LEVEL_WARN
)
// 3.3 线上问题定位
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_UID_STRING,
"deep_voice_user"
)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING,
"deep_voice_device"
)
// 3.4 授权
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING,
appId
)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING,
"Bearer;$token"
)
// 3.5 合成场景
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING,
SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL
)
// 3.6 合成策略
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_WORK_MODE_INT,
SpeechEngineDefines.TTS_WORK_MODE_ONLINE
)
// 3.7 在线请求资源配置
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_ADDRESS_STRING,
"wss://openspeech.bytedance.com"
)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_URI_STRING,
"/api/v1/tts/ws_binary"
)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_CLUSTER_STRING,
cluster
)
// 3.8 启用内建播放器
mSpeechEngine?.setOptionBoolean(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL,
true
)
// 3.9 配置内建播放器的音源
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_AUDIO_STREAM_TYPE_INT,
SpeechEngineDefines.AUDIO_STREAM_TYPE_MEDIA
)
// 3.10 返回音频数据
mSpeechEngine?.setOptionInt(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT,
2
)
// 4. 初始化引擎实例
val ret = mSpeechEngine?.initEngine(mSpeechEngineHandler) ?: -1
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "初始化引擎失败: $ret")
return false
}
// 设置回调监听器
mSpeechEngine?.setListener(this)
isInitialized = true
Log.d(TAG, "火山语音TTS引擎初始化成功")
return true
} catch (e: Exception) {
Log.e(TAG, "火山语音TTS引擎初始化失败: ${e.message}")
e.printStackTrace()
return false
}
}
/**
* 直接合成并播放文本
*
* @param text 要合成的文本
* @param voiceType 语音类型
* @return 是否成功开始播放
*/
fun speakText(text: String, voiceType: String): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
return false
}
try {
Log.d(TAG, "开始直接播放文本: $text,使用语音类型: $voiceType")
// 先停止当前播放
try {
Log.d(TAG, "停止当前播放")
stopSpeaking()
// 添加短暂延迟,确保引擎状态已重置
Thread.sleep(100)
} catch (e: Exception) {
Log.e(TAG, "停止当前播放失败: ${e.message}")
// 继续尝试新的播放
}
// 设置发音人
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_ONLINE_STRING,
"other"
)
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING,
voiceType
)
// 设置文本
mSpeechEngine?.setOptionString(
mSpeechEngineHandler,
SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING,
text
)
// 启动引擎,开始合成并播放
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "启动引擎失败: $ret")
return false
}
Log.d(TAG, "成功开始播放")
return true
} catch (e: Exception) {
Log.e(TAG, "直接播放文本异常: ${e.message}")
return false
}
}
/**
* 停止播放
*
* @return 是否成功停止播放
*/
fun stopSpeaking(): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
return false
}
try {
Log.d(TAG, "停止播放")
// 停止引擎
val ret = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
if (ret != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "停止引擎失败: $ret")
// 尝试再次停止引擎
try {
Log.d(TAG, "尝试再次停止引擎")
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
Log.d(TAG, "引擎已停止")
} catch (e: Exception) {
Log.e(TAG, "再次停止引擎失败: ${e.message}")
}
return false
}
// 确保引擎完全停止
try {
Log.d(TAG, "确保引擎完全停止")
val stopAgainRet = mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
if (stopAgainRet != SpeechEngineDefines.ERR_NO_ERROR) {
Log.e(TAG, "再次停止引擎失败: $stopAgainRet")
} else {
Log.d(TAG, "引擎已完全停止")
}
} catch (e: Exception) {
Log.e(TAG, "再次停止引擎异常: ${e.message}")
}
return true
} catch (e: Exception) {
Log.e(TAG, "停止播放异常: ${e.message}")
e.printStackTrace()
// 尝试再次停止引擎
try {
Log.d(TAG, "尝试再次停止引擎")
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
Log.d(TAG, "引擎已停止")
} catch (e: Exception) {
Log.e(TAG, "再次停止引擎失败: ${e.message}")
}
return false
}
}
/**
* 设置播放速度
*
* @param speed 播放速度,范围0.5-2.0
* @return 是否设置成功
*/
fun setSpeed(speed: Float): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
return false
}
try {
Log.d(TAG, "设置播放速度: $speed")
// 设置播放速度 (使用新版本的API)
mSpeechEngine?.setOptionDouble(
SpeechEngineDefines.PARAMS_KEY_TTS_SPEED_RATIO_DOUBLE,
speed.toDouble()
)
return true
} catch (e: Exception) {
Log.e(TAG, "设置播放速度异常: ${e.message}")
return false
}
}
/**
* 设置播放音量
*
* @param volume 播放音量,范围0.0-1.0
* @return 是否设置成功
*/
fun setVolume(volume: Float): Boolean {
if (!isInitialized) {
Log.e(TAG, "TTS引擎尚未初始化")
return false
}
try {
Log.d(TAG, "设置播放音量: $volume")
// 设置播放音量 (使用新版本的API)
mSpeechEngine?.setOptionDouble(
SpeechEngineDefines.PARAMS_KEY_TTS_VOLUME_RATIO_DOUBLE,
volume.toDouble()
)
return true
} catch (e: Exception) {
Log.e(TAG, "设置播放音量异常: ${e.message}")
return false
}
}
/**
* 设置播放状态回调
*
* @param callback 播放状态回调
*/
fun setPlaybackCallback(callback: PlaybackCallback) {
mPlaybackCallback = callback
}
/**
* 释放资源
*/
fun dispose() {
try {
if (mSpeechEngineHandler != -1L) {
mSpeechEngine?.sendDirective(mSpeechEngineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "")
mSpeechEngine?.destroyEngine(mSpeechEngineHandler)
mSpeechEngineHandler = -1
}
mSpeechEngine = null
isInitialized = false
Log.d(TAG, "火山语音合成引擎已释放")
} catch (e: Exception) {
Log.e(TAG, "释放火山语音合成引擎失败: ${e.message}")
}
}
// 实现SpeechListener接口
override fun onSpeechMessage(type: Int, data: ByteArray, len: Int) {
val stdData = String(data)
when (type) {
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_START -> {
Log.d(TAG, "引擎启动成功: $stdData")
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> {
Log.d(TAG, "引擎已停止: $stdData")
}
SpeechEngineDefines.MESSAGE_TYPE_ENGINE_ERROR -> {
Log.e(TAG, "引擎错误: $stdData")
// 通知播放错误
mPlaybackCallback?.onPlaybackError("引擎错误: $stdData")
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_BEGIN -> {
Log.d(TAG, "合成开始: $stdData")
// 通知合成开始
mPlaybackCallback?.onSynthesisBegin()
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_END -> {
Log.d(TAG, "合成结束: $stdData")
// 通知合成结束
mPlaybackCallback?.onSynthesisEnd()
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_START_PLAYING -> {
Log.d(TAG, "开始播放: $stdData")
// 通知播放开始
mPlaybackCallback?.onPlaybackStarted()
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_FINISH_PLAYING -> {
Log.d(TAG, "播放结束: $stdData")
// 通知播放结束
mPlaybackCallback?.onPlaybackCompleted()
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_AUDIO_DATA -> {
Log.d(TAG, "收到音频数据")
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_PLAYBACK_PROGRESS -> {
Log.d(TAG, "播放进度: $stdData")
}
SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_MODE_TOGGLE -> {
Log.d(TAG, "合成模式切换: $stdData")
}
}
}
/**
* 播放状态回调接口
*/
interface PlaybackCallback {
fun onPlaybackStarted()
fun onPlaybackCompleted()
fun onPlaybackError(error: String)
// 新增合成开始回调
fun onSynthesisBegin()
// 新增合成结束回调
fun onSynthesisEnd()
}
}

8
lib/core/routes/app_pages.dart

@ -12,6 +12,8 @@ import '../../modules/test/views/asr_test_view.dart';
import '../../modules/test/bindings/test_binding.dart'; import '../../modules/test/bindings/test_binding.dart';
import '../../modules/translation/views/translation_view.dart'; import '../../modules/translation/views/translation_view.dart';
import '../../modules/translation/bindings/translation_binding.dart'; import '../../modules/translation/bindings/translation_binding.dart';
import '../../modules/translation/views/translation_history_view.dart';
import '../../modules/translation/bindings/translation_history_binding.dart';
// import '../../modules/speech_demo/speech_demo_page.dart'; // This file doesn't exist // import '../../modules/speech_demo/speech_demo_page.dart'; // This file doesn't exist
import './app_routes.dart'; import './app_routes.dart';
@ -57,6 +59,10 @@ abstract class AppPages {
page: () => const TranslationView(), page: () => const TranslationView(),
binding: TranslationBinding(), binding: TranslationBinding(),
), ),
GetPage(
name: Routes.translationHistory,
page: () => const TranslationHistoryView(),
binding: TranslationHistoryBinding(),
),
]; ];
} }

101
lib/data/providers/agent_provider.dart

@ -61,7 +61,18 @@ class AgentProvider {
请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活。 请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活。
'''), '''),
Agent(
id: 'simultaneous_interpreter',
name: '同声传译',
avatarUrl: 'assets/images/agents/interpreter.png',
description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。',
tag: '翻译',
isOnline: true,
voice: 'zh_female_daimengchuanmei_moon_bigtts',
welcomeMessage:
"大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
systemPrompt: ""),
Agent( Agent(
id: 'cyber_girlfriend', id: 'cyber_girlfriend',
name: '赛博女友 芯愿', name: '赛博女友 芯愿',
@ -199,94 +210,8 @@ class AgentProvider {
请始终以专业、关怀的态度为用户提供健康指导,帮助他们迈向更健康的生活。 请始终以专业、关怀的态度为用户提供健康指导,帮助他们迈向更健康的生活。
'''), '''),
Agent(
id: 'simultaneous_interpreter',
name: 'Lucy (同声翻译官)',
avatarUrl: 'assets/images/agents/interpreter.png',
description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。',
tag: '翻译',
isOnline: true,
voice: 'zh_female_daimengchuanmei_moon_bigtts',
welcomeMessage:
"大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
systemPrompt: '''
任务描述
你是一名专业的 同声翻译(Simultaneous Interpreter),你的任务是 即时将源语言文本翻译为目标语言,确保翻译 流畅、自然、精准,适应语境,并符合目标语言的表达习惯。你的翻译必须 迅速 但 不逐字直译,而是以 符合目标语言逻辑的方式 表达原意。
输入格式
你将接收一个 JSON 格式 的输入,格式如下:
{
"source_language": "语言代码",
"target_language": "语言代码",
"text": "待翻译文本"
}
字段说明:
• "source_language":源语言,使用 ISO 639-1 代码(如 "en" 表示英语,"zh" 表示中文)。
• "target_language":目标语言,使用 ISO 639-1 代码。
• "text":待翻译的文本内容。
示例输入(英语到中文):
{
"source_language": "en",
"target_language": "zh",
"text": "Good evening, ladies and gentlemen. Welcome to the annual conference."
}
输出格式
你的翻译必须严格按照 JSON 结构 输出,格式如下:
{
"source_language": "语言代码",
"target_language": "语言代码",
"original_text": "原始文本",
"translated_text": "翻译后的文本",
"confidence_score": 0.0
}
字段说明:
• "source_language":回传源语言代码。
• "target_language":回传目标语言代码。
• "original_text":回传原始文本内容,以便校对。
• "translated_text":翻译后的文本,必须符合目标语言的语法和表达习惯。
• "confidence_score":选填,范围 0.0 ~ 1.0,表示翻译置信度(1.0 表示极高置信度,0.0 表示最低置信度)。
示例输出(英语到中文):
{
"source_language": "en",
"target_language": "zh",
"original_text": "Good evening, ladies and gentlemen. Welcome to the annual conference.",
"translated_text": "各位女士们,先生们,晚上好!欢迎来到年度大会。",
"confidence_score": 0.98
}
翻译要求
1. 流畅自然:不能逐字直译,而是要符合目标语言的表达方式。
2. 保留语境:准确传达原意,避免误解或遗漏。
3. 即时响应:短句应尽快翻译,长句可适当拆分,提高可读性。
4. 格式严格:输出必须严格符合 JSON 结构,字段不可缺失。
可选高级模式
如果需要启用 自适应翻译模式,请在输入 JSON 中添加:
{
"mode": "adaptive"
}
• "mode": "adaptive":允许模型 根据语境调整语气(如正式、非正式风格)。
你的任务
请按照上述要求进行同声翻译,并以 JSON 结构返回结果。
''',
),
Agent( Agent(
id: 'speaking_coach', id: 'speaking_coach',
name: 'Sarah (英语口语教练)', name: 'Sarah (英语口语教练)',

11
lib/data/services/azure_asr_service.dart

@ -211,7 +211,16 @@ class AzureAsrService extends GetxService {
if (_isContinuousRecognitionActive) { if (_isContinuousRecognitionActive) {
Logger.warning('连续识别已经在进行中'); Logger.warning('连续识别已经在进行中');
return false; // 先停止当前正在进行的识别任务
Logger.info('尝试停止当前正在进行的识别任务');
try {
await stopContinuousRecognition();
// 添加短暂延迟,确保资源被正确释放
await Future.delayed(Duration(milliseconds: 300));
} catch (e) {
Logger.error('停止当前识别任务失败', e);
// 继续尝试启动新的识别任务
}
} }
try { try {

2
lib/data/services/volcano_tts_api_service.dart

@ -103,7 +103,7 @@ class VolcanoTtsApiService extends GetxController {
bool get isPlaying => _isPlaying.value; bool get isPlaying => _isPlaying.value;
// 默认发音人 - 使用预定义的值 // 默认发音人 - 使用预定义的值
final String defaultSpeaker = 'zh_female_meilinvyou_moon_bigtts'; final String defaultSpeaker = 'zh_female_yuanqinvyou_moon_bigtts';
// 音频缓冲区 // 音频缓冲区
final List<Uint8List> _audioBuffer = []; final List<Uint8List> _audioBuffer = [];

14
lib/modules/translation/bindings/translation_binding.dart

@ -7,19 +7,7 @@ import '../../../data/services/volcano_tts_service.dart';
class TranslationBinding implements Bindings { class TranslationBinding implements Bindings {
@override @override
void dependencies() { void dependencies() {
// Ensure services are registered
if (!Get.isRegistered<AzureAsrService>()) {
Get.put(AzureAsrService());
}
if (!Get.isRegistered<VolcanoTranslationService>()) {
Get.put(VolcanoTranslationService());
}
if (!Get.isRegistered<VolcanoTtsService>()) {
Get.put(VolcanoTtsService());
}
// Register the controller // Register the controller
Get.put(TranslationController()); Get.put(TranslationController());
} }

9
lib/modules/translation/bindings/translation_detail_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/translation_detail_controller.dart';
class TranslationDetailBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<TranslationDetailController>(() => TranslationDetailController());
}
}

11
lib/modules/translation/bindings/translation_history_binding.dart

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/translation_history_controller.dart';
class TranslationHistoryBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<TranslationHistoryController>(
() => TranslationHistoryController(),
);
}
}

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

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/volcano_tts_api_service.dart'; import '../../../data/services/volcano_tts_api_service.dart';
@ -11,6 +13,11 @@ class TranslationController extends GetxController {
final AzureAsrService _asrService = Get.find<AzureAsrService>(); final AzureAsrService _asrService = Get.find<AzureAsrService>();
final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>(); final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>(); final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final GetStorage _storage = GetStorage();
// Storage key
static const String _historyKey = 'translation_history';
static const int _maxHistoryItems = 100; // 最多保存100条历史记录
// State variables // State variables
final isRecognizing = false.obs; final isRecognizing = false.obs;
@ -22,6 +29,39 @@ class TranslationController extends GetxController {
final targetLanguage = '英语 (English)'.obs; final targetLanguage = '英语 (English)'.obs;
final translationHistory = <TranslationItem>[].obs; final translationHistory = <TranslationItem>[].obs;
// 支持的语言列表 - 改为Map结构,键为显示名称,值为语言代码
final Map<String, String> supportedLanguages = {
'中文 (Chinese)': 'zh',
'英语 (English)': 'en',
'日语 (Japanese)': 'ja',
'韩语 (Korean)': 'ko',
'法语 (French)': 'fr',
'德语 (German)': 'de',
'西班牙语 (Spanish)': 'es',
'意大利语 (Italian)': 'it',
'俄语 (Russian)': 'ru',
'泰语 (Thai)': 'th',
'葡萄牙语 (Portuguese)': 'pt',
'阿拉伯语 (Arabic)': 'ar',
'越南语 (Vietnamese)': 'vi',
'印尼语 (Indonesian)': 'id',
'印地语 (Hindi)': 'hi',
'土耳其语 (Turkish)': 'tr',
'波兰语 (Polish)': 'pl',
'荷兰语 (Dutch)': 'nl',
'瑞典语 (Swedish)': 'sv',
'捷克语 (Czech)': 'cs',
'希腊语 (Greek)': 'el',
'罗马尼亚语 (Romanian)': 'ro',
'匈牙利语 (Hungarian)': 'hu',
'乌克兰语 (Ukrainian)': 'uk',
'保加利亚语 (Bulgarian)': 'bg',
'丹麦语 (Danish)': 'da',
'芬兰语 (Finnish)': 'fi',
'挪威语 (Norwegian)': 'no',
'马来语 (Malay)': 'ms',
};
// Current translation item // Current translation item
TranslationItem? currentItem; TranslationItem? currentItem;
@ -34,13 +74,27 @@ class TranslationController extends GetxController {
// Timer for debouncing translation requests // Timer for debouncing translation requests
Timer? _translationDebounceTimer; Timer? _translationDebounceTimer;
// 当前会话ID
String? currentSessionId;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// 加载历史记录
loadTranslationHistory();
// Initialize the translation service // Initialize the translation service
_initTranslationService(); _initTranslationService();
// 添加后处理,确保在历史记录加载后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
// 延迟执行滚动操作,确保视图已完全渲染
Future.delayed(const Duration(milliseconds: 300), () {
_scrollToBottom();
});
});
// Start voice recognition automatically when the page is loaded // Start voice recognition automatically when the page is loaded
startRecognition(); startRecognition();
} }
@ -77,6 +131,9 @@ class TranslationController extends GetxController {
if (isRecognizing.value) return; if (isRecognizing.value) return;
try { try {
// 生成新的会话ID
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// Initialize a new translation item // Initialize a new translation item
currentItem = TranslationItem( currentItem = TranslationItem(
sourceText: '', sourceText: '',
@ -84,6 +141,8 @@ class TranslationController extends GetxController {
sourceLanguage: sourceLanguage.value, sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value, targetLanguage: targetLanguage.value,
timestamp: DateTime.now(), timestamp: DateTime.now(),
sessionId: currentSessionId!,
isFirstInSession: true, // 标记为会话中的第一条
); );
// Start the voice recognition service // Start the voice recognition service
@ -147,6 +206,9 @@ class TranslationController extends GetxController {
// 添加当前项到历史(如果有内容) // 添加当前项到历史(如果有内容)
finalizeCurrentItem(); finalizeCurrentItem();
// 重置当前会话ID
currentSessionId = null;
// 重置当前显示的文本 // 重置当前显示的文本
recognizedText.value = ''; recognizedText.value = '';
translatedText.value = ''; translatedText.value = '';
@ -165,12 +227,20 @@ class TranslationController extends GetxController {
// 如果当前没有活跃的翻译项,创建一个新的 // 如果当前没有活跃的翻译项,创建一个新的
if (currentItem == null) { if (currentItem == null) {
// 如果没有当前会话ID,创建一个新的
if (currentSessionId == null) {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
}
currentItem = TranslationItem( currentItem = TranslationItem(
sourceText: text, sourceText: text,
translatedText: '', translatedText: '',
sourceLanguage: sourceLanguage.value, sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value, targetLanguage: targetLanguage.value,
timestamp: DateTime.now(), timestamp: DateTime.now(),
sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty ||
translationHistory.last.sessionId != currentSessionId,
); );
} else { } else {
// 更新当前翻译项 // 更新当前翻译项
@ -196,12 +266,20 @@ class TranslationController extends GetxController {
// 更新当前翻译项(如果不存在则创建一个) // 更新当前翻译项(如果不存在则创建一个)
if (currentItem == null) { if (currentItem == null) {
// 如果没有当前会话ID,创建一个新的
if (currentSessionId == null) {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
}
currentItem = TranslationItem( currentItem = TranslationItem(
sourceText: text, sourceText: text,
translatedText: '', translatedText: '',
sourceLanguage: sourceLanguage.value, sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value, targetLanguage: targetLanguage.value,
timestamp: DateTime.now(), timestamp: DateTime.now(),
sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty ||
translationHistory.last.sessionId != currentSessionId,
); );
} else { } else {
currentItem!.sourceText = text; currentItem!.sourceText = text;
@ -228,6 +306,7 @@ class TranslationController extends GetxController {
currentItem!.translatedText.isNotEmpty && currentItem!.translatedText.isNotEmpty &&
!translationHistory.contains(currentItem)) { !translationHistory.contains(currentItem)) {
translationHistory.add(currentItem!); translationHistory.add(currentItem!);
saveTranslationHistory();
currentItem = null; currentItem = null;
} }
} }
@ -247,10 +326,30 @@ class TranslationController extends GetxController {
final TranslationItem? itemToUpdate = currentItem; final TranslationItem? itemToUpdate = currentItem;
try { try {
// 获取源语言和目标语言的代码
final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh';
// 尝试检测输入文本的语言(这里简化处理,实际项目可能需要更复杂的语言检测)
bool shouldSwapLanguages = false;
// 简单的语言检测逻辑 - 检查是否包含中文字符
final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text);
final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text);
// 如果源语言是中文但文本不包含中文字符,且包含拉丁字符,可能需要交换语言
if (sourceCode == 'zh' && !containsChinese && containsLatin) {
shouldSwapLanguages = true;
}
// 如果源语言不是中文但文本包含中文字符,可能需要交换语言
else if (sourceCode != 'zh' && containsChinese) {
shouldSwapLanguages = true;
}
// 调用翻译服务
final result = await _translationService.translateText( final result = await _translationService.translateText(
text: text, text: text,
sourceLanguage: sourceLanguage.value, sourceLanguage: shouldSwapLanguages ? targetLanguage.value : sourceLanguage.value,
targetLanguage: targetLanguage.value, targetLanguage: shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value,
); );
if (result != null) { if (result != null) {
@ -263,7 +362,28 @@ class TranslationController extends GetxController {
// 如果是最终结果,并且当前项仍然是发起请求时的项,则添加到历史 // 如果是最终结果,并且当前项仍然是发起请求时的项,则添加到历史
if (isFinal && itemToUpdate == currentItem) { if (isFinal && itemToUpdate == currentItem) {
translationHistory.add(itemToUpdate); // 检查是否已经存在相同会话ID的项
bool isFirstInSession = true;
for (var item in translationHistory) {
if (item.sessionId == itemToUpdate.sessionId) {
isFirstInSession = false;
break;
}
}
// 创建一个新的TranslationItem,包含正确的isFirstInSession值
final updatedItem = TranslationItem(
sourceText: itemToUpdate.sourceText,
translatedText: itemToUpdate.translatedText,
sourceLanguage: itemToUpdate.sourceLanguage,
targetLanguage: itemToUpdate.targetLanguage,
timestamp: itemToUpdate.timestamp,
sessionId: itemToUpdate.sessionId,
isFirstInSession: isFirstInSession,
);
translationHistory.add(updatedItem);
saveTranslationHistory();
currentItem = null; // 重要:将当前项设为null,表示已完成 currentItem = null; // 重要:将当前项设为null,表示已完成
// 重置当前显示的文本,避免与历史项重复 // 重置当前显示的文本,避免与历史项重复
@ -311,15 +431,90 @@ class TranslationController extends GetxController {
// Scroll to the bottom of the list // Scroll to the bottom of the list
void _scrollToBottom() { void _scrollToBottom() {
if (scrollController.hasClients) { if (scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), () { try {
scrollController.animateTo( scrollController.animateTo(
scrollController.position.maxScrollExtent, scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeOut, curve: Curves.easeOut,
); );
} catch (e) {
// 如果滚动失败,尝试延迟后再次滚动
Future.delayed(const Duration(milliseconds: 200), () {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
} else {
// 如果scrollController还没有客户端,延迟后再尝试
Future.delayed(const Duration(milliseconds: 200), _scrollToBottom);
}
}
// 切换源语言和目标语言
void swapLanguages() {
final temp = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp;
// 如果正在识别,需要停止并重新开始
if (isRecognizing.value) {
stopRecognition();
startRecognition();
}
}
// 设置源语言
void setSourceLanguage(String language) {
if (sourceLanguage.value != language) {
sourceLanguage.value = language;
// 如果正在识别,需要停止并重新开始
if (isRecognizing.value) {
stopRecognition();
startRecognition();
}
}
}
// 设置目标语言
void setTargetLanguage(String language) {
if (targetLanguage.value != language) {
targetLanguage.value = language;
}
}
// Load translation history from storage
void loadTranslationHistory() {
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 确保在历史记录加载后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
}); });
} }
} }
// Save translation history to storage
void saveTranslationHistory() {
// 限制历史记录数量,保留最新的记录
if (translationHistory.length > _maxHistoryItems) {
translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems);
}
final List<Map<String, dynamic>> historyJson =
translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson));
}
// Clear translation history
void clearTranslationHistory() {
translationHistory.clear();
_storage.remove(_historyKey);
}
} }
// Model class for translation items // Model class for translation items
@ -329,6 +524,8 @@ class TranslationItem {
final String sourceLanguage; final String sourceLanguage;
final String targetLanguage; final String targetLanguage;
final DateTime timestamp; final DateTime timestamp;
final String sessionId;
final bool isFirstInSession;
TranslationItem({ TranslationItem({
required this.sourceText, required this.sourceText,
@ -336,5 +533,31 @@ class TranslationItem {
required this.sourceLanguage, required this.sourceLanguage,
required this.targetLanguage, required this.targetLanguage,
required this.timestamp, required this.timestamp,
required this.sessionId,
this.isFirstInSession = false,
}); });
factory TranslationItem.fromJson(Map<String, dynamic> json) {
return TranslationItem(
sourceText: json['sourceText'],
translatedText: json['translatedText'],
sourceLanguage: json['sourceLanguage'],
targetLanguage: json['targetLanguage'],
timestamp: DateTime.parse(json['timestamp']),
sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
isFirstInSession: json['isFirstInSession'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'sourceText': sourceText,
'translatedText': translatedText,
'sourceLanguage': sourceLanguage,
'targetLanguage': targetLanguage,
'timestamp': timestamp.toIso8601String(),
'sessionId': sessionId,
'isFirstInSession': isFirstInSession,
};
}
} }

125
lib/modules/translation/controllers/translation_detail_controller.dart

@ -0,0 +1,125 @@
import 'package:get/get.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart';
import 'package:get_storage/get_storage.dart';
class TranslationDetailController extends GetxController {
// 依赖注入
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final TranslationHistoryController historyController = Get.find<TranslationHistoryController>();
final GetStorage _storage = GetStorage();
// 存储键前缀
static const String _summaryKeyPrefix = 'translation_summary_';
// 状态变量
final selectedTabIndex = 0.obs;
final isGeneratingSummary = false.obs;
final summaryResult = ''.obs;
// 当前会话信息
late final TranslationItem currentItem;
late final List<TranslationItem> sessionItems;
// 初始化方法
void initialize(TranslationItem item, List<TranslationItem> items) {
currentItem = item;
sessionItems = items;
// 尝试从存储中加载摘要
loadSavedSummary();
}
// 加载保存的摘要
void loadSavedSummary() {
final String? savedSummary = _storage.read<String>(_getSummaryKey());
if (savedSummary != null && savedSummary.isNotEmpty) {
summaryResult.value = savedSummary;
}
}
// 保存摘要
void saveSummary(String summary) {
if (summary.isNotEmpty) {
_storage.write(_getSummaryKey(), summary);
}
}
// 获取摘要的存储键
String _getSummaryKey() {
return _summaryKeyPrefix + currentItem.sessionId;
}
// 切换选项卡
void switchTab(int index) {
selectedTabIndex.value = index;
}
// 播放翻译项
void playTranslationItem(TranslationItem item) {
historyController.playTranslationItem(item);
}
// 播放文本
void playText(String text) {
historyController.playText(text);
}
// 获取合并的源文本
String getCombinedSourceText() {
return sessionItems.map((e) => e.sourceText).join('\n\n');
}
// 获取合并的翻译文本
String getCombinedTranslatedText() {
return sessionItems.map((e) => e.translatedText).join('\n\n');
}
// 提取摘要
Future<String> extractSummary(String text) async {
isGeneratingSummary.value = true;
try {
// 构建提示词
final messages = [
{'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'},
];
// 系统提示词
const systemPrompt = '''
你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。
请按照以下格式生成摘要:
1. 首先用1-2句话对整体内容进行简要概括
2. 然后使用项目符号列表(•)列出关键点或重要信息
3. 如果是对话内容,请特别标注出讨论的结论或达成的共识
请遵循以下原则:
1. 保留原文的核心信息和关键点
2. 使用简洁、清晰的语言
3. 保持客观,不添加个人观点
4. 摘要总长度应该是原文的1/3或更短
5. 确保列表中的每个要点都简明扼要,突出重点
''';
// 调用AI服务生成摘要
final result = await _aiService.sendMessage(
messages: messages,
systemPrompt: systemPrompt,
);
summaryResult.value = result;
// 保存摘要
saveSummary(result);
return result;
} catch (e) {
summaryResult.value = '生成摘要失败: ${e.toString()}';
return '生成摘要失败: ${e.toString()}';
} finally {
isGeneratingSummary.value = false;
}
}
}

129
lib/modules/translation/controllers/translation_history_controller.dart

@ -0,0 +1,129 @@
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../controllers/translation_controller.dart';
class TranslationHistoryController extends GetxController {
// 引用主翻译控制器,以访问历史记录数据
final TranslationController translationController = Get.find<TranslationController>();
// 按日期分组的历史记录
final Rx<Map<String, List<TranslationItem>>> groupedHistory = Rx<Map<String, List<TranslationItem>>>({});
// 排序后的日期列表
final RxList<String> sortedDates = <String>[].obs;
// 会话映射表,用于存储每个会话的所有项目
final Rx<Map<String, List<TranslationItem>>> sessionMap = Rx<Map<String, List<TranslationItem>>>({});
@override
void onInit() {
super.onInit();
// 监听翻译历史记录的变化
ever(translationController.translationHistory, (_) {
_groupHistoryByDate();
_groupHistoryBySession();
});
// 初始分组
_groupHistoryByDate();
_groupHistoryBySession();
}
// 按日期对历史记录进行分组
void _groupHistoryByDate() {
final Map<String, List<TranslationItem>> newGroupedHistory = {};
// 筛选出每个会话的第一条记录
final List<TranslationItem> firstItemsOfSessions = translationController.translationHistory
.where((item) => item.isFirstInSession)
.toList();
for (var item in firstItemsOfSessions) {
final DateTime date = item.timestamp;
String key;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final twoDaysAgo = today.subtract(const Duration(days: 2));
final itemDate = DateTime(date.year, date.month, date.day);
if (itemDate == today) {
key = 'TODAY';
} else if (itemDate == yesterday) {
key = '1 DAY AGO';
} else if (itemDate == twoDaysAgo) {
key = '2 DAYS AGO';
} else {
key = DateFormat('yyyy-MM-dd').format(date);
}
if (!newGroupedHistory.containsKey(key)) {
newGroupedHistory[key] = [];
}
newGroupedHistory[key]!.add(item);
}
// 对每个日期组内的项目按时间倒序排序
newGroupedHistory.forEach((key, items) {
items.sort((a, b) => b.timestamp.compareTo(a.timestamp));
});
// 更新分组后的历史记录
groupedHistory.value = newGroupedHistory;
// 按日期排序
final List<String> dates = newGroupedHistory.keys.toList()
..sort((a, b) {
if (a == 'TODAY') return -1;
if (b == 'TODAY') return 1;
if (a == '1 DAY AGO') return -1;
if (b == '1 DAY AGO') return 1;
if (a == '2 DAYS AGO') return -1;
if (b == '2 DAYS AGO') return 1;
return b.compareTo(a); // 降序排列其他日期
});
sortedDates.value = dates;
}
// 按会话ID对历史记录进行分组
void _groupHistoryBySession() {
final Map<String, List<TranslationItem>> newSessionMap = {};
for (var item in translationController.translationHistory) {
if (!newSessionMap.containsKey(item.sessionId)) {
newSessionMap[item.sessionId] = [];
}
newSessionMap[item.sessionId]!.add(item);
}
// 对每个会话内的项目按时间升序排序
newSessionMap.forEach((key, items) {
items.sort((a, b) => a.timestamp.compareTo(b.timestamp));
});
// 更新会话映射表
sessionMap.value = newSessionMap;
}
// 获取指定会话的所有翻译项
List<TranslationItem> getSessionItems(String sessionId) {
return sessionMap.value[sessionId] ?? [];
}
// 播放翻译项
void playTranslationItem(TranslationItem item) {
translationController.playTranslationItem(item);
}
// 播放指定文本
void playText(String text) {
translationController.playTranslatedText(text);
}
// 检查是否有历史记录
bool get hasHistory => translationController.translationHistory.isNotEmpty;
}

491
lib/modules/translation/views/translation_detail_view.dart

@ -0,0 +1,491 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart';
import '../controllers/translation_detail_controller.dart';
import 'package:intl/intl.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
class TranslationDetailView extends GetView<TranslationDetailController> {
final TranslationItem item;
final List<TranslationItem> sessionItems;
final TranslationHistoryController historyController;
const TranslationDetailView({
Key? key,
required this.item,
required this.sessionItems,
required this.historyController,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// 初始化控制器
controller.initialize(item, sessionItems);
return Scaffold(
backgroundColor: Color(0xFFF5F5F8),
appBar: AppBar(
backgroundColor: Color(0xFFF5F5F8),
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
'Detail',
style: TextStyle(
color: Colors.black,
fontSize: 24.sp,
fontWeight: FontWeight.bold,
),
),
centerTitle: true,
actions: [
IconButton(
icon: Icon(
Icons.share,
color: Colors.black,
size: 22.sp,
),
onPressed: () {
_shareContent();
},
),
],
),
body: Column(
children: [
// 选项卡按钮
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
child: Row(
children: [
Expanded(
child: Obx(() => _buildTabButton(
title: '原始记录',
isSelected: controller.selectedTabIndex.value == 0,
onTap: () => controller.switchTab(0),
)),
),
SizedBox(width: 12.w),
Expanded(
child: Obx(() => _buildTabButton(
title: '记录摘要',
isSelected: controller.selectedTabIndex.value == 1,
onTap: () => controller.switchTab(1),
)),
),
],
),
),
// 内容区域
Expanded(
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Obx(() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 会话时间
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.h),
child: Text(
DateFormat('yyyy-MM-dd HH:mm').format(item.timestamp),
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
letterSpacing: 1.0,
),
),
),
SizedBox(height: 8.h),
// 根据选中的选项卡显示不同内容
if (controller.selectedTabIndex.value == 0)
// 原始记录 - 显示所有翻译项
...sessionItems.map((sessionItem) => _buildSessionItemWidget(sessionItem)).toList()
else
// 记录总结 - 显示合并后的内容
_buildSummaryWidget(),
SizedBox(height: 24.h),
],
);
}),
),
),
],
),
);
}
// 构建选项卡按钮
Widget _buildTabButton({
required String title,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
height: 40.h,
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.white,
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(8),
blurRadius: 4,
offset: Offset(0, 1),
),
],
),
alignment: Alignment.center,
child: Text(
title,
style: TextStyle(
fontSize: 15.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? Colors.white : Colors.black87,
),
),
),
);
}
// 构建会话详情中的翻译项
Widget _buildSessionItemWidget(TranslationItem item) {
return Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(8),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本部分
Padding(
padding: EdgeInsets.fromLTRB(20.r, 20.r, 20.r, 10.r),
child: Text(
item.sourceText,
style: TextStyle(
fontSize: 15.sp,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
),
// 翻译文本部分
Padding(
padding: EdgeInsets.fromLTRB(20.r, 10.r, 20.r, 16.r),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[700],
fontWeight: FontWeight.normal,
),
),
),
SizedBox(width: 12.w),
// 播放按钮
GestureDetector(
onTap: () {
controller.playTranslationItem(item);
},
child: Container(
width: 28.w,
height: 28.h,
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.volume_up,
color: Colors.blue,
size: 16.sp,
),
),
),
],
),
),
],
),
);
}
// 构建总结视图
Widget _buildSummaryWidget() {
final String combinedSourceText = controller.getCombinedSourceText();
return Column(
children: [
// 摘要内容容器
Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(8),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Obx(() {
if (controller.summaryResult.value.isEmpty) {
// 如果没有摘要,显示提示信息
return Padding(
padding: EdgeInsets.all(24.r),
child: Center(
child: Text(
'点击下方按钮生成会话摘要',
style: TextStyle(
fontSize: 15.sp,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
),
);
} else {
// 如果有摘要,显示摘要内容
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(20.r),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'摘要内容',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
SizedBox(height: 12.h),
Text(
controller.summaryResult.value,
style: TextStyle(
fontSize: 15.sp,
color: Colors.black87,
height: 1.5,
),
),
],
),
),
Divider(
height: 1,
thickness: 0.5,
color: Colors.grey[200],
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.r, vertical: 8.r),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 播放按钮
GestureDetector(
onTap: () {
controller.playText(controller.summaryResult.value);
},
child: Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.volume_up,
color: Colors.blue,
size: 18.sp,
),
),
),
SizedBox(width: 12.w),
// 复制按钮
GestureDetector(
onTap: () {
// 复制摘要到剪贴板
Clipboard.setData(ClipboardData(text: controller.summaryResult.value));
Get.snackbar(
'已复制',
'摘要已复制到剪贴板',
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2),
);
},
child: Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.copy,
color: Colors.blue,
size: 18.sp,
),
),
),
],
),
),
],
);
}
}),
),
// 提取摘要按钮
Obx(() => Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
child: ElevatedButton(
onPressed: controller.isGeneratingSummary.value || controller.summaryResult.value.isNotEmpty
? null
: () => _extractSummary(combinedSourceText),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
disabledBackgroundColor: controller.isGeneratingSummary.value
? Colors.blue.withAlpha(128)
: Colors.grey.withAlpha(128),
),
child: controller.isGeneratingSummary.value
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20.w,
height: 20.h,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.0,
),
),
SizedBox(width: 12.w),
Text(
'生成中...',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
],
)
: Text(
controller.summaryResult.value.isEmpty ? '提取摘要' : '已生成摘要',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
)),
// 重新生成按钮
Obx(() {
if (controller.summaryResult.value.isEmpty || controller.isGeneratingSummary.value) {
return SizedBox.shrink();
}
return Container(
width: double.infinity,
margin: EdgeInsets.only(bottom: 16.h),
child: OutlinedButton(
onPressed: () => _extractSummary(combinedSourceText),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
side: BorderSide(color: Colors.blue),
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
),
child: Text(
'重新生成',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
),
);
}),
],
);
}
// 提取摘要方法
void _extractSummary(String text) async {
await controller.extractSummary(text);
}
// 分享内容方法
void _shareContent() {
String shareText = '';
if (controller.selectedTabIndex.value == 0) {
// 分享原始记录
final String sourceText = controller.getCombinedSourceText();
final String translatedText = controller.getCombinedTranslatedText();
shareText = '原文:\n$sourceText\n\n译文:\n$translatedText';
} else {
// 分享摘要
if (controller.summaryResult.value.isNotEmpty) {
shareText = '会话摘要:\n${controller.summaryResult.value}';
} else {
Get.snackbar(
'无法分享',
'请先生成摘要',
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2),
);
return;
}
}
// 调用系统分享
Share.share(
shareText,
subject: '翻译会话记录 - ${DateFormat('yyyy-MM-dd HH:mm').format(controller.currentItem.timestamp)}',
);
}
}

244
lib/modules/translation/views/translation_history_view.dart

@ -0,0 +1,244 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart';
import '../bindings/translation_detail_binding.dart';
import 'translation_detail_view.dart';
class TranslationHistoryView extends GetView<TranslationHistoryController> {
const TranslationHistoryView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFF5F5F8),
appBar: AppBar(
backgroundColor: Color(0xFFF5F5F8),
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
'History',
style: TextStyle(
color: Colors.black,
fontSize: 24.sp,
fontWeight: FontWeight.bold,
),
),
centerTitle: true,
actions: [
IconButton(
icon: Icon(
Icons.delete_outline,
color: Colors.black,
size: 24.sp,
),
onPressed: () {
_showClearHistoryConfirmDialog();
},
),
],
),
body: Obx(() {
if (!controller.hasHistory) {
return Center(
child: Text(
'暂无历史记录',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey,
),
),
);
}
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
itemCount: controller.sortedDates.length,
itemBuilder: (context, index) {
final date = controller.sortedDates[index];
final items = controller.groupedHistory.value[date]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 24.h, bottom: 12.h, left: 12.w),
child: Text(
date,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
letterSpacing: 1.0,
),
),
),
_buildDateGroupContainer(items),
],
);
},
);
}),
);
}
// 构建日期组容器
Widget _buildDateGroupContainer(List<TranslationItem> items) {
return Container(
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(8),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: ListView.separated(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: items.length,
separatorBuilder: (context, index) => Divider(
height: 1,
thickness: 0.5,
color: Colors.grey[200],
indent: 0,
endIndent: 0,
),
itemBuilder: (context, index) {
return _buildHistoryItemInGroup(items[index]);
},
),
);
}
// 构建组内的历史记录项
Widget _buildHistoryItemInGroup(TranslationItem item) {
return GestureDetector(
onTap: () {
// 点击查看详情
_showTranslationDetail(item);
},
child: Stack(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Text(
item.sourceText,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 6.h),
// 翻译文本
Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
Positioned(
right: 12.w,
top: 0,
bottom: 0,
child: Icon(
Icons.chevron_right,
color: Colors.grey[350],
size: 20.sp,
),
),
],
),
);
}
void _showTranslationDetail(TranslationItem item) {
// 获取该会话的所有翻译项
final List<TranslationItem> sessionItems = controller.getSessionItems(item.sessionId);
Get.to(
() => TranslationDetailView(
item: item,
sessionItems: sessionItems,
historyController: controller,
),
binding: TranslationDetailBinding(),
);
}
void _showClearHistoryConfirmDialog() {
Get.dialog(
AlertDialog(
title: Text(
'清除历史记录',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
content: Text(
'确定要清除所有翻译历史记录吗?此操作无法撤销。',
style: TextStyle(
fontSize: 16.sp,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
// 清除历史记录
controller.translationController.clearTranslationHistory();
Get.back();
},
child: Text(
'确定',
style: TextStyle(
fontSize: 16.sp,
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
}

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

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/translation_controller.dart'; import '../controllers/translation_controller.dart';
import '../views/translation_history_view.dart';
import '../bindings/translation_history_binding.dart';
// import 'package:intl/intl.dart'; // import 'package:intl/intl.dart';
class TranslationView extends GetView<TranslationController> { class TranslationView extends GetView<TranslationController> {
@ -34,29 +36,63 @@ class TranslationView extends GetView<TranslationController> {
title: Row( title: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Obx(() => Text( // 源语言选择
controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part GestureDetector(
style: TextStyle( onTap: () => _showLanguageSelectionDialog(true),
fontSize: 16.sp, child: Container(
fontWeight: FontWeight.w600, padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
color: Colors.black, decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Obx(() => Text(
controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
)),
), ),
)),
SizedBox(width: 4.w),
Icon(
Icons.arrow_forward_ios,
color: Colors.black,
size: 16.sp,
), ),
SizedBox(width: 4.w),
Obx(() => Text( // 语言互换按钮
controller.targetLanguage.value.split(' ')[0], // Just show the English part GestureDetector(
style: TextStyle( onTap: controller.swapLanguages,
fontSize: 16.sp, child: Container(
fontWeight: FontWeight.w600, margin: EdgeInsets.symmetric(horizontal: 8.w),
color: Colors.black, padding: EdgeInsets.all(4.r),
decoration: BoxDecoration(
color: Colors.blue[50],
shape: BoxShape.circle,
),
child: Icon(
Icons.swap_horiz,
color: Colors.blue,
size: 20.sp,
),
),
),
// 目标语言选择
GestureDetector(
onTap: () => _showLanguageSelectionDialog(false),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Obx(() => Text(
controller.targetLanguage.value.split(' ')[0], // Just show the English part
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
),
)),
), ),
)), ),
], ],
), ),
centerTitle: true, centerTitle: true,
@ -71,6 +107,26 @@ class TranslationView extends GetView<TranslationController> {
onPressed: controller.toggleTts, onPressed: controller.toggleTts,
tooltip: '自动播放翻译结果', tooltip: '自动播放翻译结果',
)), )),
// 历史记录按钮
IconButton(
icon: Icon(
Icons.history,
color: Colors.black,
size: 24.sp,
),
onPressed: () async {
// 停止语音识别,释放资源
controller.stopRecognition();
await Get.to(
() => const TranslationHistoryView(),
binding: TranslationHistoryBinding(),
);
// 返回时重新启动语音识别
controller.startRecognition();
},
tooltip: '查看历史记录',
),
], ],
); );
} }
@ -229,4 +285,137 @@ class TranslationView extends GetView<TranslationController> {
), ),
); );
} }
// 显示语言选择对话框
void _showLanguageSelectionDialog(bool isSource) {
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
insetPadding: EdgeInsets.symmetric(horizontal: 20.w),
backgroundColor: Colors.white,
child: Container(
width: 0.9.sw,
padding: EdgeInsets.only(top: 32.h, bottom: 16.h),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Text(
'Select Language',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
SizedBox(height: 24.h),
Container(
constraints: BoxConstraints(
maxHeight: 0.6.sh,
),
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: controller.supportedLanguages.keys.map((language) {
final isSelected = isSource
? controller.sourceLanguage.value == language
: controller.targetLanguage.value == language;
return _buildLanguageItem(
language: language.split(' ')[0],
isSelected: isSelected,
onTap: () {
if (isSource) {
controller.setSourceLanguage(language);
} else {
controller.setTargetLanguage(language);
}
Get.back();
},
);
}).toList(),
),
),
),
SizedBox(height: 24.h),
GestureDetector(
onTap: () => Get.back(),
child: Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 16.h),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'Cancel',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
),
],
),
),
),
barrierDismissible: true,
);
}
// 构建语言选择项
Widget _buildLanguageItem({
required String language,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
language,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.normal,
color: Colors.black,
),
),
Container(
width: 28.w,
height: 28.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.r),
border: Border.all(
color: isSelected ? Colors.transparent : Colors.grey[300]!,
width: 1.5,
),
color: isSelected ? Color(0xFF6C5CE7) : Colors.white,
),
child: isSelected
? Icon(
Icons.check,
color: Colors.white,
size: 20.sp,
)
: null,
),
],
),
),
);
}
} }

4
linux/flutter/generated_plugin_registrant.cc

@ -7,12 +7,16 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <record_linux/record_linux_plugin.h> #include <record_linux/record_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_size/window_size_plugin.h> #include <window_size/window_size_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) record_linux_registrar = g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar); record_linux_plugin_register_with_registrar(record_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_size_registrar = g_autoptr(FlPluginRegistrar) window_size_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin");
window_size_plugin_register_with_registrar(window_size_registrar); window_size_plugin_register_with_registrar(window_size_registrar);

1
linux/flutter/generated_plugins.cmake

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
record_linux record_linux
url_launcher_linux
window_size window_size
) )

1
pubspec.yaml

@ -35,6 +35,7 @@ dependencies:
device_info_plus: ^10.1.2 device_info_plus: ^10.1.2
uuid: ^4.3.3 uuid: ^4.3.3
record: ^5.2.1 record: ^5.2.1
share_plus: ^10.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

6
windows/flutter/generated_plugin_registrant.cc

@ -8,6 +8,8 @@
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h> #include <record_windows/record_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_size/window_size_plugin.h> #include <window_size/window_size_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
@ -15,6 +17,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar( RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowSizePluginRegisterWithRegistrar( WindowSizePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowSizePlugin")); registry->GetRegistrarForPlugin("WindowSizePlugin"));
} }

2
windows/flutter/generated_plugins.cmake

@ -5,6 +5,8 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows permission_handler_windows
record_windows record_windows
share_plus
url_launcher_windows
window_size window_size
) )

Loading…
Cancel
Save