import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:yx_asr/yx_asr.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'YX ASR Example', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), home: const SpeechRecognitionPage(), ); } } /// 语音识别演示页面 class SpeechRecognitionPage extends StatefulWidget { const SpeechRecognitionPage({super.key}); @override State createState() => _SpeechRecognitionPageState(); } class _SpeechRecognitionPageState extends State { final YxAsrService _speechService = YxAsrService(); final TextEditingController _textController = TextEditingController(); final FocusNode _textFocusNode = FocusNode(); // 状态变量 bool _isInitialized = false; bool _isListening = false; String _currentText = ''; String _errorMessage = ''; List _recognitionHistory = []; // 录音相关 final List _realtimeResults = []; // 存储实时识别片段 @override void initState() { super.initState(); _initializeSpeechService(); } @override void dispose() { _textController.dispose(); _textFocusNode.dispose(); _speechService.dispose(); super.dispose(); } /// 初始化语音识别服务 Future _initializeSpeechService() async { try { // 🚀 使用高质量模式获得最佳识别效果 _speechService.setRecognitionQuality(RecognitionQuality.highQuality); // 🎵 设置标准采样率,与模型匹配获得最佳效果 _speechService.setSampleRate(SampleRate.standard); // 🔧 可选:进一步自定义高级配置(禁用端点检测,用户手动控制) _speechService.setAdvancedConfig( const AdvancedRecognitionConfig( decodingMethod: DecodingMethod.modifiedBeamSearch, maxActivePaths: 8, enableEndpoint: false, // 禁用自动端点检测 rule1MinTrailingSilence: 2.4, rule2MinTrailingSilence: 1.2, rule3MinUtteranceLength: 20.0, featureDim: 80, blankPenalty: 0.0, ), ); // 初始化服务 - 使用2023年模型 final success = await _speechService.initializeWithDefaultModel(ModelConfig.zh2023); if (success) { // 监听识别结果 _speechService.onResult.listen((result) { print('📱 [Example] 接收到识别结果: "${result.recognizedWords}"'); setState(() { // 更新当前识别的文本(实时显示) if (result.recognizedWords.isNotEmpty) { print('📱 [Example] 实时识别: ${result.recognizedWords}'); _currentText = result.recognizedWords; // 更新文本框显示 _updateTextController(); } }); }); // 监听错误 _speechService.onError.listen((error) { setState(() { _errorMessage = error.errorMsg; _isListening = false; }); _showErrorSnackBar(error.errorMsg); }); // 监听状态变化 _speechService.onListeningStatusChanged.listen((isListening) { setState(() { _isListening = isListening; if (!isListening) { _currentText = ''; } }); }); setState(() { _isInitialized = true; _errorMessage = ''; }); } else { setState(() { _errorMessage = '初始化失败,请检查权限和模型文件'; }); } } catch (e) { setState(() { _errorMessage = '初始化异常: $e'; }); } } /// 显示错误提示 void _showErrorSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, action: SnackBarAction( label: '重试', textColor: Colors.white, onPressed: _initializeSpeechService, ), ), ); } /// 清除历史记录 void _clearHistory() { setState(() { _recognitionHistory.clear(); _realtimeResults.clear(); _currentText = ''; _textController.clear(); _errorMessage = ''; }); } /// 更新文本控制器内容 void _updateTextController() { // 组合已确认的结果和当前实时结果 final confirmedText = _realtimeResults.join(' '); final displayText = _currentText.isNotEmpty ? '$confirmedText ${_currentText}'.trim() : confirmedText; // 只有当内容真正改变时才更新,避免光标跳动 if (_textController.text != displayText) { final cursorPosition = _textController.selection.baseOffset; _textController.text = displayText; // 尝试保持光标位置,如果超出范围则移到末尾 if (cursorPosition <= displayText.length) { _textController.selection = TextSelection.fromPosition( TextPosition(offset: cursorPosition), ); } else { _textController.selection = TextSelection.fromPosition( TextPosition(offset: displayText.length), ); } } } /// 切换录音状态 Future _toggleRecording() async { if (!_isInitialized) return; try { if (_isListening) { print('📱 [Example] 停止录音'); await _speechService.stopListening(); // 录音结束后,将当前识别的文本保存到历史记录 if (_currentText.isNotEmpty) { setState(() { _realtimeResults.add(_currentText); _recognitionHistory.insert(0, _currentText); print('📱 [Example] 添加到历史记录: $_currentText'); // 保持历史记录在合理数量 if (_recognitionHistory.length > 10) { _recognitionHistory.removeLast(); } // 清空当前文本 _currentText = ''; _updateTextController(); }); } } else { print('📱 [Example] 开始录音'); // 清空之前的结果,开始新的录音 setState(() { _realtimeResults.clear(); _currentText = ''; _textController.clear(); }); await _speechService.startListening(partialResults: true); } } catch (e) { print('📱 [Example] 录音操作失败: $e'); _showErrorSnackBar('录音操作失败: $e'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('YX ASR 语音识别演示'), backgroundColor: Theme.of(context).colorScheme.inversePrimary, actions: [ IconButton( icon: const Icon(Icons.clear_all), onPressed: _clearHistory, tooltip: '清除历史', ), ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ // 状态卡片 _buildStatusCard(), const SizedBox(height: 16), // 识别结果卡片 _buildRecognitionCard(), const SizedBox(height: 16), // 历史记录 Expanded(child: _buildHistoryCard()), ], ), ), floatingActionButton: _buildFloatingActionButton(), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } /// 构建状态卡片 Widget _buildStatusCard() { return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '服务状态', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 12), Row( children: [ Icon( _isInitialized ? Icons.check_circle : Icons.error, color: _isInitialized ? Colors.green : Colors.red, ), const SizedBox(width: 8), Text( _isInitialized ? '已初始化' : '未初始化', style: TextStyle( color: _isInitialized ? Colors.green : Colors.red, fontWeight: FontWeight.w500, ), ), ], ), if (_isInitialized) ...[ const SizedBox(height: 8), Row( children: [ const Icon(Icons.model_training, size: 16, color: Colors.purple), const SizedBox(width: 4), Text( '模型: ${ModelConfig.getModelDescription(ModelConfig.zh2023)}', style: const TextStyle(color: Colors.purple, fontSize: 12), ), ], ), const SizedBox(height: 4), Row( children: [ const Icon(Icons.speed, size: 16, color: Colors.blue), const SizedBox(width: 4), Text( '识别速度: ${_speechService.recognitionSpeed.description}', style: const TextStyle(color: Colors.blue, fontSize: 12), ), ], ), const SizedBox(height: 4), Row( children: [ const Icon(Icons.graphic_eq, size: 16, color: Colors.green), const SizedBox(width: 4), Text( '采样率: ${_speechService.sampleRate.description}', style: const TextStyle(color: Colors.green, fontSize: 12), ), ], ), ], if (_errorMessage.isNotEmpty) ...[ const SizedBox(height: 8), Text( '错误: $_errorMessage', style: const TextStyle(color: Colors.red, fontSize: 12), ), ], ], ), ), ); } /// 构建识别结果卡片(可编辑文本框) Widget _buildRecognitionCard() { return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text( '语音识别', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const Spacer(), if (_isListening) ...[ const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), const SizedBox(width: 8), Text( '正在录音...', style: TextStyle( color: Colors.red[600], fontWeight: FontWeight.w500, ), ), ], ], ), const SizedBox(height: 16), // 可编辑的文本框 Container( width: double.infinity, constraints: const BoxConstraints( minHeight: 120, maxHeight: 300, ), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8.0), border: Border.all( color: _isListening ? Colors.red[300]! : Colors.grey[300]!, width: _isListening ? 2.0 : 1.0, ), ), child: TextField( controller: _textController, focusNode: _textFocusNode, maxLines: null, style: const TextStyle(fontSize: 16), decoration: InputDecoration( hintText: _isListening ? '🎤 正在监听,请说话...\n实时识别结果会显示在这里,您可以随时编辑' : _isInitialized ? '点击麦克风按钮开始录音\n录音结束后可以编辑识别结果' : '正在初始化...', hintStyle: TextStyle( color: Colors.grey[500], fontSize: 14, ), border: InputBorder.none, contentPadding: const EdgeInsets.all(16.0), ), ), ), // 实时状态提示 if (_currentText.isNotEmpty && _isListening) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.blue[200]!), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.mic, size: 14, color: Colors.blue[600]), const SizedBox(width: 4), Text( '实时识别中...', style: TextStyle( fontSize: 12, color: Colors.blue[600], fontWeight: FontWeight.w500, ), ), ], ), ), ], // 操作提示 const SizedBox(height: 12), Text( '💡 提示:录音过程中会实时显示识别结果,录音结束后您可以编辑文本内容', style: TextStyle( fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ], ), ), ); } /// 构建历史记录卡片 Widget _buildHistoryCard() { return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '识别历史', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Expanded( child: _recognitionHistory.isEmpty ? Center( child: Text( '暂无识别历史', style: TextStyle( color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ) : ListView.builder( itemCount: _recognitionHistory.length, itemBuilder: (context, index) { return Card( margin: const EdgeInsets.only(bottom: 8.0), child: ListTile( leading: CircleAvatar( backgroundColor: Theme.of(context).primaryColor, child: Text( '${index + 1}', style: const TextStyle(color: Colors.white), ), ), title: Text(_recognitionHistory[index]), trailing: IconButton( icon: const Icon(Icons.copy), onPressed: () { // TODO: 实现复制到剪贴板功能 ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('已复制到剪贴板'), ), ); }, ), ), ); }, ), ), ], ), ), ); } /// 构建浮动操作按钮 Widget _buildFloatingActionButton() { if (!_isInitialized) { return FloatingActionButton( onPressed: _initializeSpeechService, backgroundColor: Colors.orange, child: const Icon(Icons.refresh), ); } // 使用我们提供的 RecordingButton 组件 return RecordingButton( speechService: _speechService, size: 80, onResult: (result) { print( '📱 [Example] RecordingButton 接收到识别结果: "${result.recognizedWords}"'); setState(() { // 更新当前识别的文本(实时显示) if (result.recognizedWords.isNotEmpty) { print('📱 [Example] 实时识别: ${result.recognizedWords}'); _currentText = result.recognizedWords; // 更新文本框显示 _updateTextController(); } }); }, onError: (error) { setState(() { _errorMessage = error.errorMsg; }); _showErrorSnackBar('识别错误: ${error.errorMsg}'); }, onListeningStatusChanged: (isListening) { setState(() { _isListening = isListening; }); if (!isListening) { // 录音结束后,将当前识别的文本保存到历史记录 if (_currentText.isNotEmpty) { setState(() { _recognitionHistory.insert(0, _currentText); // 限制历史记录数量 if (_recognitionHistory.length > 10) { _recognitionHistory.removeLast(); } }); } } else { // 开始录音时清空之前的结果 setState(() { _realtimeResults.clear(); }); } }, tooltip: _isListening ? '点击停止录音' : '点击开始录音', enabled: _isInitialized, ); } }