You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
476 lines
19 KiB
476 lines
19 KiB
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import '../controllers/flutter_asr_test_controller.dart';
|
|
import '../../../data/services/flutter_asr_service.dart';
|
|
|
|
/// Flutter ASR测试视图,演示如何使用FlutterAsrService
|
|
class FlutterAsrTestView extends GetView<FlutterAsrTestController> {
|
|
const FlutterAsrTestView({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Flutter ASR Test'),
|
|
centerTitle: true,
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// 语音识别控制卡片
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Speech Recognition Controls',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
// 添加连续识别模式开关
|
|
Obx(() {
|
|
final isContinuous = controller.continuousMode;
|
|
return SwitchListTile(
|
|
title: const Text('连续识别模式'),
|
|
subtitle: Text(
|
|
isContinuous
|
|
? '已开启:识别将自动连续进行'
|
|
: '已关闭:识别完成后需手动重新开始',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
value: isContinuous,
|
|
onChanged: controller.isAvailable
|
|
? (value) => controller.toggleContinuousMode()
|
|
: null,
|
|
activeColor: Colors.blue,
|
|
dense: true,
|
|
);
|
|
}),
|
|
const Divider(height: 24),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
Obx(() {
|
|
final isListening = controller.isListening;
|
|
return ElevatedButton.icon(
|
|
onPressed: isListening || !controller.isAvailable
|
|
? null
|
|
: () => controller.startListening(),
|
|
icon: isListening
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.mic),
|
|
label: Text(isListening ? 'Listening...' : 'Start Listening'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
);
|
|
}),
|
|
Obx(() {
|
|
final isActive = controller.isListening;
|
|
return ElevatedButton.icon(
|
|
onPressed: isActive
|
|
? () => controller.stopListening()
|
|
: null,
|
|
icon: const Icon(Icons.stop),
|
|
label: const Text('Stop'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// 语言选择卡片
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Language Selection',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Obx(() {
|
|
final locales = controller.locales;
|
|
final currentLocale = controller.currentLocale;
|
|
|
|
if (locales.isEmpty) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Text(
|
|
'No languages available. Please check ASR settings on your device.',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return DropdownButtonFormField<String>(
|
|
decoration: const InputDecoration(
|
|
labelText: 'Select Language',
|
|
border: OutlineInputBorder(),
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
),
|
|
value: currentLocale.isNotEmpty ? currentLocale : null,
|
|
items: locales.map((locale) {
|
|
final name = locale.name ?? 'Unknown';
|
|
final id = locale.localeId ?? '';
|
|
return DropdownMenuItem<String>(
|
|
value: id,
|
|
child: Text('$name (${id.isNotEmpty ? id : 'Unknown'})'),
|
|
);
|
|
}).toList(),
|
|
onChanged: controller.isListening
|
|
? null
|
|
: (value) {
|
|
if (value != null) {
|
|
controller.setLocale(value);
|
|
}
|
|
},
|
|
isExpanded: true,
|
|
hint: const Text('Select a language'),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// 当前识别结果卡片
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Current Recognition',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Obx(() {
|
|
final text = controller.recognizedText;
|
|
final confidence = controller.confidence;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
),
|
|
width: double.infinity,
|
|
child: Text(
|
|
text.isEmpty ? 'Speak to see recognition results here' : text,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontStyle: text.isEmpty ? FontStyle.italic : FontStyle.normal,
|
|
color: text.isEmpty ? Colors.grey : Colors.black,
|
|
),
|
|
),
|
|
),
|
|
|
|
if (confidence > 0)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Text(
|
|
'Confidence: ${(confidence * 100).toStringAsFixed(1)}%',
|
|
style: TextStyle(
|
|
color: _getConfidenceColor(confidence),
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// 历史记录卡片
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Recognition History',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Obx(() {
|
|
final hasHistory = controller.history.isNotEmpty;
|
|
return TextButton.icon(
|
|
onPressed: hasHistory ? () => controller.clearHistory() : null,
|
|
icon: const Icon(Icons.clear_all, size: 18),
|
|
label: const Text('Clear'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Colors.red,
|
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
|
|
Obx(() {
|
|
final history = controller.history;
|
|
|
|
if (history.isEmpty) {
|
|
return const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 16.0),
|
|
child: Center(
|
|
child: Text(
|
|
'No recognition history yet',
|
|
style: TextStyle(
|
|
fontStyle: FontStyle.italic,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: history.length,
|
|
separatorBuilder: (context, index) => const Divider(height: 1),
|
|
itemBuilder: (context, index) {
|
|
final text = history[index];
|
|
|
|
return ListTile(
|
|
title: Text(text),
|
|
leading: CircleAvatar(
|
|
backgroundColor: Colors.blue.shade100,
|
|
radius: 18,
|
|
child: Text(
|
|
'${index + 1}',
|
|
style: const TextStyle(
|
|
color: Colors.blue,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
dense: true,
|
|
);
|
|
},
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// 状态显示卡片
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'Status',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Obx(() {
|
|
final state = controller.asrState;
|
|
final stateText = _getAsrStateText(state);
|
|
final stateColor = _getAsrStateColor(state);
|
|
final isContinuous = controller.continuousMode;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: stateColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: stateColor.withOpacity(0.3)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
_getAsrStateIcon(state),
|
|
color: stateColor,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Current Status: $stateText',
|
|
style: TextStyle(
|
|
color: stateColor,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
if (isContinuous)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 8.0),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(4),
|
|
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
|
),
|
|
child: Row(
|
|
children: const [
|
|
Icon(
|
|
Icons.repeat,
|
|
color: Colors.blue,
|
|
size: 16,
|
|
),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'连续识别模式已开启',
|
|
style: TextStyle(
|
|
color: Colors.blue,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 获取ASR状态文本
|
|
String _getAsrStateText(AsrState state) {
|
|
switch (state) {
|
|
case AsrState.notInitialized:
|
|
return 'Not Initialized';
|
|
case AsrState.initialized:
|
|
return 'Initialized';
|
|
case AsrState.listening:
|
|
return 'Listening';
|
|
case AsrState.notListening:
|
|
return 'Not Listening';
|
|
case AsrState.error:
|
|
return 'Error';
|
|
default:
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
// 获取ASR状态颜色
|
|
Color _getAsrStateColor(AsrState state) {
|
|
switch (state) {
|
|
case AsrState.notInitialized:
|
|
return Colors.grey;
|
|
case AsrState.initialized:
|
|
return Colors.blue;
|
|
case AsrState.listening:
|
|
return Colors.green;
|
|
case AsrState.notListening:
|
|
return Colors.orange;
|
|
case AsrState.error:
|
|
return Colors.red;
|
|
default:
|
|
return Colors.grey;
|
|
}
|
|
}
|
|
|
|
// 获取ASR状态图标
|
|
IconData _getAsrStateIcon(AsrState state) {
|
|
switch (state) {
|
|
case AsrState.notInitialized:
|
|
return Icons.hourglass_empty;
|
|
case AsrState.initialized:
|
|
return Icons.check_circle;
|
|
case AsrState.listening:
|
|
return Icons.mic;
|
|
case AsrState.notListening:
|
|
return Icons.mic_off;
|
|
case AsrState.error:
|
|
return Icons.error;
|
|
default:
|
|
return Icons.help_outline;
|
|
}
|
|
}
|
|
|
|
// 获取置信度颜色
|
|
Color _getConfidenceColor(double confidence) {
|
|
if (confidence >= 0.8) {
|
|
return Colors.green;
|
|
} else if (confidence >= 0.6) {
|
|
return Colors.orange;
|
|
} else {
|
|
return Colors.red;
|
|
}
|
|
}
|
|
}
|