init
This commit is contained in:
150
client/lib/features/speaking/models/ai_tutor.dart
Normal file
150
client/lib/features/speaking/models/ai_tutor.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// AI导师类型枚举
|
||||
enum TutorType {
|
||||
business,
|
||||
daily,
|
||||
travel,
|
||||
academic,
|
||||
}
|
||||
|
||||
/// AI导师扩展方法
|
||||
extension TutorTypeExtension on TutorType {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case TutorType.business:
|
||||
return '商务导师';
|
||||
case TutorType.daily:
|
||||
return '日常导师';
|
||||
case TutorType.travel:
|
||||
return '旅行导师';
|
||||
case TutorType.academic:
|
||||
return '学术导师';
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
switch (this) {
|
||||
case TutorType.business:
|
||||
return '专业商务场景';
|
||||
case TutorType.daily:
|
||||
return '生活场景对话';
|
||||
case TutorType.travel:
|
||||
return '旅游场景专训';
|
||||
case TutorType.academic:
|
||||
return '学术讨论演讲';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case TutorType.business:
|
||||
return Icons.business_center;
|
||||
case TutorType.daily:
|
||||
return Icons.chat;
|
||||
case TutorType.travel:
|
||||
return Icons.flight;
|
||||
case TutorType.academic:
|
||||
return Icons.school;
|
||||
}
|
||||
}
|
||||
|
||||
Color get color {
|
||||
switch (this) {
|
||||
case TutorType.business:
|
||||
return Colors.blue;
|
||||
case TutorType.daily:
|
||||
return Colors.green;
|
||||
case TutorType.travel:
|
||||
return Colors.orange;
|
||||
case TutorType.academic:
|
||||
return Colors.purple;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// AI导师模型
|
||||
class AITutor {
|
||||
final String id;
|
||||
final TutorType type;
|
||||
final String name;
|
||||
final String avatar;
|
||||
final String introduction;
|
||||
final List<String> specialties;
|
||||
final List<String> sampleQuestions;
|
||||
final String personality;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const AITutor({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.avatar,
|
||||
required this.introduction,
|
||||
required this.specialties,
|
||||
required this.sampleQuestions,
|
||||
required this.personality,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory AITutor.fromJson(Map<String, dynamic> json) {
|
||||
return AITutor(
|
||||
id: json['id'] as String,
|
||||
type: TutorType.values.firstWhere(
|
||||
(e) => e.name == json['type'],
|
||||
orElse: () => TutorType.daily,
|
||||
),
|
||||
name: json['name'] as String,
|
||||
avatar: json['avatar'] as String,
|
||||
introduction: json['introduction'] as String,
|
||||
specialties: List<String>.from(json['specialties'] as List),
|
||||
sampleQuestions: List<String>.from(json['sampleQuestions'] as List),
|
||||
personality: json['personality'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type.name,
|
||||
'name': name,
|
||||
'avatar': avatar,
|
||||
'introduction': introduction,
|
||||
'specialties': specialties,
|
||||
'sampleQuestions': sampleQuestions,
|
||||
'personality': personality,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
AITutor copyWith({
|
||||
String? id,
|
||||
TutorType? type,
|
||||
String? name,
|
||||
String? avatar,
|
||||
String? introduction,
|
||||
List<String>? specialties,
|
||||
List<String>? sampleQuestions,
|
||||
String? personality,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return AITutor(
|
||||
id: id ?? this.id,
|
||||
type: type ?? this.type,
|
||||
name: name ?? this.name,
|
||||
avatar: avatar ?? this.avatar,
|
||||
introduction: introduction ?? this.introduction,
|
||||
specialties: specialties ?? this.specialties,
|
||||
sampleQuestions: sampleQuestions ?? this.sampleQuestions,
|
||||
personality: personality ?? this.personality,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
155
client/lib/features/speaking/models/conversation.dart
Normal file
155
client/lib/features/speaking/models/conversation.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
enum MessageType {
|
||||
user,
|
||||
ai,
|
||||
system;
|
||||
}
|
||||
|
||||
enum ConversationStatus {
|
||||
active,
|
||||
paused,
|
||||
completed,
|
||||
cancelled;
|
||||
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ConversationStatus.active:
|
||||
return '进行中';
|
||||
case ConversationStatus.paused:
|
||||
return '已暂停';
|
||||
case ConversationStatus.completed:
|
||||
return '已完成';
|
||||
case ConversationStatus.cancelled:
|
||||
return '已取消';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConversationMessage {
|
||||
final String id;
|
||||
final String content;
|
||||
final MessageType type;
|
||||
final DateTime timestamp;
|
||||
final String? audioUrl;
|
||||
final double? confidence; // 语音识别置信度
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
const ConversationMessage({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.type,
|
||||
required this.timestamp,
|
||||
this.audioUrl,
|
||||
this.confidence,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory ConversationMessage.fromJson(Map<String, dynamic> json) {
|
||||
return ConversationMessage(
|
||||
id: json['id'] as String,
|
||||
content: json['content'] as String,
|
||||
type: MessageType.values.firstWhere(
|
||||
(e) => e.name == json['type'],
|
||||
orElse: () => MessageType.user,
|
||||
),
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
audioUrl: json['audioUrl'] as String?,
|
||||
confidence: json['confidence'] as double?,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'content': content,
|
||||
'type': type.name,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'audioUrl': audioUrl,
|
||||
'confidence': confidence,
|
||||
'metadata': metadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Conversation {
|
||||
final String id;
|
||||
final String taskId;
|
||||
final String userId;
|
||||
final List<ConversationMessage> messages;
|
||||
final ConversationStatus status;
|
||||
final DateTime startTime;
|
||||
final DateTime? endTime;
|
||||
final int totalDuration; // 总时长(秒)
|
||||
final Map<String, dynamic>? settings;
|
||||
|
||||
const Conversation({
|
||||
required this.id,
|
||||
required this.taskId,
|
||||
required this.userId,
|
||||
required this.messages,
|
||||
required this.status,
|
||||
required this.startTime,
|
||||
this.endTime,
|
||||
required this.totalDuration,
|
||||
this.settings,
|
||||
});
|
||||
|
||||
factory Conversation.fromJson(Map<String, dynamic> json) {
|
||||
return Conversation(
|
||||
id: json['id'] as String,
|
||||
taskId: json['taskId'] as String,
|
||||
userId: json['userId'] as String,
|
||||
messages: (json['messages'] as List<dynamic>)
|
||||
.map((e) => ConversationMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
status: ConversationStatus.values.firstWhere(
|
||||
(e) => e.name == json['status'],
|
||||
orElse: () => ConversationStatus.active,
|
||||
),
|
||||
startTime: DateTime.parse(json['startTime'] as String),
|
||||
endTime: json['endTime'] != null
|
||||
? DateTime.parse(json['endTime'] as String)
|
||||
: null,
|
||||
totalDuration: json['totalDuration'] as int,
|
||||
settings: json['settings'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'taskId': taskId,
|
||||
'userId': userId,
|
||||
'messages': messages.map((e) => e.toJson()).toList(),
|
||||
'status': status.name,
|
||||
'startTime': startTime.toIso8601String(),
|
||||
'endTime': endTime?.toIso8601String(),
|
||||
'totalDuration': totalDuration,
|
||||
'settings': settings,
|
||||
};
|
||||
}
|
||||
|
||||
Conversation copyWith({
|
||||
String? id,
|
||||
String? taskId,
|
||||
String? userId,
|
||||
List<ConversationMessage>? messages,
|
||||
ConversationStatus? status,
|
||||
DateTime? startTime,
|
||||
DateTime? endTime,
|
||||
int? totalDuration,
|
||||
Map<String, dynamic>? settings,
|
||||
}) {
|
||||
return Conversation(
|
||||
id: id ?? this.id,
|
||||
taskId: taskId ?? this.taskId,
|
||||
userId: userId ?? this.userId,
|
||||
messages: messages ?? this.messages,
|
||||
status: status ?? this.status,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
totalDuration: totalDuration ?? this.totalDuration,
|
||||
settings: settings ?? this.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
193
client/lib/features/speaking/models/conversation_scenario.dart
Normal file
193
client/lib/features/speaking/models/conversation_scenario.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
/// 对话场景数据模型
|
||||
class ConversationScenario {
|
||||
final String id;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String description;
|
||||
final String duration;
|
||||
final String level;
|
||||
final ScenarioType type;
|
||||
final List<String> objectives;
|
||||
final List<String> keyPhrases;
|
||||
final List<ScenarioStep> steps;
|
||||
final DateTime createdAt;
|
||||
|
||||
ConversationScenario({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.description,
|
||||
required this.duration,
|
||||
required this.level,
|
||||
required this.type,
|
||||
required this.objectives,
|
||||
required this.keyPhrases,
|
||||
required this.steps,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory ConversationScenario.fromJson(Map<String, dynamic> json) {
|
||||
return ConversationScenario(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
subtitle: json['subtitle'],
|
||||
description: json['description'],
|
||||
duration: json['duration'],
|
||||
level: json['level'],
|
||||
type: ScenarioType.values.firstWhere(
|
||||
(e) => e.toString().split('.').last == json['type'],
|
||||
),
|
||||
objectives: List<String>.from(json['objectives']),
|
||||
keyPhrases: List<String>.from(json['keyPhrases']),
|
||||
steps: (json['steps'] as List)
|
||||
.map((step) => ScenarioStep.fromJson(step))
|
||||
.toList(),
|
||||
createdAt: DateTime.parse(json['createdAt']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'subtitle': subtitle,
|
||||
'description': description,
|
||||
'duration': duration,
|
||||
'level': level,
|
||||
'type': type.toString().split('.').last,
|
||||
'objectives': objectives,
|
||||
'keyPhrases': keyPhrases,
|
||||
'steps': steps.map((step) => step.toJson()).toList(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
ConversationScenario copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? subtitle,
|
||||
String? description,
|
||||
String? duration,
|
||||
String? level,
|
||||
ScenarioType? type,
|
||||
List<String>? objectives,
|
||||
List<String>? keyPhrases,
|
||||
List<ScenarioStep>? steps,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return ConversationScenario(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
description: description ?? this.description,
|
||||
duration: duration ?? this.duration,
|
||||
level: level ?? this.level,
|
||||
type: type ?? this.type,
|
||||
objectives: objectives ?? this.objectives,
|
||||
keyPhrases: keyPhrases ?? this.keyPhrases,
|
||||
steps: steps ?? this.steps,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 场景类型枚举
|
||||
enum ScenarioType {
|
||||
restaurant,
|
||||
interview,
|
||||
business,
|
||||
travel,
|
||||
shopping,
|
||||
medical,
|
||||
education,
|
||||
social,
|
||||
}
|
||||
|
||||
extension ScenarioTypeExtension on ScenarioType {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case ScenarioType.restaurant:
|
||||
return '餐厅用餐';
|
||||
case ScenarioType.interview:
|
||||
return '工作面试';
|
||||
case ScenarioType.business:
|
||||
return '商务会议';
|
||||
case ScenarioType.travel:
|
||||
return '旅行出行';
|
||||
case ScenarioType.shopping:
|
||||
return '购物消费';
|
||||
case ScenarioType.medical:
|
||||
return '医疗健康';
|
||||
case ScenarioType.education:
|
||||
return '教育学习';
|
||||
case ScenarioType.social:
|
||||
return '社交聚会';
|
||||
}
|
||||
}
|
||||
|
||||
String get icon {
|
||||
switch (this) {
|
||||
case ScenarioType.restaurant:
|
||||
return '🍽️';
|
||||
case ScenarioType.interview:
|
||||
return '💼';
|
||||
case ScenarioType.business:
|
||||
return '🏢';
|
||||
case ScenarioType.travel:
|
||||
return '✈️';
|
||||
case ScenarioType.shopping:
|
||||
return '🛍️';
|
||||
case ScenarioType.medical:
|
||||
return '🏥';
|
||||
case ScenarioType.education:
|
||||
return '📚';
|
||||
case ScenarioType.social:
|
||||
return '🎉';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 场景步骤
|
||||
class ScenarioStep {
|
||||
final int stepNumber;
|
||||
final String title;
|
||||
final String description;
|
||||
final String role; // 'user' or 'npc'
|
||||
final String content;
|
||||
final List<String> options;
|
||||
final String? correctOption;
|
||||
|
||||
ScenarioStep({
|
||||
required this.stepNumber,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.role,
|
||||
required this.content,
|
||||
required this.options,
|
||||
this.correctOption,
|
||||
});
|
||||
|
||||
factory ScenarioStep.fromJson(Map<String, dynamic> json) {
|
||||
return ScenarioStep(
|
||||
stepNumber: json['stepNumber'],
|
||||
title: json['title'],
|
||||
description: json['description'],
|
||||
role: json['role'],
|
||||
content: json['content'],
|
||||
options: List<String>.from(json['options']),
|
||||
correctOption: json['correctOption'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'stepNumber': stepNumber,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'role': role,
|
||||
'content': content,
|
||||
'options': options,
|
||||
'correctOption': correctOption,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
enum PronunciationCriteria {
|
||||
accuracy,
|
||||
fluency,
|
||||
completeness,
|
||||
prosody;
|
||||
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case PronunciationCriteria.accuracy:
|
||||
return '准确性';
|
||||
case PronunciationCriteria.fluency:
|
||||
return '流利度';
|
||||
case PronunciationCriteria.completeness:
|
||||
return '完整性';
|
||||
case PronunciationCriteria.prosody:
|
||||
return '韵律';
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
switch (this) {
|
||||
case PronunciationCriteria.accuracy:
|
||||
return '发音的准确程度';
|
||||
case PronunciationCriteria.fluency:
|
||||
return '语音的流畅程度';
|
||||
case PronunciationCriteria.completeness:
|
||||
return '内容的完整程度';
|
||||
case PronunciationCriteria.prosody:
|
||||
return '语调和节奏的自然程度';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WordPronunciation {
|
||||
final String word;
|
||||
final double accuracyScore; // 0-100
|
||||
final String? errorType;
|
||||
final List<String> phonemes;
|
||||
final List<double> phonemeScores;
|
||||
final int startTime; // 毫秒
|
||||
final int endTime; // 毫秒
|
||||
|
||||
const WordPronunciation({
|
||||
required this.word,
|
||||
required this.accuracyScore,
|
||||
this.errorType,
|
||||
required this.phonemes,
|
||||
required this.phonemeScores,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
});
|
||||
|
||||
factory WordPronunciation.fromJson(Map<String, dynamic> json) {
|
||||
return WordPronunciation(
|
||||
word: json['word'] as String,
|
||||
accuracyScore: (json['accuracyScore'] as num).toDouble(),
|
||||
errorType: json['errorType'] as String?,
|
||||
phonemes: List<String>.from(json['phonemes'] ?? []),
|
||||
phonemeScores: List<double>.from(
|
||||
(json['phonemeScores'] as List<dynamic>? ?? [])
|
||||
.map((e) => (e as num).toDouble()),
|
||||
),
|
||||
startTime: json['startTime'] as int,
|
||||
endTime: json['endTime'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'word': word,
|
||||
'accuracyScore': accuracyScore,
|
||||
'errorType': errorType,
|
||||
'phonemes': phonemes,
|
||||
'phonemeScores': phonemeScores,
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PronunciationAssessment {
|
||||
final String id;
|
||||
final String conversationId;
|
||||
final String messageId;
|
||||
final String originalText;
|
||||
final String recognizedText;
|
||||
final Map<PronunciationCriteria, double> scores; // 0-100
|
||||
final double overallScore; // 0-100
|
||||
final List<WordPronunciation> wordDetails;
|
||||
final List<String> suggestions;
|
||||
final DateTime assessedAt;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
const PronunciationAssessment({
|
||||
required this.id,
|
||||
required this.conversationId,
|
||||
required this.messageId,
|
||||
required this.originalText,
|
||||
required this.recognizedText,
|
||||
required this.scores,
|
||||
required this.overallScore,
|
||||
required this.wordDetails,
|
||||
required this.suggestions,
|
||||
required this.assessedAt,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory PronunciationAssessment.fromJson(Map<String, dynamic> json) {
|
||||
final scoresMap = <PronunciationCriteria, double>{};
|
||||
final scoresJson = json['scores'] as Map<String, dynamic>? ?? {};
|
||||
for (final criteria in PronunciationCriteria.values) {
|
||||
scoresMap[criteria] = (scoresJson[criteria.name] as num?)?.toDouble() ?? 0.0;
|
||||
}
|
||||
|
||||
return PronunciationAssessment(
|
||||
id: json['id'] as String,
|
||||
conversationId: json['conversationId'] as String,
|
||||
messageId: json['messageId'] as String,
|
||||
originalText: json['originalText'] as String,
|
||||
recognizedText: json['recognizedText'] as String,
|
||||
scores: scoresMap,
|
||||
overallScore: (json['overallScore'] as num).toDouble(),
|
||||
wordDetails: (json['wordDetails'] as List<dynamic>? ?? [])
|
||||
.map((e) => WordPronunciation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
suggestions: List<String>.from(json['suggestions'] ?? []),
|
||||
assessedAt: DateTime.parse(json['assessedAt'] as String),
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final scoresJson = <String, double>{};
|
||||
for (final entry in scores.entries) {
|
||||
scoresJson[entry.key.name] = entry.value;
|
||||
}
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'conversationId': conversationId,
|
||||
'messageId': messageId,
|
||||
'originalText': originalText,
|
||||
'recognizedText': recognizedText,
|
||||
'scores': scoresJson,
|
||||
'overallScore': overallScore,
|
||||
'wordDetails': wordDetails.map((e) => e.toJson()).toList(),
|
||||
'suggestions': suggestions,
|
||||
'assessedAt': assessedAt.toIso8601String(),
|
||||
'metadata': metadata,
|
||||
};
|
||||
}
|
||||
|
||||
String get accuracyLevel {
|
||||
if (overallScore >= 90) return '优秀';
|
||||
if (overallScore >= 80) return '良好';
|
||||
if (overallScore >= 70) return '中等';
|
||||
if (overallScore >= 60) return '及格';
|
||||
return '需要改进';
|
||||
}
|
||||
|
||||
List<String> get mainIssues {
|
||||
final issues = <String>[];
|
||||
|
||||
if (scores[PronunciationCriteria.accuracy]! < 70) {
|
||||
issues.add('发音准确性需要提高');
|
||||
}
|
||||
if (scores[PronunciationCriteria.fluency]! < 70) {
|
||||
issues.add('语音流利度有待改善');
|
||||
}
|
||||
if (scores[PronunciationCriteria.prosody]! < 70) {
|
||||
issues.add('语调和节奏需要调整');
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
}
|
||||
201
client/lib/features/speaking/models/pronunciation_item.dart
Normal file
201
client/lib/features/speaking/models/pronunciation_item.dart
Normal file
@@ -0,0 +1,201 @@
|
||||
/// 发音练习项目数据模型
|
||||
class PronunciationItem {
|
||||
final String id;
|
||||
final String text;
|
||||
final String phonetic;
|
||||
final String audioUrl;
|
||||
final PronunciationType type;
|
||||
final DifficultyLevel difficulty;
|
||||
final String category;
|
||||
final List<String> tips;
|
||||
final DateTime createdAt;
|
||||
|
||||
PronunciationItem({
|
||||
required this.id,
|
||||
required this.text,
|
||||
required this.phonetic,
|
||||
required this.audioUrl,
|
||||
required this.type,
|
||||
required this.difficulty,
|
||||
required this.category,
|
||||
required this.tips,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory PronunciationItem.fromJson(Map<String, dynamic> json) {
|
||||
return PronunciationItem(
|
||||
id: json['id'],
|
||||
text: json['text'],
|
||||
phonetic: json['phonetic'],
|
||||
audioUrl: json['audioUrl'],
|
||||
type: PronunciationType.values.firstWhere(
|
||||
(e) => e.toString().split('.').last == json['type'],
|
||||
),
|
||||
difficulty: DifficultyLevel.values.firstWhere(
|
||||
(e) => e.toString().split('.').last == json['difficulty'],
|
||||
),
|
||||
category: json['category'],
|
||||
tips: List<String>.from(json['tips']),
|
||||
createdAt: DateTime.parse(json['createdAt']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'text': text,
|
||||
'phonetic': phonetic,
|
||||
'audioUrl': audioUrl,
|
||||
'type': type.toString().split('.').last,
|
||||
'difficulty': difficulty.toString().split('.').last,
|
||||
'category': category,
|
||||
'tips': tips,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
PronunciationItem copyWith({
|
||||
String? id,
|
||||
String? text,
|
||||
String? phonetic,
|
||||
String? audioUrl,
|
||||
PronunciationType? type,
|
||||
DifficultyLevel? difficulty,
|
||||
String? category,
|
||||
List<String>? tips,
|
||||
DateTime? createdAt,
|
||||
}) {
|
||||
return PronunciationItem(
|
||||
id: id ?? this.id,
|
||||
text: text ?? this.text,
|
||||
phonetic: phonetic ?? this.phonetic,
|
||||
audioUrl: audioUrl ?? this.audioUrl,
|
||||
type: type ?? this.type,
|
||||
difficulty: difficulty ?? this.difficulty,
|
||||
category: category ?? this.category,
|
||||
tips: tips ?? this.tips,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发音练习类型
|
||||
enum PronunciationType {
|
||||
word,
|
||||
sentence,
|
||||
phrase,
|
||||
phoneme,
|
||||
}
|
||||
|
||||
extension PronunciationTypeExtension on PronunciationType {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case PronunciationType.word:
|
||||
return '单词发音';
|
||||
case PronunciationType.sentence:
|
||||
return '句子朗读';
|
||||
case PronunciationType.phrase:
|
||||
return '短语练习';
|
||||
case PronunciationType.phoneme:
|
||||
return '音素练习';
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
switch (this) {
|
||||
case PronunciationType.word:
|
||||
return '练习单个单词的准确发音';
|
||||
case PronunciationType.sentence:
|
||||
return '练习完整句子的语调和节奏';
|
||||
case PronunciationType.phrase:
|
||||
return '练习常用短语的连读';
|
||||
case PronunciationType.phoneme:
|
||||
return '练习基础音素的发音';
|
||||
}
|
||||
}
|
||||
|
||||
String get icon {
|
||||
switch (this) {
|
||||
case PronunciationType.word:
|
||||
return '🔤';
|
||||
case PronunciationType.sentence:
|
||||
return '📝';
|
||||
case PronunciationType.phrase:
|
||||
return '💬';
|
||||
case PronunciationType.phoneme:
|
||||
return '🔊';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 难度级别
|
||||
enum DifficultyLevel {
|
||||
beginner,
|
||||
intermediate,
|
||||
advanced,
|
||||
}
|
||||
|
||||
extension DifficultyLevelExtension on DifficultyLevel {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case DifficultyLevel.beginner:
|
||||
return '初级';
|
||||
case DifficultyLevel.intermediate:
|
||||
return '中级';
|
||||
case DifficultyLevel.advanced:
|
||||
return '高级';
|
||||
}
|
||||
}
|
||||
|
||||
String get code {
|
||||
switch (this) {
|
||||
case DifficultyLevel.beginner:
|
||||
return 'A1-A2';
|
||||
case DifficultyLevel.intermediate:
|
||||
return 'B1-B2';
|
||||
case DifficultyLevel.advanced:
|
||||
return 'C1-C2';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 发音练习记录
|
||||
class PronunciationRecord {
|
||||
final String id;
|
||||
final String itemId;
|
||||
final double score;
|
||||
final String feedback;
|
||||
final DateTime practiceDate;
|
||||
final int attempts;
|
||||
|
||||
PronunciationRecord({
|
||||
required this.id,
|
||||
required this.itemId,
|
||||
required this.score,
|
||||
required this.feedback,
|
||||
required this.practiceDate,
|
||||
required this.attempts,
|
||||
});
|
||||
|
||||
factory PronunciationRecord.fromJson(Map<String, dynamic> json) {
|
||||
return PronunciationRecord(
|
||||
id: json['id'],
|
||||
itemId: json['itemId'],
|
||||
score: json['score'].toDouble(),
|
||||
feedback: json['feedback'],
|
||||
practiceDate: DateTime.parse(json['practiceDate']),
|
||||
attempts: json['attempts'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'itemId': itemId,
|
||||
'score': score,
|
||||
'feedback': feedback,
|
||||
'practiceDate': practiceDate.toIso8601String(),
|
||||
'attempts': attempts,
|
||||
};
|
||||
}
|
||||
}
|
||||
163
client/lib/features/speaking/models/speaking_scenario.dart
Normal file
163
client/lib/features/speaking/models/speaking_scenario.dart
Normal file
@@ -0,0 +1,163 @@
|
||||
enum SpeakingScenario {
|
||||
dailyConversation,
|
||||
businessMeeting,
|
||||
jobInterview,
|
||||
shopping,
|
||||
restaurant,
|
||||
travel,
|
||||
academic,
|
||||
socializing,
|
||||
phoneCall,
|
||||
presentation;
|
||||
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case SpeakingScenario.dailyConversation:
|
||||
return '日常对话';
|
||||
case SpeakingScenario.businessMeeting:
|
||||
return '商务会议';
|
||||
case SpeakingScenario.jobInterview:
|
||||
return '求职面试';
|
||||
case SpeakingScenario.shopping:
|
||||
return '购物';
|
||||
case SpeakingScenario.restaurant:
|
||||
return '餐厅';
|
||||
case SpeakingScenario.travel:
|
||||
return '旅行';
|
||||
case SpeakingScenario.academic:
|
||||
return '学术讨论';
|
||||
case SpeakingScenario.socializing:
|
||||
return '社交聚会';
|
||||
case SpeakingScenario.phoneCall:
|
||||
return '电话通话';
|
||||
case SpeakingScenario.presentation:
|
||||
return '演讲展示';
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
switch (this) {
|
||||
case SpeakingScenario.dailyConversation:
|
||||
return '练习日常生活中的基本对话';
|
||||
case SpeakingScenario.businessMeeting:
|
||||
return '提升商务环境下的沟通能力';
|
||||
case SpeakingScenario.jobInterview:
|
||||
return '准备求职面试的常见问题';
|
||||
case SpeakingScenario.shopping:
|
||||
return '学习购物时的实用表达';
|
||||
case SpeakingScenario.restaurant:
|
||||
return '掌握餐厅点餐的对话技巧';
|
||||
case SpeakingScenario.travel:
|
||||
return '旅行中的必备口语交流';
|
||||
case SpeakingScenario.academic:
|
||||
return '学术环境下的专业讨论';
|
||||
case SpeakingScenario.socializing:
|
||||
return '社交场合的自然交流';
|
||||
case SpeakingScenario.phoneCall:
|
||||
return '电话沟通的特殊技巧';
|
||||
case SpeakingScenario.presentation:
|
||||
return '公开演讲和展示技能';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SpeakingDifficulty {
|
||||
beginner,
|
||||
elementary,
|
||||
intermediate,
|
||||
upperIntermediate,
|
||||
advanced;
|
||||
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case SpeakingDifficulty.beginner:
|
||||
return '初学者';
|
||||
case SpeakingDifficulty.elementary:
|
||||
return '基础';
|
||||
case SpeakingDifficulty.intermediate:
|
||||
return '中级';
|
||||
case SpeakingDifficulty.upperIntermediate:
|
||||
return '中高级';
|
||||
case SpeakingDifficulty.advanced:
|
||||
return '高级';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SpeakingTask {
|
||||
final String id;
|
||||
final String title;
|
||||
final String description;
|
||||
final SpeakingScenario scenario;
|
||||
final SpeakingDifficulty difficulty;
|
||||
final List<String> objectives;
|
||||
final List<String> keyPhrases;
|
||||
final String? backgroundInfo;
|
||||
final int estimatedDuration; // 预估时长(分钟)
|
||||
final bool isRecommended; // 是否推荐
|
||||
final bool isFavorite; // 是否收藏
|
||||
final int completionCount; // 完成次数
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const SpeakingTask({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.scenario,
|
||||
required this.difficulty,
|
||||
required this.objectives,
|
||||
required this.keyPhrases,
|
||||
this.backgroundInfo,
|
||||
required this.estimatedDuration,
|
||||
this.isRecommended = false,
|
||||
this.isFavorite = false,
|
||||
this.completionCount = 0,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory SpeakingTask.fromJson(Map<String, dynamic> json) {
|
||||
return SpeakingTask(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
scenario: SpeakingScenario.values.firstWhere(
|
||||
(e) => e.name == json['scenario'],
|
||||
orElse: () => SpeakingScenario.dailyConversation,
|
||||
),
|
||||
difficulty: SpeakingDifficulty.values.firstWhere(
|
||||
(e) => e.name == json['difficulty'],
|
||||
orElse: () => SpeakingDifficulty.intermediate,
|
||||
),
|
||||
objectives: List<String>.from(json['objectives'] ?? []),
|
||||
keyPhrases: List<String>.from(json['keyPhrases'] ?? []),
|
||||
backgroundInfo: json['backgroundInfo'] as String?,
|
||||
estimatedDuration: json['estimatedDuration'] as int,
|
||||
isRecommended: json['isRecommended'] as bool? ?? false,
|
||||
isFavorite: json['isFavorite'] as bool? ?? false,
|
||||
completionCount: json['completionCount'] as int? ?? 0,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'scenario': scenario.name,
|
||||
'difficulty': difficulty.name,
|
||||
'objectives': objectives,
|
||||
'keyPhrases': keyPhrases,
|
||||
'backgroundInfo': backgroundInfo,
|
||||
'estimatedDuration': estimatedDuration,
|
||||
'isRecommended': isRecommended,
|
||||
'isFavorite': isFavorite,
|
||||
'completionCount': completionCount,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
244
client/lib/features/speaking/models/speaking_stats.dart
Normal file
244
client/lib/features/speaking/models/speaking_stats.dart
Normal file
@@ -0,0 +1,244 @@
|
||||
import 'speaking_scenario.dart';
|
||||
import 'pronunciation_assessment.dart';
|
||||
|
||||
class SpeakingStats {
|
||||
final int totalSessions;
|
||||
final int totalMinutes;
|
||||
final double averageScore;
|
||||
final Map<SpeakingScenario, int> scenarioStats;
|
||||
final Map<SpeakingDifficulty, int> difficultyStats;
|
||||
final List<SpeakingProgressData> progressData;
|
||||
final SpeakingSkillAnalysis skillAnalysis;
|
||||
final DateTime lastUpdated;
|
||||
|
||||
const SpeakingStats({
|
||||
required this.totalSessions,
|
||||
required this.totalMinutes,
|
||||
required this.averageScore,
|
||||
required this.scenarioStats,
|
||||
required this.difficultyStats,
|
||||
required this.progressData,
|
||||
required this.skillAnalysis,
|
||||
required this.lastUpdated,
|
||||
});
|
||||
|
||||
factory SpeakingStats.fromJson(Map<String, dynamic> json) {
|
||||
final scenarioStatsJson = json['scenarioStats'] as Map<String, dynamic>? ?? {};
|
||||
final scenarioStats = <SpeakingScenario, int>{};
|
||||
for (final scenario in SpeakingScenario.values) {
|
||||
final v = scenarioStatsJson[scenario.name];
|
||||
scenarioStats[scenario] = (v is num) ? v.toInt() : 0;
|
||||
}
|
||||
|
||||
final difficultyStatsLegacy = json['difficultyStats'] as Map<String, dynamic>?;
|
||||
final levelStatsBackend = json['stats_by_level'] as Map<String, dynamic>?;
|
||||
final difficultyStats = <SpeakingDifficulty, int>{};
|
||||
if (difficultyStatsLegacy != null) {
|
||||
for (final difficulty in SpeakingDifficulty.values) {
|
||||
final v = difficultyStatsLegacy[difficulty.name];
|
||||
difficultyStats[difficulty] = (v is num) ? v.toInt() : 0;
|
||||
}
|
||||
} else {
|
||||
for (final difficulty in SpeakingDifficulty.values) {
|
||||
final key = {
|
||||
SpeakingDifficulty.beginner: 'beginner',
|
||||
SpeakingDifficulty.elementary: 'elementary',
|
||||
SpeakingDifficulty.intermediate: 'intermediate',
|
||||
SpeakingDifficulty.upperIntermediate: 'upper_intermediate',
|
||||
SpeakingDifficulty.advanced: 'advanced',
|
||||
}[difficulty]!;
|
||||
final entry = levelStatsBackend?[key];
|
||||
int count = 0;
|
||||
if (entry is Map) {
|
||||
final c = entry['count'];
|
||||
if (c is num) count = c.toInt();
|
||||
}
|
||||
difficultyStats[difficulty] = count;
|
||||
}
|
||||
}
|
||||
|
||||
final avgScores = json['average_scores'] as Map<String, dynamic>? ?? {};
|
||||
final averageScore = (json['averageScore'] as num?)?.toDouble() ?? (avgScores['overall'] as num?)?.toDouble() ?? 0.0;
|
||||
|
||||
final skillAnalysisJson = json['skillAnalysis'] as Map<String, dynamic>?;
|
||||
final skillAnalysis = skillAnalysisJson != null
|
||||
? SpeakingSkillAnalysis.fromJson(skillAnalysisJson)
|
||||
: SpeakingSkillAnalysis(
|
||||
criteriaScores: {
|
||||
PronunciationCriteria.accuracy: (avgScores['accuracy'] as num?)?.toDouble() ?? 0.0,
|
||||
PronunciationCriteria.fluency: (avgScores['fluency'] as num?)?.toDouble() ?? 0.0,
|
||||
PronunciationCriteria.completeness: (avgScores['completeness'] as num?)?.toDouble() ?? 0.0,
|
||||
PronunciationCriteria.prosody: (avgScores['prosody'] as num?)?.toDouble() ?? 0.0,
|
||||
},
|
||||
commonErrors: {},
|
||||
strengths: const [],
|
||||
weaknesses: const [],
|
||||
recommendations: const [],
|
||||
improvementRate: 0.0,
|
||||
lastAnalyzed: DateTime.now(),
|
||||
);
|
||||
|
||||
return SpeakingStats(
|
||||
totalSessions: (json['totalSessions'] as int?) ?? (json['total_records'] as num?)?.toInt() ?? 0,
|
||||
totalMinutes: (json['totalMinutes'] as int?) ?? (json['total_duration'] as num?)?.toInt() ?? 0,
|
||||
averageScore: averageScore,
|
||||
scenarioStats: scenarioStats,
|
||||
difficultyStats: difficultyStats,
|
||||
progressData: (json['progressData'] as List<dynamic>? ?? [])
|
||||
.map((e) => SpeakingProgressData.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
skillAnalysis: skillAnalysis,
|
||||
lastUpdated: (json['lastUpdated'] is String)
|
||||
? DateTime.parse(json['lastUpdated'] as String)
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final scenarioStatsJson = <String, int>{};
|
||||
for (final entry in scenarioStats.entries) {
|
||||
scenarioStatsJson[entry.key.name] = entry.value;
|
||||
}
|
||||
|
||||
final difficultyStatsJson = <String, int>{};
|
||||
for (final entry in difficultyStats.entries) {
|
||||
difficultyStatsJson[entry.key.name] = entry.value;
|
||||
}
|
||||
|
||||
return {
|
||||
'totalSessions': totalSessions,
|
||||
'totalMinutes': totalMinutes,
|
||||
'averageScore': averageScore,
|
||||
'scenarioStats': scenarioStatsJson,
|
||||
'difficultyStats': difficultyStatsJson,
|
||||
'progressData': progressData.map((e) => e.toJson()).toList(),
|
||||
'skillAnalysis': skillAnalysis.toJson(),
|
||||
'lastUpdated': lastUpdated.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SpeakingProgressData {
|
||||
final DateTime date;
|
||||
final double averageScore;
|
||||
final int sessionCount;
|
||||
final int totalMinutes;
|
||||
final Map<PronunciationCriteria, double> criteriaScores;
|
||||
|
||||
const SpeakingProgressData({
|
||||
required this.date,
|
||||
required this.averageScore,
|
||||
required this.sessionCount,
|
||||
required this.totalMinutes,
|
||||
required this.criteriaScores,
|
||||
});
|
||||
|
||||
factory SpeakingProgressData.fromJson(Map<String, dynamic> json) {
|
||||
final criteriaScoresJson = json['criteriaScores'] as Map<String, dynamic>? ?? {};
|
||||
final criteriaScores = <PronunciationCriteria, double>{};
|
||||
for (final criteria in PronunciationCriteria.values) {
|
||||
criteriaScores[criteria] = (criteriaScoresJson[criteria.name] as num?)?.toDouble() ?? 0.0;
|
||||
}
|
||||
|
||||
return SpeakingProgressData(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
averageScore: (json['averageScore'] as num).toDouble(),
|
||||
sessionCount: (json['sessionCount'] as num).toInt(),
|
||||
totalMinutes: (json['totalMinutes'] as num).toInt(),
|
||||
criteriaScores: criteriaScores,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final criteriaScoresJson = <String, double>{};
|
||||
for (final entry in criteriaScores.entries) {
|
||||
criteriaScoresJson[entry.key.name] = entry.value;
|
||||
}
|
||||
|
||||
return {
|
||||
'date': date.toIso8601String(),
|
||||
'averageScore': averageScore,
|
||||
'sessionCount': sessionCount,
|
||||
'totalMinutes': totalMinutes,
|
||||
'criteriaScores': criteriaScoresJson,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SpeakingSkillAnalysis {
|
||||
final Map<PronunciationCriteria, double> criteriaScores;
|
||||
final Map<String, int> commonErrors;
|
||||
final List<String> strengths;
|
||||
final List<String> weaknesses;
|
||||
final List<String> recommendations;
|
||||
final double improvementRate; // 改进速度
|
||||
final DateTime lastAnalyzed;
|
||||
|
||||
const SpeakingSkillAnalysis({
|
||||
required this.criteriaScores,
|
||||
required this.commonErrors,
|
||||
required this.strengths,
|
||||
required this.weaknesses,
|
||||
required this.recommendations,
|
||||
required this.improvementRate,
|
||||
required this.lastAnalyzed,
|
||||
});
|
||||
|
||||
factory SpeakingSkillAnalysis.fromJson(Map<String, dynamic> json) {
|
||||
final criteriaScoresJson = json['criteriaScores'] as Map<String, dynamic>? ?? {};
|
||||
final criteriaScores = <PronunciationCriteria, double>{};
|
||||
for (final criteria in PronunciationCriteria.values) {
|
||||
criteriaScores[criteria] = (criteriaScoresJson[criteria.name] as num?)?.toDouble() ?? 0.0;
|
||||
}
|
||||
|
||||
return SpeakingSkillAnalysis(
|
||||
criteriaScores: criteriaScores,
|
||||
commonErrors: Map<String, int>.from(json['commonErrors'] ?? {}),
|
||||
strengths: List<String>.from(json['strengths'] ?? []),
|
||||
weaknesses: List<String>.from(json['weaknesses'] ?? []),
|
||||
recommendations: List<String>.from(json['recommendations'] ?? []),
|
||||
improvementRate: (json['improvementRate'] as num?)?.toDouble() ?? 0.0,
|
||||
lastAnalyzed: (json['lastAnalyzed'] is String)
|
||||
? DateTime.parse(json['lastAnalyzed'] as String)
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final criteriaScoresJson = <String, double>{};
|
||||
for (final entry in criteriaScores.entries) {
|
||||
criteriaScoresJson[entry.key.name] = entry.value;
|
||||
}
|
||||
|
||||
return {
|
||||
'criteriaScores': criteriaScoresJson,
|
||||
'commonErrors': commonErrors,
|
||||
'strengths': strengths,
|
||||
'weaknesses': weaknesses,
|
||||
'recommendations': recommendations,
|
||||
'improvementRate': improvementRate,
|
||||
'lastAnalyzed': lastAnalyzed.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
String get overallLevel {
|
||||
final averageScore = criteriaScores.values.reduce((a, b) => a + b) / criteriaScores.length;
|
||||
if (averageScore >= 90) return '优秀';
|
||||
if (averageScore >= 80) return '良好';
|
||||
if (averageScore >= 70) return '中等';
|
||||
if (averageScore >= 60) return '及格';
|
||||
return '需要改进';
|
||||
}
|
||||
|
||||
PronunciationCriteria get strongestSkill {
|
||||
return criteriaScores.entries
|
||||
.reduce((a, b) => a.value > b.value ? a : b)
|
||||
.key;
|
||||
}
|
||||
|
||||
PronunciationCriteria get weakestSkill {
|
||||
return criteriaScores.entries
|
||||
.reduce((a, b) => a.value < b.value ? a : b)
|
||||
.key;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user