leafittome/lib/features/plants/data/plant_recognition_service.dart
cschlaefke 41e31bed88 Foto-Flow mit KI-Erkennung: Kamera, Cloud Function identifyPlant, Storage für Fotos
- 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>
2026-07-19 00:05:46 +02:00

85 lines
2.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
);
});