Vatxzz commited on
Commit
76e984e
·
1 Parent(s): 4b31493

feat: self-host on-device model, fix FlutterGemma init, surface real errors

Browse files
NOTICE ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Cachy — on-device model attribution and use restrictions
2
+ =========================================================
3
+
4
+ The optional on-device AI feature downloads and runs Gemma 3 1B (int4 .task
5
+ build), redistributed by this project from a GitHub release for install
6
+ convenience. Gemma is provided by Google under the Gemma Terms of Use.
7
+
8
+ Your use of the Gemma model is subject to:
9
+ - Gemma Terms of Use: https://ai.google.dev/gemma/terms
10
+ - Gemma Prohibited Use Policy: https://ai.google.dev/gemma/prohibited_use_policy
11
+
12
+ Gemma is a trademark of Google LLC. This project is not affiliated with or
13
+ endorsed by Google. The model weights are unmodified; only the hosting location
14
+ differs from the upstream litert-community/Gemma3-1B-IT repository.
15
+
16
+ By downloading the model through Cachy you agree to the Gemma Terms of Use and
17
+ Prohibited Use Policy above.
app/lib/data/services/local_ai/gemma_local_ai_service.dart CHANGED
@@ -12,10 +12,11 @@ import '../local_store.dart';
12
  import 'local_ai_service.dart';
13
 
14
  /// Gemma 3 1B IT, int4 .task build — the flutter_gemma-supported model with the
15
- /// best JSON-following per size. HF-gated: download needs a HuggingFace token
16
- /// with the Gemma license accepted.
 
17
  const kLocalAiModelUrl =
18
- 'https://huggingface.co/litert-community/Gemma3-1B-IT/resolve/main/gemma3-1b-it-int4.task';
19
  const kLocalAiModelFile = 'gemma3-1b-it-int4.task';
20
  const kLocalAiModelSizeLabel = '~550 MB';
21
 
