316 lines
9.0 KiB
Dart
316 lines
9.0 KiB
Dart
|
|
import 'dart:async';
|
|||
|
|
import 'dart:math';
|
|||
|
|
import '../models/pronunciation_item.dart';
|
|||
|
|
|
|||
|
|
/// 语音识别和评分服务
|
|||
|
|
class SpeechRecognitionService {
|
|||
|
|
static final SpeechRecognitionService _instance = SpeechRecognitionService._internal();
|
|||
|
|
factory SpeechRecognitionService() => _instance;
|
|||
|
|
SpeechRecognitionService._internal();
|
|||
|
|
|
|||
|
|
bool _isListening = false;
|
|||
|
|
StreamController<double>? _volumeController;
|
|||
|
|
StreamController<String>? _recognitionController;
|
|||
|
|
|
|||
|
|
/// 获取音量流
|
|||
|
|
Stream<double> get volumeStream => _volumeController?.stream ?? const Stream.empty();
|
|||
|
|
|
|||
|
|
/// 获取识别结果流
|
|||
|
|
Stream<String> get recognitionStream => _recognitionController?.stream ?? const Stream.empty();
|
|||
|
|
|
|||
|
|
/// 是否正在监听
|
|||
|
|
bool get isListening => _isListening;
|
|||
|
|
|
|||
|
|
/// 开始语音识别
|
|||
|
|
Future<bool> startListening() async {
|
|||
|
|
if (_isListening) return false;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
_isListening = true;
|
|||
|
|
_volumeController = StreamController<double>.broadcast();
|
|||
|
|
_recognitionController = StreamController<String>.broadcast();
|
|||
|
|
|
|||
|
|
// TODO: 实现真实的语音识别
|
|||
|
|
// 这里应该调用语音识别API,如Google Speech-to-Text、百度语音识别等
|
|||
|
|
|
|||
|
|
// 模拟音量变化
|
|||
|
|
_simulateVolumeChanges();
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
} catch (e) {
|
|||
|
|
_isListening = false;
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 停止语音识别
|
|||
|
|
Future<void> stopListening() async {
|
|||
|
|
if (!_isListening) return;
|
|||
|
|
|
|||
|
|
_isListening = false;
|
|||
|
|
await _volumeController?.close();
|
|||
|
|
await _recognitionController?.close();
|
|||
|
|
_volumeController = null;
|
|||
|
|
_recognitionController = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 分析发音并评分
|
|||
|
|
Future<PronunciationResult> analyzePronunciation(
|
|||
|
|
String recordedText,
|
|||
|
|
PronunciationItem targetItem,
|
|||
|
|
) async {
|
|||
|
|
// TODO: 实现真实的发音分析
|
|||
|
|
// 这里应该调用发音评估API,如科大讯飞语音评测、腾讯云语音评测等
|
|||
|
|
|
|||
|
|
// 模拟分析过程
|
|||
|
|
await Future.delayed(const Duration(milliseconds: 1500));
|
|||
|
|
|
|||
|
|
return _simulateAnalysis(recordedText, targetItem);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 模拟音量变化
|
|||
|
|
void _simulateVolumeChanges() {
|
|||
|
|
Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
|||
|
|
if (!_isListening) {
|
|||
|
|
timer.cancel();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成随机音量值
|
|||
|
|
final volume = Random().nextDouble() * 0.8 + 0.1;
|
|||
|
|
_volumeController?.add(volume);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 模拟发音分析
|
|||
|
|
PronunciationResult _simulateAnalysis(String recordedText, PronunciationItem targetItem) {
|
|||
|
|
final random = Random();
|
|||
|
|
|
|||
|
|
// 基础分数
|
|||
|
|
double baseScore = 60.0 + random.nextDouble() * 30;
|
|||
|
|
|
|||
|
|
// 根据文本相似度调整分数
|
|||
|
|
final similarity = _calculateSimilarity(recordedText.toLowerCase(), targetItem.text.toLowerCase());
|
|||
|
|
final adjustedScore = baseScore + (similarity * 20);
|
|||
|
|
|
|||
|
|
// 确保分数在合理范围内
|
|||
|
|
final finalScore = adjustedScore.clamp(0.0, 100.0);
|
|||
|
|
|
|||
|
|
return PronunciationResult(
|
|||
|
|
score: finalScore,
|
|||
|
|
accuracy: _getAccuracyLevel(finalScore),
|
|||
|
|
feedback: _generateFeedback(finalScore, targetItem),
|
|||
|
|
detailedAnalysis: _generateDetailedAnalysis(finalScore, targetItem),
|
|||
|
|
suggestions: _generateSuggestions(finalScore, targetItem),
|
|||
|
|
recognizedText: recordedText,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 计算文本相似度
|
|||
|
|
double _calculateSimilarity(String text1, String text2) {
|
|||
|
|
if (text1 == text2) return 1.0;
|
|||
|
|
|
|||
|
|
final words1 = text1.split(' ');
|
|||
|
|
final words2 = text2.split(' ');
|
|||
|
|
|
|||
|
|
int matches = 0;
|
|||
|
|
final maxLength = max(words1.length, words2.length);
|
|||
|
|
|
|||
|
|
for (int i = 0; i < min(words1.length, words2.length); i++) {
|
|||
|
|
if (words1[i] == words2[i]) {
|
|||
|
|
matches++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return matches / maxLength;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取准确度等级
|
|||
|
|
AccuracyLevel _getAccuracyLevel(double score) {
|
|||
|
|
if (score >= 90) return AccuracyLevel.excellent;
|
|||
|
|
if (score >= 80) return AccuracyLevel.good;
|
|||
|
|
if (score >= 70) return AccuracyLevel.fair;
|
|||
|
|
if (score >= 60) return AccuracyLevel.needsImprovement;
|
|||
|
|
return AccuracyLevel.poor;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 生成反馈
|
|||
|
|
String _generateFeedback(double score, PronunciationItem item) {
|
|||
|
|
if (score >= 90) {
|
|||
|
|
return '发音非常标准!语调和节奏都很自然。';
|
|||
|
|
} else if (score >= 80) {
|
|||
|
|
return '发音很好,只需要在个别音素上稍作调整。';
|
|||
|
|
} else if (score >= 70) {
|
|||
|
|
return '发音基本正确,建议多练习重音和语调。';
|
|||
|
|
} else if (score >= 60) {
|
|||
|
|
return '发音需要改进,请注意音素的准确性。';
|
|||
|
|
} else {
|
|||
|
|
return '发音需要大幅改进,建议从基础音素开始练习。';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 生成详细分析
|
|||
|
|
Map<String, double> _generateDetailedAnalysis(double score, PronunciationItem item) {
|
|||
|
|
final random = Random();
|
|||
|
|
final baseVariation = (score - 70) / 30; // 基于总分的变化
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
'音素准确度': (70 + baseVariation * 20 + random.nextDouble() * 10).clamp(0, 100),
|
|||
|
|
'语调自然度': (65 + baseVariation * 25 + random.nextDouble() * 15).clamp(0, 100),
|
|||
|
|
'语速适中度': (75 + baseVariation * 15 + random.nextDouble() * 10).clamp(0, 100),
|
|||
|
|
'重音正确度': (70 + baseVariation * 20 + random.nextDouble() * 10).clamp(0, 100),
|
|||
|
|
'流畅度': (60 + baseVariation * 30 + random.nextDouble() * 15).clamp(0, 100),
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 生成改进建议
|
|||
|
|
List<String> _generateSuggestions(double score, PronunciationItem item) {
|
|||
|
|
List<String> suggestions = [];
|
|||
|
|
|
|||
|
|
if (score < 80) {
|
|||
|
|
suggestions.addAll([
|
|||
|
|
'多听标准发音,模仿语调变化',
|
|||
|
|
'注意重音位置,突出重读音节',
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (score < 70) {
|
|||
|
|
suggestions.addAll([
|
|||
|
|
'放慢语速,确保每个音素清晰',
|
|||
|
|
'练习困难音素的发音方法',
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (score < 60) {
|
|||
|
|
suggestions.addAll([
|
|||
|
|
'从基础音标开始系统学习',
|
|||
|
|
'使用镜子观察口型变化',
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据练习类型添加特定建议
|
|||
|
|
switch (item.type) {
|
|||
|
|
case PronunciationType.word:
|
|||
|
|
suggestions.add('分解单词,逐个音节练习');
|
|||
|
|
break;
|
|||
|
|
case PronunciationType.sentence:
|
|||
|
|
suggestions.add('注意句子的语调起伏和停顿');
|
|||
|
|
break;
|
|||
|
|
case PronunciationType.phrase:
|
|||
|
|
suggestions.add('练习短语内部的连读现象');
|
|||
|
|
break;
|
|||
|
|
case PronunciationType.phoneme:
|
|||
|
|
suggestions.add('重点练习该音素的舌位和口型');
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return suggestions;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 获取发音提示
|
|||
|
|
List<String> getPronunciationTips(PronunciationItem item) {
|
|||
|
|
return item.tips;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 播放标准发音
|
|||
|
|
Future<void> playStandardAudio(String audioUrl) async {
|
|||
|
|
// TODO: 实现音频播放功能
|
|||
|
|
// 这里应该使用音频播放插件,如audioplayers
|
|||
|
|
|
|||
|
|
// 模拟播放时间
|
|||
|
|
await Future.delayed(const Duration(seconds: 2));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 检查麦克风权限
|
|||
|
|
Future<bool> checkMicrophonePermission() async {
|
|||
|
|
// TODO: 实现权限检查
|
|||
|
|
// 这里应该使用权限插件,如permission_handler
|
|||
|
|
|
|||
|
|
// 模拟权限检查
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 请求麦克风权限
|
|||
|
|
Future<bool> requestMicrophonePermission() async {
|
|||
|
|
// TODO: 实现权限请求
|
|||
|
|
// 这里应该使用权限插件,如permission_handler
|
|||
|
|
|
|||
|
|
// 模拟权限请求
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 发音分析结果
|
|||
|
|
class PronunciationResult {
|
|||
|
|
final double score;
|
|||
|
|
final AccuracyLevel accuracy;
|
|||
|
|
final String feedback;
|
|||
|
|
final Map<String, double> detailedAnalysis;
|
|||
|
|
final List<String> suggestions;
|
|||
|
|
final String recognizedText;
|
|||
|
|
|
|||
|
|
PronunciationResult({
|
|||
|
|
required this.score,
|
|||
|
|
required this.accuracy,
|
|||
|
|
required this.feedback,
|
|||
|
|
required this.detailedAnalysis,
|
|||
|
|
required this.suggestions,
|
|||
|
|
required this.recognizedText,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 准确度等级
|
|||
|
|
enum AccuracyLevel {
|
|||
|
|
excellent,
|
|||
|
|
good,
|
|||
|
|
fair,
|
|||
|
|
needsImprovement,
|
|||
|
|
poor,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
extension AccuracyLevelExtension on AccuracyLevel {
|
|||
|
|
String get displayName {
|
|||
|
|
switch (this) {
|
|||
|
|
case AccuracyLevel.excellent:
|
|||
|
|
return '优秀';
|
|||
|
|
case AccuracyLevel.good:
|
|||
|
|
return '良好';
|
|||
|
|
case AccuracyLevel.fair:
|
|||
|
|
return '一般';
|
|||
|
|
case AccuracyLevel.needsImprovement:
|
|||
|
|
return '需要改进';
|
|||
|
|
case AccuracyLevel.poor:
|
|||
|
|
return '较差';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
String get description {
|
|||
|
|
switch (this) {
|
|||
|
|
case AccuracyLevel.excellent:
|
|||
|
|
return '发音非常标准,语调自然';
|
|||
|
|
case AccuracyLevel.good:
|
|||
|
|
return '发音准确,略有改进空间';
|
|||
|
|
case AccuracyLevel.fair:
|
|||
|
|
return '发音基本正确,需要练习';
|
|||
|
|
case AccuracyLevel.needsImprovement:
|
|||
|
|
return '发音有明显问题,需要改进';
|
|||
|
|
case AccuracyLevel.poor:
|
|||
|
|
return '发音问题较多,需要系统练习';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
String get emoji {
|
|||
|
|
switch (this) {
|
|||
|
|
case AccuracyLevel.excellent:
|
|||
|
|
return '🌟';
|
|||
|
|
case AccuracyLevel.good:
|
|||
|
|
return '👍';
|
|||
|
|
case AccuracyLevel.fair:
|
|||
|
|
return '👌';
|
|||
|
|
case AccuracyLevel.needsImprovement:
|
|||
|
|
return '📈';
|
|||
|
|
case AccuracyLevel.poor:
|
|||
|
|
return '💪';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|