yx_speech_to_text_flutter/example/lib/main.dart

570 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: true,
),
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,
),
);
// 初始化服务 - 使用2025年最新模型推荐
final success =
await _speechService.initializeWithDefaultModel(ModelConfig.zh2025);
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<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),
);
}
return GestureDetector(
onTap: () {
// 触觉反馈
HapticFeedback.mediumImpact();
_toggleRecording();
},
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: _isListening
? [Colors.red.shade400, Colors.red.shade600]
: [Colors.blue.shade400, Colors.blue.shade600],
),
boxShadow: [
BoxShadow(
color: (_isListening ? Colors.red : Colors.blue)
.withValues(alpha: 0.4),
spreadRadius: 4,
blurRadius: 12,
offset: const Offset(0, 6),
),
],
),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Icon(
_isListening ? Icons.stop_rounded : Icons.mic_rounded,
key: ValueKey(_isListening),
size: 32,
color: Colors.white,
),
),
),
);
}
}