voice_recognizer 1.0.1
voice_recognizer: ^1.0.1 copied to clipboard
基于阿里 Paraformer-zh 模型的离线语音识别组件,长按录音交互。
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:voice_recognizer/voice_recognizer.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 加载热词文件用于后处理纠错(Paraformer 模型不支持热词,使用后处理方式)
await GlobalTextCorrector.instance.loadFromAsset('assets/hotWords.json');
/// 慎用:模型加载后大约占用 50-100MB 内存
VoiceRecognizerRegistry.instance.preInitialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '语音识别示例',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const VoiceRecognizerDemoPage(),
);
}
}
/// 语音识别示例页面
///
/// 展示两种使用方式:
/// 1. 使用 VoiceRecordButton 按钮组件
/// 2. 使用 showVoiceRecordOverlay 弹窗
class VoiceRecognizerDemoPage extends StatefulWidget {
const VoiceRecognizerDemoPage({super.key});
@override
State<VoiceRecognizerDemoPage> createState() =>
_VoiceRecognizerDemoPageState();
}
class _VoiceRecognizerDemoPageState extends State<VoiceRecognizerDemoPage> {
final TextEditingController _textController = TextEditingController();
final List<String> _recognitionHistory = [];
String _lastResult = '';
@override
void dispose() {
_textController.dispose();
super.dispose();
}
void _addToHistory(String text) {
if (text.isNotEmpty) {
setState(() {
_lastResult = text;
_recognitionHistory.insert(0, text);
if (_recognitionHistory.length > 10) {
_recognitionHistory.removeLast();
}
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('语音识别示例'),
elevation: 0,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ========== 方式一:按钮组件 ==========
_buildSection(
title: '方式一:VoiceRecordButton 按钮组件',
description: '长按按钮开始录音,松开后自动识别。支持实时显示音量、录音时长、波纹动画等效果。',
child: Center(
child: VoiceRecordButton(
themeColor: Theme.of(context).primaryColor,
size: 72,
maxDuration: 10,
showDuration: true,
showRipple: true,
enableHaptic: true,
onResult: (text) {
_addToHistory(text);
_showSnackBar('识别结果: $text');
},
onPartialResult: (text) {
debugPrint('实时结果: $text');
},
onRecordingStateChanged: (isRecording) {
debugPrint('录音状态: $isRecording');
},
onError: (error) {
_showSnackBar(error, isError: true);
},
),
),
),
const SizedBox(height: 40),
// ========== 方式二:弹窗方式 ==========
_buildSection(
title: '方式二:showVoiceRecordOverlay 弹窗',
description: '点击按钮弹出语音录入对话框,带有完整的 UI 交互和动画效果。',
child: Center(
child: ElevatedButton.icon(
onPressed: _showVoiceOverlay,
icon: const Icon(Icons.mic),
label: const Text('打开语音录入弹窗'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 14,
),
),
),
),
),
const SizedBox(height: 40),
// ========== 方式三:集成到输入框 ==========
_buildSection(
title: '方式三:集成到输入框',
description: '将语音输入功能集成到 TextField 中,点击麦克风图标开始语音输入。',
child: TextField(
controller: _textController,
decoration: InputDecoration(
hintText: '输入文字或点击麦克风语音输入...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
suffixIcon: IconButton(
onPressed: _showVoiceOverlayForInput,
icon: Icon(
Icons.mic,
color: Theme.of(context).primaryColor,
),
tooltip: '语音输入',
),
),
maxLines: 3,
),
),
const SizedBox(height: 40),
// ========== 最近识别结果 ==========
if (_lastResult.isNotEmpty)
_buildSection(
title: '最近识别结果',
description: '',
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).primaryColor.withOpacity(0.3),
),
),
child: Text(
_lastResult,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
if (_recognitionHistory.isNotEmpty) ...[
const SizedBox(height: 24),
_buildSection(
title: '识别历史',
description: '最近 10 条识别记录',
child: Column(
children: _recognitionHistory.asMap().entries.map((entry) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 24,
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey.shade200,
shape: BoxShape.circle,
),
child: Text(
'${entry.key + 1}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
entry.value,
style: const TextStyle(fontSize: 14),
),
),
],
),
);
}).toList(),
),
),
],
const SizedBox(height: 40),
],
),
),
);
}
Widget _buildSection({
required String title,
required String description,
required Widget child,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (description.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
const SizedBox(height: 16),
child,
],
);
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : null,
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 2),
),
);
}
/// 方式二:显示语音录入弹窗
Future<void> _showVoiceOverlay() async {
final result = await showVoiceRecordOverlay(
context: context,
themeColor: Theme.of(context).primaryColor,
maxDuration: 10,
title: '语音输入',
hintText: '长按按钮开始录音...',
);
if (result != null && result.isNotEmpty) {
_addToHistory(result);
_showSnackBar('识别结果: $result');
}
}
/// 方式三:为输入框显示语音录入弹窗
Future<void> _showVoiceOverlayForInput() async {
final result = await showVoiceRecordOverlay(
context: context,
themeColor: Theme.of(context).primaryColor,
maxDuration: 10,
title: '语音输入',
hintText: '请说出您要输入的内容...',
);
if (result != null && result.isNotEmpty) {
setState(() {
// 追加到现有文本
final currentText = _textController.text;
if (currentText.isNotEmpty && !currentText.endsWith(' ')) {
_textController.text = '$currentText $result';
} else {
_textController.text = currentText + result;
}
// 移动光标到末尾
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: _textController.text.length),
);
});
_addToHistory(result);
}
}
}