feat: integrate YOLO runtime, update screens, and sync docs

This commit is contained in:
2026-05-24 10:26:58 +07:00
parent aa3dc4560e
commit 6ca89ca896
14 changed files with 1352 additions and 107 deletions
@@ -33,9 +33,16 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
String _status = 'Ready';
CameraController? _camera;
DetectionResult? _lastDetection;
bool _processingFrame = false;
DateTime _lastInferenceAt = DateTime.fromMillisecondsSinceEpoch(0);
DateTime _lastAlertAt = DateTime.fromMillisecondsSinceEpoch(0);
@override
void dispose() {
final camera = _camera;
if (camera != null && camera.value.isStreamingImages) {
unawaited(camera.stopImageStream().catchError((Object _) {}));
}
_camera?.dispose();
sl<LocationReporterService>().stop();
super.dispose();
@@ -47,8 +54,7 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
await _startCamera();
await sl<LocationReporterService>().start(walkGuideActive: true);
} else {
await _camera?.dispose();
_camera = null;
await _stopCamera();
await sl<LocationReporterService>().stop();
}
setState(() {
@@ -67,35 +73,89 @@ class _WalkGuideScreenState extends State<WalkGuideScreen> {
final cameras = await availableCameras();
if (cameras.isEmpty) return;
final controller = CameraController(
cameras.first, ResolutionPreset.medium,
enableAudio: false);
cameras.first,
ResolutionPreset.medium,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.yuv420,
);
await controller.initialize();
if (!mounted) {
await controller.dispose();
return;
}
try {
await controller.startImageStream(_onCameraImage);
} catch (_) {
// Preview still works; manual Demo Detect remains available.
}
setState(() => _camera = controller);
} catch (_) {
setState(() => _status = 'Camera unavailable. Demo mode active.');
}
}
Future<void> _stopCamera() async {
final camera = _camera;
_camera = null;
if (camera == null) return;
try {
if (camera.value.isStreamingImages) {
await camera.stopImageStream();
}
} catch (_) {}
await camera.dispose();
}
void _onCameraImage(CameraImage image) {
if (!_active || _processingFrame) return;
final now = DateTime.now();
if (now.difference(_lastInferenceAt) < const Duration(milliseconds: 900)) {
return;
}
_lastInferenceAt = now;
_processingFrame = true;
unawaited(_runYolo(image).whenComplete(() => _processingFrame = false));
}
Future<void> _runYolo(CameraImage image) async {
final detection = await sl<YoloDetector>().detect(image);
if (detection == null || !mounted) return;
await _handleDetection(detection);
}
Future<void> _simulateObstacle() async {
final detection = await sl<YoloDetector>().detectFallback();
if (detection == null) return;
await _handleDetection(detection, forceAlert: true);
}
Future<void> _handleDetection(
DetectionResult detection, {
bool forceAlert = false,
}) async {
_lastDetection = detection;
await sl<ApiClient>().dio.post('/user/obstacle', data: {
'label': detection.label,
'confidence': detection.confidence,
'direction': detection.directionName,
'estimatedDist': detection.estimatedDistance,
'lat': null,
'lng': null,
});
await sl<HapticService>().obstacleClose();
await sl<TtsService>().speakImmediate(detection.spokenId);
setState(() => _status =
'Obstacle: ${detection.label} ${detection.directionName} ${detection.estimatedDistance}');
final now = DateTime.now();
if (!forceAlert &&
now.difference(_lastAlertAt) < const Duration(seconds: 4)) {
return;
}
_lastAlertAt = now;
try {
await sl<ApiClient>().dio.post('/user/obstacle', data: {
'label': detection.label,
'confidence': detection.confidence,
'direction': detection.directionName,
'estimatedDist': detection.estimatedDistance,
'lat': null,
'lng': null,
});
} catch (_) {}
await sl<HapticService>().obstacleClose();
await sl<TtsService>().speakImmediate(detection.spokenId);
}
@override
@@ -235,12 +295,8 @@ class _AiBenchmarkScreenState extends State<AiBenchmarkScreen> {
String label = 'person';
String direction = 'CENTER';
String distance = 'Demo';
var modelLoaded = false;
try {
await rootBundle.load(_selectedModel).timeout(const Duration(seconds: 3));
modelLoaded = true;
} catch (_) {}
final detection = await sl<YoloDetector>().detectFallback();
final modelLoaded = sl<YoloDetector>().isReady;
final detection = await sl<YoloDetector>().detectSynthetic();
if (detection != null) {
label = detection.label;
direction = detection.directionName;
@@ -507,9 +563,7 @@ class _EmptyPanel extends StatelessWidget {
final String title;
final String message;
const _EmptyPanel(
{required this.icon,
required this.title,
required this.message});
{required this.icon, required this.title, required this.message});
@override
Widget build(BuildContext context) {