- Cloud Function (europe-west3): PlantNet-Erkennung mit Claude-Fallback, deutsches Pflegeprofil von Claude, Keys als Secrets
- Pflanzen-Formular: Foto aufnehmen/wählen, Felder werden automatisch vorbefüllt
- Fotos in Storage unter households/{id}/plant-photos, Anzeige in Liste und Profil
- storage.rules mit Haushalts-Prinzip, iOS-Berechtigungstexte für Kamera/Fotos
- Doku Schritt 5: Storage aktivieren, PlantNet-Key, Secrets setzen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85 lines
2.9 KiB
Dart
85 lines
2.9 KiB
Dart
import 'dart:convert';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:cloud_functions/cloud_functions.dart';
|
||
import 'package:firebase_storage/firebase_storage.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import '../../household/data/household_providers.dart';
|
||
|
||
/// Ergebnis der KI-Erkennung (PlantNet + Claude über die Cloud Function).
|
||
class RecognitionResult {
|
||
const RecognitionResult({
|
||
required this.scientificName,
|
||
required this.germanName,
|
||
required this.confidence,
|
||
required this.description,
|
||
required this.careNotes,
|
||
required this.wateringIntervalDays,
|
||
required this.fertilizingIntervalDays,
|
||
});
|
||
|
||
final String scientificName;
|
||
final String germanName;
|
||
final double confidence;
|
||
final String description;
|
||
final String careNotes;
|
||
final int wateringIntervalDays;
|
||
final int fertilizingIntervalDays;
|
||
|
||
/// Anzeige-Name: "Fensterblatt (Monstera deliciosa)".
|
||
String get displaySpecies =>
|
||
germanName.isNotEmpty ? '$germanName ($scientificName)' : scientificName;
|
||
}
|
||
|
||
class PlantRecognitionService {
|
||
PlantRecognitionService(this._functions, this._storage, this._householdIdGetter);
|
||
|
||
final FirebaseFunctions _functions;
|
||
final FirebaseStorage _storage;
|
||
final String? Function() _householdIdGetter;
|
||
|
||
/// Ruft die Cloud Function auf (PlantNet + Claude) – dauert einige Sekunden.
|
||
Future<RecognitionResult> identify(Uint8List imageBytes) async {
|
||
final callable = _functions.httpsCallable('identifyPlant');
|
||
final response = await callable.call<Map<String, dynamic>>({
|
||
'imageBase64': base64Encode(imageBytes),
|
||
});
|
||
final data = response.data;
|
||
return RecognitionResult(
|
||
scientificName: data['scientificName'] as String? ?? '',
|
||
germanName: data['germanName'] as String? ?? '',
|
||
confidence: (data['confidence'] as num?)?.toDouble() ?? 0,
|
||
description: data['description'] as String? ?? '',
|
||
careNotes: data['careNotes'] as String? ?? '',
|
||
wateringIntervalDays: (data['wateringIntervalDays'] as num?)?.toInt() ?? 7,
|
||
fertilizingIntervalDays:
|
||
(data['fertilizingIntervalDays'] as num?)?.toInt() ?? 28,
|
||
);
|
||
}
|
||
|
||
/// Lädt das Foto in den Haushalts-Ordner in Storage und liefert die URL.
|
||
Future<String> uploadPhoto(Uint8List imageBytes) async {
|
||
final householdId = _householdIdGetter();
|
||
if (householdId == null) {
|
||
throw StateError('Kein Haushalt geladen – Aktion nicht möglich.');
|
||
}
|
||
final ref = _storage.ref(
|
||
'households/$householdId/plant-photos/'
|
||
'${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||
);
|
||
await ref.putData(
|
||
imageBytes,
|
||
SettableMetadata(contentType: 'image/jpeg'),
|
||
);
|
||
return ref.getDownloadURL();
|
||
}
|
||
}
|
||
|
||
final plantRecognitionServiceProvider = Provider<PlantRecognitionService>((ref) {
|
||
return PlantRecognitionService(
|
||
FirebaseFunctions.instanceFor(region: 'europe-west3'),
|
||
FirebaseStorage.instance,
|
||
() => ref.read(householdIdProvider).value,
|
||
);
|
||
});
|