import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; class ModelDownloadService { static const modelFileName = 'gemma-2b-it-cpu-int4.bin'; static const modelSizeBytes = 1183285248; // ~1.1 GB // Gemma 2B IT CPU int4 — MediaPipe LLM Inference model static const _downloadUrl = 'https://storage.googleapis.com/mediapipe-models/llm_inference/' 'gemma-2b-it-cpu-int4/float32/1/gemma-2b-it-cpu-int4.bin'; Future get modelPath async { final dir = await getApplicationSupportDirectory(); return p.join(dir.path, modelFileName); } Future isModelPresent() async { final path = await modelPath; final file = File(path); if (!file.existsSync()) return false; // Verify the file isn't a partial download (allow small variance) final size = await file.length(); return size > modelSizeBytes * 0.99; } /// Streams download progress as a value from 0.0 to 1.0. Stream download() async* { final path = await modelPath; final file = File(path); final client = HttpClient(); try { final request = await client.getUrl(Uri.parse(_downloadUrl)); final response = await request.close(); if (response.statusCode != 200) { throw Exception('Download failed: HTTP ${response.statusCode}'); } final totalBytes = response.contentLength > 0 ? response.contentLength : modelSizeBytes; int receivedBytes = 0; final sink = file.openWrite(); try { await for (final chunk in response) { sink.add(chunk); receivedBytes += chunk.length; yield (receivedBytes / totalBytes).clamp(0.0, 1.0); } } finally { await sink.close(); } } catch (_) { if (await file.exists()) await file.delete(); rethrow; } finally { client.close(); } } }