yx_speech_to_text_flutter/example/lib/main.dart

541 lines
18 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = [];
/// 本次录音开始前的文本内容(用于实时追加)
String _baseText = '';
// 录音相关
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 = '';
_baseText = ''; // 也清空基础文本
_textController.clear();
_errorMessage = '';
});
}
@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),
Flexible(
child: Text(
_currentText.isEmpty ? '实时识别中...' : _currentText,
style: TextStyle(
fontSize: 12,
color: Colors.blue[600],
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
],
// 操作提示
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) {
// 最终结果确认当前文本更新base text为下次录音做准备
print('📱 [Example] 最终识别结果,确认文本: ${result.recognizedWords}');
_baseText = _textController.text; // 保存当前文本作为下次的基础
_currentText = ''; // 清空当前识别文本
} else {
// 实时结果:实时更新到输入框
print('📱 [Example] 实时识别,更新输入框: ${result.recognizedWords}');
_currentText = result.recognizedWords;
// 实时更新输入框内容 = 基础文本 + 当前识别文本
String newText = _baseText;
if (newText.isNotEmpty && !newText.endsWith(' ') && _currentText.isNotEmpty) {
newText += ' '; // 添加空格分隔
}
newText += _currentText;
_textController.text = newText;
// 将光标移到最后
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: newText.length),
);
}
}
});
},
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();
}
});
}
// 注意不在这里清空_currentText因为最终结果会处理
} else {
// 开始录音时记录当前文本作为基础,清空当前识别文本
setState(() {
_baseText = _textController.text; // 记录录音开始前的文本
_realtimeResults.clear();
_currentText = ''; // 清空当前识别文本
});
}
},
tooltip: _isListening ? '点击停止录音' : '点击开始录音',
enabled: _isInitialized,
);
}
}