feat: STEM Virtual Lab, English TTS companion, dynamic textbooks and zero-demo video fallback

This commit is contained in:
Hamza-Ayed
2026-09-03 17:37:15 +03:00
parent 445a1aa0e3
commit 58c3c48f69
19 changed files with 2162 additions and 258 deletions
@@ -85,6 +85,7 @@ class LessonPlaybackData {
final String storageType;
final List<LessonVersionModel> availableVersions;
final List<SocraticCheckpointModel> checkpoints;
final int? lastPositionSeconds;
const LessonPlaybackData({
required this.lessonId,
@@ -94,6 +95,7 @@ class LessonPlaybackData {
this.storageType = 'api_upload',
this.availableVersions = const [],
this.checkpoints = const [],
this.lastPositionSeconds,
});
factory LessonPlaybackData.fromJson(Map<String, dynamic> json) {
@@ -124,6 +126,8 @@ class LessonPlaybackData {
playback['hls_url']?.toString() ?? playback['stream_url']?.toString() ?? playback['video_url']?.toString() ?? '',
);
final lastPos = (json['last_position_seconds'] ?? playback['last_position_seconds'] ?? json['progress']?['position_seconds']) as num?;
return LessonPlaybackData(
lessonId: (lesson['id'] as num?)?.toInt() ?? 0,
title: lesson['title']?.toString() ?? 'الدرس التفاعلي',
@@ -132,6 +136,7 @@ class LessonPlaybackData {
storageType: playback['storage_type']?.toString() ?? 'api_upload',
availableVersions: versions,
checkpoints: points,
lastPositionSeconds: lastPos?.toInt(),
);
}
}
@@ -201,6 +201,7 @@ class CurriculumLessonItemModel {
final int durationSeconds;
final int checkpointsCount;
final bool isCompleted;
final bool hasVideo;
const CurriculumLessonItemModel({
required this.id,
@@ -210,6 +211,7 @@ class CurriculumLessonItemModel {
this.durationSeconds = 1200, // 20 mins default
this.checkpointsCount = 3,
this.isCompleted = false,
this.hasVideo = false,
});
factory CurriculumLessonItemModel.fromJson(Map<String, dynamic> json) {
@@ -221,14 +223,21 @@ class CurriculumLessonItemModel {
}
}
final filePath = json['file']?.toString();
// Default video availability: true for seeded math lessons, false for unseeded physics/english
final bool hasVideoExplicit = (json['has_video'] as bool?) ??
((json['video_url'] != null && json['video_url'].toString().isNotEmpty) ||
(filePath != null && filePath.contains('math_10/semester_1/unit_01/lesson_01')));
return CurriculumLessonItemModel(
id: json['id']?.toString() ?? 'lesson_${DateTime.now().millisecondsSinceEpoch}',
title: json['title']?.toString() ?? 'درس بدون عنوان',
outcomes: outs,
markdownFilePath: json['file']?.toString(),
markdownFilePath: filePath,
durationSeconds: (json['duration_seconds'] as num?)?.toInt() ?? 1200,
checkpointsCount: (json['checkpoints_count'] as num?)?.toInt() ?? 3,
isCompleted: (json['is_completed'] as bool?) ?? false,
hasVideo: hasVideoExplicit,
);
}
}