diff --git a/app.json b/app.json index 6598584..430e94d 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "GreenLens", "slug": "greenlens", - "version": "2.1.6", + "version": "2.2.1", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", @@ -68,4 +68,4 @@ } } } -} +} diff --git a/app/scanner.tsx b/app/scanner.tsx index dc87e42..8b4af51 100644 --- a/app/scanner.tsx +++ b/app/scanner.tsx @@ -7,6 +7,7 @@ import { Ionicons } from '@expo/vector-icons'; import { CameraView, useCameraPermissions } from 'expo-camera'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; +import * as ImageManipulator from 'expo-image-manipulator'; import * as Haptics from 'expo-haptics'; import { usePostHog } from 'posthog-react-native'; import { useApp } from '../context/AppContext'; @@ -175,6 +176,20 @@ export default function ScannerScreen() { }; }, [isAnalyzing, scanLineProgress, scanPulse]); + const resizeForAnalysis = async (uri: string): Promise => { + if (uri.startsWith('data:')) return uri; + try { + const result = await ImageManipulator.manipulateAsync( + uri, + [{ resize: { width: 1024 } }], + { compress: 0.6, format: ImageManipulator.SaveFormat.JPEG, base64: true }, + ); + return result.base64 ? `data:image/jpeg;base64,${result.base64}` : result.uri; + } catch { + return uri; + } + }; + const analyzeImage = async (imageUri: string, galleryImageUri?: string) => { if (isAnalyzing) return; @@ -342,7 +357,7 @@ export default function ScannerScreen() { const takePicture = async () => { if (!cameraRef.current || isAnalyzing) return; await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - const photo = await cameraRef.current.takePictureAsync({ base64: true, quality: 0.7 }); + const photo = await cameraRef.current.takePictureAsync({ base64: true, quality: 0.5 }); if (photo) { const analysisUri = photo.base64 ? `data:image/jpeg;base64,${photo.base64}` @@ -358,17 +373,14 @@ export default function ScannerScreen() { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], - quality: 0.7, - base64: true, + quality: 1, + base64: false, }); if (!result.canceled && result.assets[0]) { const asset = result.assets[0]; - const uri = asset.base64 - ? `data:image/jpeg;base64,${asset.base64}` - : asset.uri; - - setSelectedImage(uri); - analyzeImage(uri, asset.uri || uri); + const analysisUri = await resizeForAnalysis(asset.uri); + setSelectedImage(asset.uri); + analyzeImage(analysisUri, asset.uri); } }; diff --git a/components/ResultCard.tsx b/components/ResultCard.tsx index d555443..3e1d804 100644 --- a/components/ResultCard.tsx +++ b/components/ResultCard.tsx @@ -99,9 +99,9 @@ export const ResultCard: React.FC = ({ {[ - { icon: 'water' as const, label: t.water, value: result.careInfo.waterIntervalDays <= 7 ? t.waterModerate : t.waterLittle, color: colors.info, bg: colors.infoSoft }, - { icon: 'sunny' as const, label: t.light, value: result.careInfo.light, color: colors.warning, bg: colors.warningSoft }, - { icon: 'thermometer' as const, label: t.temp, value: result.careInfo.temp, color: colors.danger, bg: colors.dangerSoft }, + { icon: 'water' as const, label: t.water, value: t.waterEveryXDays.replace('{0}', result.careInfo.waterIntervalDays.toString()), color: colors.info, bg: colors.infoSoft }, + { icon: 'sunny' as const, label: t.light, value: result.careInfo.light || t.unknown, color: colors.warning, bg: colors.warningSoft }, + { icon: 'thermometer' as const, label: t.temp, value: result.careInfo.temp || t.unknown, color: colors.danger, bg: colors.dangerSoft }, ].map((item) => ( @@ -118,8 +118,8 @@ export const ResultCard: React.FC = ({ {t.detailedCare} {[ { text: t.careTextWater.replace('{0}', result.careInfo.waterIntervalDays.toString()), color: colors.success }, - { text: t.careTextLight.replace('{0}', result.careInfo.light), color: colors.warning }, - { text: t.careTextTemp.replace('{0}', result.careInfo.temp), color: colors.danger }, + { text: t.careTextLight.replace('{0}', result.careInfo.light || t.unknown), color: colors.warning }, + { text: t.careTextTemp.replace('{0}', result.careInfo.temp || t.unknown), color: colors.danger }, ].map((item, i) => ( diff --git a/server/lib/openai.js b/server/lib/openai.js index ab9acd7..2ceb65a 100644 --- a/server/lib/openai.js +++ b/server/lib/openai.js @@ -107,10 +107,16 @@ const normalizeIdentifyResult = (raw, language) => { const waterIntervalRaw = getNumber(careInfoRaw.waterIntervalDays); const light = getString(careInfoRaw.light); const temp = getString(careInfoRaw.temp); - if (waterIntervalRaw == null || !light || !temp) { + if (waterIntervalRaw == null) { return null; } + const LIGHT_DEFAULTS = { de: 'Helles indirektes Licht', es: 'Luz indirecta brillante', en: 'Bright indirect light' }; + const TEMP_DEFAULTS = { de: '15–25 °C', es: '15–25 °C', en: '59–77 °F (15–25 °C)' }; + const lang = language || 'en'; + const resolvedLight = (light && light !== 'Unknown') ? light : (LIGHT_DEFAULTS[lang] || LIGHT_DEFAULTS.en); + const resolvedTemp = (temp && temp !== 'Unknown') ? temp : (TEMP_DEFAULTS[lang] || TEMP_DEFAULTS.en); + const fallbackDescription = language === 'de' ? `${name} wurde per KI erkannt. Pflegehinweise sind unten aufgefuehrt.` : language === 'es' @@ -124,8 +130,8 @@ const normalizeIdentifyResult = (raw, language) => { description: description || fallbackDescription, careInfo: { waterIntervalDays: Math.round(clamp(waterIntervalRaw, 1, 45)), - light, - temp, + light: resolvedLight, + temp: resolvedTemp, }, }; }; @@ -216,6 +222,10 @@ const buildIdentifyPrompt = (language, mode) => { 'Rules:', nameLanguageInstruction, `- "description" and "careInfo.light" must be written in ${getLanguageLabel(language)}.`, + `- "careInfo.light": short light requirement in ${getLanguageLabel(language)} (e.g. "bright indirect light", "full sun", "partial shade"). Must always be a real value, never "Unknown".`, + language === 'en' + ? '- "careInfo.temp": temperature range in both Celsius and Fahrenheit (e.g. "18–24 °C (64–75 °F)"). Must always be a real plant-specific value, never "Unknown".' + : '- "careInfo.temp": temperature range in Celsius (e.g. "18–24 °C"). Must always be a real plant-specific value, never "Unknown".', '- "botanicalName" must use accepted Latin scientific naming and must not be invented or misspelled.', '- If species is uncertain, prefer genus-level naming (for example: "Calathea sp.").', '- "confidence" must be between 0 and 1.', @@ -356,7 +366,7 @@ const identifyPlant = async ({ imageUri, language, mode = 'primary', plan = 'fre role: 'user', content: [ { type: 'text', text: buildIdentifyPrompt(language, mode) }, - { type: 'image_url', image_url: { url: imageUri } }, + { type: 'image_url', image_url: { url: imageUri, detail: 'low' } }, ], }, ], @@ -416,7 +426,7 @@ const analyzePlantHealth = async ({ imageUri, language, plantContext }) => { role: 'user', content: [ { type: 'text', text: buildHealthPrompt(language, plantContext) }, - { type: 'image_url', image_url: { url: imageUri } }, + { type: 'image_url', image_url: { url: imageUri, detail: 'low' } }, ], }, ], diff --git a/utils/translations.ts b/utils/translations.ts index 46f0b9e..6041344 100644 --- a/utils/translations.ts +++ b/utils/translations.ts @@ -145,6 +145,7 @@ export const translations = { temp: "Temperatur", // Care Values (UI Helper) + unknown: "Unbekannt", waterModerate: "Mäßig", waterLittle: "Wenig", waterEveryXDays: "Alle {0} Tage", @@ -367,6 +368,7 @@ registerToSave: "Sign up to save", light: "Light", temp: "Temperature", + unknown: "Unknown", waterModerate: "Moderate", waterLittle: "Little", waterEveryXDays: "Every {0} days", @@ -588,6 +590,7 @@ registerToSave: "Regístrate para guardar", light: "Luz", temp: "Temperatura", + unknown: "Desconocido", waterModerate: "Moderado", waterLittle: "Poco", waterEveryXDays: "Cada {0} días",