541 lines
18 KiB
Dart
541 lines
18 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 = [];
|
||
|
||
/// 本次录音开始前的文本内容(用于实时追加)
|
||
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,
|
||
);
|
||
}
|
||
}
|