@@ -41,8 +42,10 @@ class GemmaLocalAiService extends LocalAiService {
41
 
42
  static bool get _supported => !kIsWeb && Platform.isAndroid;
43
 
44
- @override
45
- Future<void> saveHfToken(String token) => _store.setHfToken(token);
 
 
46
 
47
  void _set(LocalAiStatus s) {
48
  _status = s;
@@ -53,6 +56,7 @@ class GemmaLocalAiService extends LocalAiService {
53
  if (!_supported) return;
54
  _set(const LocalAiStatus(LocalAiPhase.notInstalled));
55
  try {
 
56
  final installed = await FlutterGemma.isModelInstalled(kLocalAiModelFile);
57
  if (installed) _set(const LocalAiStatus(LocalAiPhase.ready));
58
  } catch (e) {
@@ -65,16 +69,20 @@ class GemmaLocalAiService extends LocalAiService {
65
  if (!_supported || _status.phase == LocalAiPhase.downloading) return;
66
  _set(const LocalAiStatus(LocalAiPhase.downloading));
67
  try {
 
68
  await FlutterGemma.installModel(modelType: ModelType.gemmaIt)
69
- .fromNetwork(kLocalAiModelUrl, token: _store.hfToken)
70
  .withProgress((int percent) {
71
  _set(LocalAiStatus(LocalAiPhase.downloading, progress: percent / 100));
72
  }).install();
73
  _set(const LocalAiStatus(LocalAiPhase.ready));
74
  } catch (e) {
75
  debugPrint('local-ai: download failed: $e');
 
 
76
  _set(LocalAiStatus(LocalAiPhase.error,
77
- message: 'Download failed — check connection and HF token'));
 
78
  }
79
  }
80
 
@@ -82,6 +90,7 @@ class GemmaLocalAiService extends LocalAiService {
82
  Future<void> delete() async {
83
  if (!_supported) return;
84
  try {
 
85
  await FlutterGemma.uninstallModel(kLocalAiModelFile);
86
  } catch (e) {
87
  debugPrint('local-ai: delete failed: $e');
@@ -98,6 +107,7 @@ class GemmaLocalAiService extends LocalAiService {
98
  if (!canStructure) return null;
99
  InferenceModel? model;
100
  try {
 
101
  model = await FlutterGemma.getActiveModel(maxTokens: 2048);
102
  final session = await model.createSession(temperature: 0.3, topK: 40);
103
  try {
 
12
  import 'local_ai_service.dart';
13
 
14
  /// Gemma 3 1B IT, int4 .task build — the flutter_gemma-supported model with the
15
+ /// best JSON-following per size. Self-hosted on our GitHub release so end users
16
+ /// need no HuggingFace account or license token (Gemma license permits
17
+ /// redistribution with its use-restrictions notice, shipped in NOTICE).
18
  const kLocalAiModelUrl =
19
+ 'https://github.com/Vatsal057/Cachy/releases/download/model-v1/gemma3-1b-it-int4.task';
20
  const kLocalAiModelFile = 'gemma3-1b-it-int4.task';
21
  const kLocalAiModelSizeLabel = '~550 MB';
22
 
 
42
 
43
  static bool get _supported => !kIsWeb && Platform.isAndroid;
44
 
45
+ /// flutter_gemma requires FlutterGemma.initialize() before any other call.
46
+ /// Lazy + memoized so app startup never pays for it when the feature is unused.
47
+ Future<void>? _gemmaInit;
48
+ Future<void> _ensureGemmaInit() => _gemmaInit ??= FlutterGemma.initialize();
49
 
50
  void _set(LocalAiStatus s) {
51
  _status = s;
 
56
  if (!_supported) return;
57
  _set(const LocalAiStatus(LocalAiPhase.notInstalled));
58
  try {
59
+ await _ensureGemmaInit();
60
  final installed = await FlutterGemma.isModelInstalled(kLocalAiModelFile);
61
  if (installed) _set(const LocalAiStatus(LocalAiPhase.ready));
62
  } catch (e) {
 
69
  if (!_supported || _status.phase == LocalAiPhase.downloading) return;
70
  _set(const LocalAiStatus(LocalAiPhase.downloading));
71
  try {
72
+ await _ensureGemmaInit();
73
  await FlutterGemma.installModel(modelType: ModelType.gemmaIt)
74
+ .fromNetwork(kLocalAiModelUrl)
75
  .withProgress((int percent) {
76
  _set(LocalAiStatus(LocalAiPhase.downloading, progress: percent / 100));
77
  }).install();
78
  _set(const LocalAiStatus(LocalAiPhase.ready));
79
  } catch (e) {
80
  debugPrint('local-ai: download failed: $e');
81
+ // Surface the real cause — a generic message made this undebuggable.
82
+ final detail = e.toString();
83
  _set(LocalAiStatus(LocalAiPhase.error,
84
+ message:
85
+ 'Download failed: ${detail.length > 220 ? detail.substring(0, 220) : detail}'));
86
  }
87
  }
88
 
 
90
  Future<void> delete() async {
91
  if (!_supported) return;
92
  try {
93
+ await _ensureGemmaInit();
94
  await FlutterGemma.uninstallModel(kLocalAiModelFile);
95
  } catch (e) {
96
  debugPrint('local-ai: delete failed: $e');
 
107
  if (!canStructure) return null;
108
  InferenceModel? model;
109
  try {
110
+ await _ensureGemmaInit();
111
  model = await FlutterGemma.getActiveModel(maxTokens: 2048);
112
  final session = await model.createSession(temperature: 0.3, topK: 40);
113
  try {
app/lib/data/services/local_ai/local_ai_service.dart CHANGED
@@ -44,9 +44,6 @@ abstract class LocalAiService extends ChangeNotifier {
44
  bool get canStructure =>
45
  enabled && status.phase == LocalAiPhase.ready;
46
 
47
- /// Persist the HuggingFace token used for the license-gated model download.
48
- Future<void> saveHfToken(String token);
49
-
50
  /// Download + install the model (~550 MB — caller shows the Wi-Fi warning).
51
  Future<void> download();
52
 
 
44
  bool get canStructure =>
45
  enabled && status.phase == LocalAiPhase.ready;
46
 
 
 
 
47
  /// Download + install the model (~550 MB — caller shows the Wi-Fi warning).
48
  Future<void> download();
49
 
app/lib/data/services/local_store.dart CHANGED
@@ -21,7 +21,6 @@ class LocalStore {
21
  static const _apiBaseUrlKey = 'api_base_url';
22
  static const _splitPaneFractionKey = 'split_pane_fraction';
23
  static const _localAiEnabledKey = 'local_ai_enabled';
24
- static const _hfTokenKey = 'hf_token';
25
 
26
  static Future<LocalStore> open() async =>
27
  LocalStore(await SharedPreferences.getInstance());
@@ -48,14 +47,6 @@ class LocalStore {
48
  bool get localAiEnabled => _prefs.getBool(_localAiEnabledKey) ?? false;
49
  Future<void> setLocalAiEnabled(bool v) => _prefs.setBool(_localAiEnabledKey, v);
50
 
51
- /// HuggingFace token for the license-gated local model download.
52
- String? get hfToken {
53
- final t = _prefs.getString(_hfTokenKey);
54
- return (t == null || t.isEmpty) ? null : t;
55
- }
56
-
57
- Future<void> setHfToken(String token) => _prefs.setString(_hfTokenKey, token);
58
-
59
  /// Clears user identity and onboarding state, effectively signing the user
60
  /// out and forcing them back through the name-entry gate on next launch.
61
  Future<void> clearUser() async {
 
21
  static const _apiBaseUrlKey = 'api_base_url';
22
  static const _splitPaneFractionKey = 'split_pane_fraction';
23
  static const _localAiEnabledKey = 'local_ai_enabled';
 
24
 
25
  static Future<LocalStore> open() async =>
26
  LocalStore(await SharedPreferences.getInstance());
 
47
  bool get localAiEnabled => _prefs.getBool(_localAiEnabledKey) ?? false;
48
  Future<void> setLocalAiEnabled(bool v) => _prefs.setBool(_localAiEnabledKey, v);
49
 
 
 
 
 
 
 
 
 
50
  /// Clears user identity and onboarding state, effectively signing the user
51
  /// out and forcing them back through the name-entry gate on next launch.
52
  Future<void> clearUser() async {
app/lib/ui/features/profile/views/profile_screen.dart CHANGED
@@ -478,30 +478,14 @@ class _OfflineAiSection extends StatelessWidget {
478
  }
479
 
480
  Future<void> _confirmDownload(BuildContext context, LocalAiService ai) async {
481
- final controller = TextEditingController();
482
  final ok = await showDialog<bool>(
483
  context: context,
484
  builder: (ctx) => AlertDialog(
485
  title: const Text('Download model?'),
486
- content: Column(
487
- mainAxisSize: MainAxisSize.min,
488
- crossAxisAlignment: CrossAxisAlignment.start,
489
- children: [
490
- const Text(
491
- '$kLocalAiModelSizeLabel download — Wi-Fi recommended. The model '
492
- 'is license-gated on HuggingFace: paste a free HF token that has '
493
- 'accepted the Gemma license.'),
494
- const SizedBox(height: 12),
495
- TextField(
496
- controller: controller,
497
- decoration: const InputDecoration(
498
- labelText: 'HuggingFace token (hf_…)',
499
- border: OutlineInputBorder(),
500
- ),
501
- obscureText: true,
502
- ),
503
- ],
504
- ),
505
  actions: [
506
  TextButton(
507
  onPressed: () => Navigator.pop(ctx, false),
@@ -513,8 +497,6 @@ class _OfflineAiSection extends StatelessWidget {
513
  ),
514
  );
515
  if (ok != true) return;
516
- final token = controller.text.trim();
517
- if (token.isNotEmpty) await ai.saveHfToken(token);
518
  await ai.download();
519
  }
520
 
 
478
  }
479
 
480
  Future<void> _confirmDownload(BuildContext context, LocalAiService ai) async {
 
481
  final ok = await showDialog<bool>(
482
  context: context,
483
  builder: (ctx) => AlertDialog(
484
  title: const Text('Download model?'),
485
+ content: const Text(
486
+ '$kLocalAiModelSizeLabel download — Wi-Fi recommended. Runs entirely '
487
+ 'on your phone once installed; no account or sign-in needed. Keep the '
488
+ 'app open until it finishes.'),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  actions: [
490
  TextButton(
491
  onPressed: () => Navigator.pop(ctx, false),
 
497
  ),
498
  );
499
  if (ok != true) return;
 
 
500
  await ai.download();
501
  }
502
 
app/test/local_ai_test.dart CHANGED
@@ -46,9 +46,6 @@ class FakeLocalAiService extends LocalAiService {
46
  notifyListeners();
47
  }
48
 
49
- @override
50
- Future<void> saveHfToken(String token) async {}
51
-
52
  @override
53
  Future<void> download() async {
54
  _phase = LocalAiPhase.downloading;
 
46
  notifyListeners();
47
  }
48
 
 
 
 
49
  @override
50
  Future<void> download() async {
51
  _phase = LocalAiPhase.downloading;