yx_speech_to_text_flutter/example/lib/main.dart

589 lines
19 KiB
Dart

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: false, // 暂时禁用Material 3以避免着色器编译问题
),
home: const SpeechRecognitionPage(),
);
}
}
/// 语音识别演示页面
class SpeechRecognitionPage extends StatefulWidget {
const SpeechRecognitionPage({super.key});
@override
State<SpeechRecognitionPage> createState() => _SpeechRecognitionPageState();
}
class _SpeechRecognitionPageState extends State<SpeechRecognitionPage> {
final YxAsrService _speechService = YxAsrService();
final TextEditingController _textController = TextEditingController();
final FocusNode _textFocusNode = FocusNode();
// 状态变量
bool _isInitialized = false;
bool _isListening = false;
String _currentText = '';
String _errorMessage = '';
List<String> _recognitionHistory = [];
// 录音相关
final List<String> _realtimeResults = []; // 存储实时识别片段
@override
void initState() {
super.initState();
_initializeSpeechService();
}
@override
void dispose() {
_textController.dispose();
_textFocusNode.dispose();
_speechService.dispose();
super.dispose();
}
/// 初始化语音识别服务
Future<void> _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;
}
});
});
// 监听错误
_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<void> _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) {
if (result.finalResult) {
// 最终结果:追加到文本框
print('📱 [Example] 最终识别结果,追加到文本框: ${result.recognizedWords}');
String currentTextInBox = _textController.text;
if (currentTextInBox.isNotEmpty &&
!currentTextInBox.endsWith(' ')) {
currentTextInBox += ' '; // 添加空格分隔
}
_textController.text = currentTextInBox + result.recognizedWords;
_currentText = result.recognizedWords; // 保存当前识别结果
} else {
// 实时结果:仅用于显示,不追加到文本框
print('📱 [Example] 实时识别: ${result.recognizedWords}');
_currentText = result.recognizedWords;
}
}
});
},
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();
_currentText = ''; // 清空当前识别文本
});
}
},
tooltip: _isListening ? '点击停止录音' : '点击开始录音',
enabled: _isInitialized,
);
}
}