From d08d192188673553ae12882e172b5e4558e0faa0 Mon Sep 17 00:00:00 2001 From: JamTheDev <2004jamvillarosa@gmail.com> Date: Mon, 11 May 2026 14:11:24 +0800 Subject: [PATCH 1/5] feat: bootstrap web flutter_gemma --- .../datasources/flutter_gemma_bootstrap.dart | 14 ++++++++ .../datasources/gemma_model_file_type.dart | 14 ++++++++ .../datasources/local_gemma_datasource.dart | 34 +++++++++++-------- lib/main.dart | 11 ++---- web/index.html | 8 +++++ 5 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 lib/features/translator/data/datasources/flutter_gemma_bootstrap.dart create mode 100644 lib/features/translator/data/datasources/gemma_model_file_type.dart diff --git a/lib/features/translator/data/datasources/flutter_gemma_bootstrap.dart b/lib/features/translator/data/datasources/flutter_gemma_bootstrap.dart new file mode 100644 index 0000000..f9c5ff6 --- /dev/null +++ b/lib/features/translator/data/datasources/flutter_gemma_bootstrap.dart @@ -0,0 +1,14 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_gemma/core/api/flutter_gemma.dart'; +import 'package:flutter_gemma/core/domain/web_storage_mode.dart'; + +Future initializeFlutterGemma({String? huggingFaceToken}) async { + final String? normalizedToken = huggingFaceToken?.trim().isEmpty ?? true + ? null + : huggingFaceToken?.trim(); + + await FlutterGemma.initialize( + huggingFaceToken: normalizedToken, + webStorageMode: kIsWeb ? WebStorageMode.streaming : WebStorageMode.cacheApi, + ); +} diff --git a/lib/features/translator/data/datasources/gemma_model_file_type.dart b/lib/features/translator/data/datasources/gemma_model_file_type.dart new file mode 100644 index 0000000..9f0945e --- /dev/null +++ b/lib/features/translator/data/datasources/gemma_model_file_type.dart @@ -0,0 +1,14 @@ +import 'package:flutter_gemma/flutter_gemma.dart'; + +ModelFileType resolveGemmaModelFileType(String modelUrl) { + final Uri uri = Uri.parse(modelUrl); + final String fileName = uri.pathSegments.isEmpty + ? modelUrl + : uri.pathSegments.last; + final String lower = fileName.toLowerCase(); + + if (lower.endsWith('.litertlm')) { + return ModelFileType.litertlm; + } + return ModelFileType.task; +} diff --git a/lib/features/translator/data/datasources/local_gemma_datasource.dart b/lib/features/translator/data/datasources/local_gemma_datasource.dart index 72f5d28..8e8632d 100644 --- a/lib/features/translator/data/datasources/local_gemma_datasource.dart +++ b/lib/features/translator/data/datasources/local_gemma_datasource.dart @@ -4,6 +4,7 @@ import 'package:flutter_gemma/flutter_gemma.dart'; import 'package:kudlit_ph/core/error/exceptions.dart'; import 'package:kudlit_ph/features/translator/data/datasources/ai_datasource.dart'; +import 'package:kudlit_ph/features/translator/data/datasources/gemma_model_file_type.dart'; import 'package:kudlit_ph/features/translator/domain/entities/baybayin_challenge.dart'; import 'package:kudlit_ph/features/translator/domain/entities/chat_message.dart'; import 'package:kudlit_ph/features/translator/domain/entities/gemma_model_info.dart'; @@ -16,7 +17,6 @@ import 'package:kudlit_ph/features/translator/domain/entities/gemma_model_info.d /// - iOS: `flutter_gemma` uses `NSURLSession` which schedules the /// download discretionarily — iOS picks the timing, the app may /// be backgrounded or terminated while download proceeds. -/// - Web: not supported by this datasource (`kIsWeb` guard upstream). class LocalGemmaDatasource implements AiDatasource { LocalGemmaDatasource(); @@ -32,7 +32,9 @@ class LocalGemmaDatasource implements AiDatasource { Future probeReadiness(GemmaModelInfo model) { // Fast path: model is already loaded — skip all native work. if (_activeModel != null) { - debugPrint('[Gemma][local] readiness probe fast-path (model already loaded)'); + debugPrint( + '[Gemma][local] readiness probe fast-path (model already loaded)', + ); return Future.value( LocalGemmaReadiness( installed: true, @@ -97,7 +99,7 @@ class LocalGemmaDatasource implements AiDatasource { /// Ensures the model is loaded into memory without blocking inference. /// Safe to call fire-and-forget after download completes. Future ensureModelLoaded() async { - if (kIsWeb || _activeModel != null) return; + if (_activeModel != null) return; try { _activeModel = await FlutterGemma.getActiveModel(); _activeModelHasVision = false; @@ -127,7 +129,7 @@ class LocalGemmaDatasource implements AiDatasource { final InferenceInstallationBuilder builder = FlutterGemma.installModel( modelType: ModelType.gemma4, - fileType: _modelFileTypeFor(model), + fileType: resolveGemmaModelFileType(model.modelLink), ) .fromNetwork(model.modelLink, token: hfToken) .withCancelToken(_cancelToken!); @@ -205,9 +207,16 @@ class LocalGemmaDatasource implements AiDatasource { String mimeType = 'image/png', String? prompt, }) async* { + if (kIsWeb) { + throw UnsupportedError( + 'Image analysis is not supported by flutter_gemma on web yet.', + ); + } InferenceChat? imageChat; try { - debugPrint('[Gemma][local] analyzeImage called | bytes=${imageBytes.length}'); + debugPrint( + '[Gemma][local] analyzeImage called | bytes=${imageBytes.length}', + ); // Close any active text chat — native model allows one session at a time. if (_chat != null) { await _chat!.close(); @@ -242,7 +251,10 @@ class LocalGemmaDatasource implements AiDatasource { debugPrint('[Gemma][local] analyzeImage stream finished'); } catch (e, s) { debugPrint('[Gemma][local] analyzeImage error: $e'); - debugPrintStack(stackTrace: s, label: '[Gemma][local] analyzeImage stack'); + debugPrintStack( + stackTrace: s, + label: '[Gemma][local] analyzeImage stack', + ); rethrow; } finally { await imageChat?.close(); @@ -261,19 +273,11 @@ class LocalGemmaDatasource implements AiDatasource { final String? hfToken = dotenv.env['HUGGINGFACE_TOKEN']; await FlutterGemma.installModel( modelType: ModelType.gemma4, - fileType: _modelFileTypeFor(model), + fileType: resolveGemmaModelFileType(model.modelLink), ).fromNetwork(model.modelLink, token: hfToken).install(); debugPrint('[Gemma][local] active model restored for ${model.fileName}'); } - ModelFileType _modelFileTypeFor(GemmaModelInfo model) { - final String lower = model.fileName.toLowerCase(); - if (lower.endsWith('.litertlm')) { - return ModelFileType.litertlm; - } - return ModelFileType.task; - } - @override Future dispose() async { await _activeModel?.close(); diff --git a/lib/main.dart b/lib/main.dart index 2374c9f..347b62d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,11 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_gemma/core/api/flutter_gemma.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:kudlit_ph/app/app.dart'; import 'package:kudlit_ph/core/config/supabase_config.dart'; +import 'package:kudlit_ph/features/translator/data/datasources/flutter_gemma_bootstrap.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -15,11 +14,7 @@ Future main() async { url: SupabaseConfig.url, anonKey: SupabaseConfig.anonKey, ); - if (!kIsWeb) { - final String hfToken = dotenv.env['HUGGINGFACE_TOKEN'] ?? ''; - await FlutterGemma.initialize( - huggingFaceToken: hfToken.isEmpty ? null : hfToken, - ); - } + final String hfToken = dotenv.env['HUGGINGFACE_TOKEN'] ?? ''; + await initializeFlutterGemma(huggingFaceToken: hfToken); runApp(const ProviderScope(child: KudlitApp())); } diff --git a/web/index.html b/web/index.html index f4cd097..e1be68a 100644 --- a/web/index.html +++ b/web/index.html @@ -43,6 +43,14 @@ Kudlit App - Baybayin Scanner, Translator, and Learning Companion +