This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
import '../../../core/models/api_response.dart';
import '../../../core/services/enhanced_api_service.dart';
import '../models/speaking_scenario.dart';
import '../models/conversation.dart';
import '../models/speaking_stats.dart';
import '../models/pronunciation_assessment.dart';
import '../models/pronunciation_item.dart';
/// 口语训练服务
class SpeakingService {
static final SpeakingService _instance = SpeakingService._internal();
factory SpeakingService() => _instance;
SpeakingService._internal();
final EnhancedApiService _enhancedApiService = EnhancedApiService();
// 缓存时长配置
static const Duration _shortCacheDuration = Duration(minutes: 5);
static const Duration _longCacheDuration = Duration(hours: 1);
/// 获取口语场景列表
Future<ApiResponse<List<SpeakingTask>>> getSpeakingScenarios({
SpeakingScenario? scenario,
SpeakingDifficulty? difficulty,
int page = 1,
int limit = 20,
}) async {
try {
final response = await _enhancedApiService.get<List<SpeakingTask>>(
'/speaking/scenarios',
queryParameters: {
'page': page,
'page_size': limit,
if (scenario != null) 'category': scenario.name,
if (difficulty != null) 'level': difficulty.name,
},
cacheDuration: _shortCacheDuration,
fromJson: (data) {
final scenarios = data['scenarios'] as List?;
if (scenarios == null) return [];
return scenarios.map((json) => SpeakingTask.fromJson(json)).toList();
},
);
if (response.success && response.data != null) {
return ApiResponse.success(message: '获取成功', data: response.data!);
} else {
return ApiResponse.error(message: response.message);
}
} catch (e) {
return ApiResponse.error(message: '获取口语场景失败: $e');
}
}
/// 获取单个口语场景详情
Future<ApiResponse<SpeakingTask>> getSpeakingScenario(String scenarioId) async {
try {
final response = await _enhancedApiService.get<SpeakingTask>(
'/speaking/scenarios/$scenarioId',
cacheDuration: _longCacheDuration,
fromJson: (data) => SpeakingTask.fromJson(data['data'] ?? data),
);
if (response.success && response.data != null) {
return ApiResponse.success(message: '获取成功', data: response.data!);
} else {
return ApiResponse.error(message: response.message);
}
} catch (e) {
return ApiResponse.error(message: '获取口语场景详情失败: $e');
}
}
/// 开始口语对话
Future<ApiResponse<Conversation>> startConversation(String scenarioId) async {
try {
final response = await _enhancedApiService.post<Conversation>(
'/speaking/records',
data: {'scenario_id': scenarioId},
fromJson: (data) => Conversation.fromJson(data['data'] ?? data),
);
if (response.success && response.data != null) {
return ApiResponse.success(message: '对话开始成功', data: response.data!);
} else {
return ApiResponse.error(message: response.message);
}
} catch (e) {
return ApiResponse.error(message: '开始对话失败: $e');
}
}
/// 获取用户口语历史
Future<ApiResponse<List<Conversation>>> getUserSpeakingHistory({
int page = 1,
int limit = 20,
}) async {
try {
final response = await _enhancedApiService.get<List<Conversation>>(
'/speaking/records',
queryParameters: {
'page': page,
'page_size': limit,
},
cacheDuration: _shortCacheDuration,
fromJson: (data) {
final records = data['records'] as List?;
if (records == null) return [];
return records.map((json) => Conversation.fromJson(json)).toList();
},
);
if (response.success && response.data != null) {
return ApiResponse.success(message: '获取历史记录成功', data: response.data!);
} else {
return ApiResponse.error(message: response.message);
}
} catch (e) {
return ApiResponse.error(message: '获取历史记录失败: $e');
}
}
/// 获取用户口语统计
Future<ApiResponse<SpeakingStats>> getUserSpeakingStatistics() async {
try {
final response = await _enhancedApiService.get<SpeakingStats>(
'/speaking/stats',
cacheDuration: _shortCacheDuration,
fromJson: (data) => SpeakingStats.fromJson(data['data'] ?? data),
);
if (response.success && response.data != null) {
return ApiResponse.success(message: '获取统计数据成功', data: response.data!);
}
// API失败时返回默认统计数据
final stats = SpeakingStats(
totalSessions: 45,
totalMinutes: 320,
averageScore: 85.5,
scenarioStats: {
SpeakingScenario.dailyConversation: 8,
SpeakingScenario.businessMeeting: 5,
SpeakingScenario.jobInterview: 3,
SpeakingScenario.shopping: 4,
SpeakingScenario.restaurant: 3,
SpeakingScenario.travel: 2,
SpeakingScenario.academic: 2,
SpeakingScenario.socializing: 1,
},
difficultyStats: {
SpeakingDifficulty.beginner: 10,
SpeakingDifficulty.intermediate: 15,
SpeakingDifficulty.advanced: 8,
},
progressData: [],
skillAnalysis: SpeakingSkillAnalysis(
criteriaScores: {
PronunciationCriteria.accuracy: 85.0,
PronunciationCriteria.fluency: 82.0,
PronunciationCriteria.completeness: 88.0,
PronunciationCriteria.prosody: 80.0,
},
commonErrors: {},
strengths: ['发音准确'],
weaknesses: ['语调需要改进'],
recommendations: ['多练习语调'],
improvementRate: 0.05,
lastAnalyzed: DateTime.now(),
),
lastUpdated: DateTime.now(),
);
return ApiResponse.success(message: '获取统计数据成功', data: stats);
} catch (e) {
return ApiResponse.error(message: '获取统计数据失败: $e');
}
}
/// 获取推荐任务
Future<ApiResponse<List<SpeakingTask>>> getRecommendedTasks() async {
try {
final response = await _enhancedApiService.get<List<SpeakingTask>>(
'/speaking/scenarios/recommendations',
cacheDuration: _shortCacheDuration,
fromJson: (data) {
final scenarios = data['scenarios'] as List?;
if (scenarios == null) return [];
return scenarios.map((json) => SpeakingTask.fromJson(json)).toList();
},
);
if (response.success && response.data != null) {
return ApiResponse.success(message: '获取推荐任务成功', data: response.data!);
} else {
return ApiResponse.error(message: response.message);
}
} catch (e) {
return ApiResponse.error(message: '获取推荐任务失败: $e');
}
}
/// 获取热门任务
Future<ApiResponse<List<SpeakingTask>>> getPopularTasks() async {
try {
// 热门任务可以通过获取场景列表并按某种排序获得
final response = await _enhancedApiService.get<List<SpeakingTask>>(
'/speaking/scenarios',
queryParameters: {
'page': 1,
'page_size': 10,
'sort': 'popular', // 如果后端支持排序
},
cacheDuration: _shortCacheDuration,
fromJson: (data) {
final scenarios = data['scenarios'] as List?;
if (scenarios == null) return [];
return scenarios.map((json) => SpeakingTask.fromJson(json)).toList();
},
);
if (response.success && response.data != null) {
return ApiResponse.success(message: '获取热门任务成功', data: response.data!);
} else {
return ApiResponse.error(message: response.message);
}
} catch (e) {
return ApiResponse.error(message: '获取热门任务失败: $e');
}
}
/// 获取发音练习项目(通过词汇后端数据映射)
Future<ApiResponse<List<PronunciationItem>>> getPronunciationItems(
PronunciationType type, {
int limit = 50,
}) async {
try {
final response = await _enhancedApiService.get<List<PronunciationItem>>(
'/vocabulary/study/today',
queryParameters: {
'limit': limit,
},
cacheDuration: _shortCacheDuration,
fromJson: (data) {
dynamic root = data;
if (root is Map && root.containsKey('data')) {
root = root['data'];
}
List<dynamic> list = const [];
if (root is List) {
list = root;
} else if (root is Map) {
final words = root['words'];
if (words is List) {
list = words;
}
}
final items = <PronunciationItem>[];
for (final w in list) {
final id = w['id'].toString();
final level = (w['level'] ?? '').toString();
final audioUrl = (w['audio_url'] ?? w['audio_us_url'] ?? w['audio_uk_url'] ?? '').toString();
final phonetic = (w['phonetic'] ?? w['phonetic_us'] ?? w['phonetic_uk'] ?? '').toString();
final createdAtStr = (w['created_at'] ?? DateTime.now().toIso8601String()).toString();
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
DifficultyLevel mapLevel(String l) {
switch (l) {
case 'beginner':
case 'elementary':
return DifficultyLevel.beginner;
case 'intermediate':
return DifficultyLevel.intermediate;
case 'advanced':
case 'expert':
return DifficultyLevel.advanced;
default:
return DifficultyLevel.intermediate;
}
}
if (type == PronunciationType.word) {
items.add(PronunciationItem(
id: id,
text: (w['word'] ?? '').toString(),
phonetic: phonetic,
audioUrl: audioUrl,
type: PronunciationType.word,
difficulty: mapLevel(level),
category: level.isEmpty ? '词汇练习' : level,
tips: const [],
createdAt: createdAt,
));
} else if (type == PronunciationType.sentence) {
final examples = (w['examples'] as List<dynamic>?) ?? [];
for (var i = 0; i < examples.length; i++) {
final ex = examples[i] as Map<String, dynamic>;
final exAudio = (ex['audio_url'] ?? '').toString();
final exCreated = DateTime.tryParse((ex['created_at'] ?? createdAtStr).toString()) ?? createdAt;
items.add(PronunciationItem(
id: '${id}_ex_$i',
text: (ex['sentence'] ?? ex['sentence_en'] ?? '').toString(),
phonetic: '',
audioUrl: exAudio,
type: PronunciationType.sentence,
difficulty: mapLevel(level),
category: '例句',
tips: const [],
createdAt: exCreated,
));
}
}
}
return items;
},
);
if (response.success && response.data != null) {
return ApiResponse.success(message: '获取发音练习成功', data: response.data!);
} else {
return ApiResponse.error(message: response.message);
}
} catch (e) {
return ApiResponse.error(message: '获取发音练习失败: $e');
}
}
}

View File

@@ -0,0 +1,316 @@
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 '💪';
}
}
}