From 538344ca0cd3f1fe7bde4cd0baf01c43c5885034 Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Sun, 5 Jul 2026 20:00:45 +0200 Subject: [PATCH] Kalibrierte Scan-Confidence + Cross-Model-Bonus --- __tests__/server/scanReview.test.js | 29 +++++ app.json | 166 ++++++++++++++-------------- package.json | 2 +- server/index.js | 4 + server/lib/openai.js | 7 +- server/lib/scanReview.js | 13 +++ 6 files changed, 135 insertions(+), 86 deletions(-) diff --git a/__tests__/server/scanReview.test.js b/__tests__/server/scanReview.test.js index 4a6917d..88c5b96 100644 --- a/__tests__/server/scanReview.test.js +++ b/__tests__/server/scanReview.test.js @@ -82,6 +82,35 @@ describe('decideReviewOutcome', () => { expect(decision.reason).toBe('review-confirmed-primary'); }); + it('boosts confidence when two different models agree on the species', () => { + // The 55%-forever bug: primary 0.55 + agreeing review 0.54 must NOT stay + // at 0.55 — independent cross-model agreement is real evidence. + const decision = decideReviewOutcome({ + primaryResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.55), + reviewResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.54), + agrees: true, + }); + expect(decision.confidence).toBeCloseTo(0.75, 5); + }); + + it('caps the agreement-boosted confidence at 0.97', () => { + const decision = decideReviewOutcome({ + primaryResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.95), + reviewResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.9), + agrees: true, + }); + expect(decision.confidence).toBe(0.97); + }); + + it('gives no confidence bonus on disagreement', () => { + const decision = decideReviewOutcome({ + primaryResult: ai('Rose', 'Rosa chinensis', 0.5), + reviewResult: ai('Sacred lotus', 'Nelumbo nucifera', 0.6), + agrees: false, + }); + expect(decision.confidence).toBeUndefined(); + }); + it('replaces with the agreeing review on a confidence tie (stronger model wins ties)', () => { const decision = decideReviewOutcome({ primaryResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.7), diff --git a/app.json b/app.json index b836cf1..5052983 100644 --- a/app.json +++ b/app.json @@ -1,87 +1,87 @@ -{ - "expo": { - "name": "GreenLens", - "slug": "greenlens", - "version": "2.2.8", - "orientation": "portrait", - "icon": "./assets/icon.png", - "userInterfaceStyle": "automatic", - "scheme": "greenlens", - "splash": { - "image": "./assets/transparent.png", - "resizeMode": "contain", - "backgroundColor": "#111813" - }, - "assetBundlePatterns": [ - "**/*" - ], - "ios": { +{ + "expo": { + "name": "GreenLens", + "slug": "greenlens", + "version": "2.2.9", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "automatic", + "scheme": "greenlens", + "splash": { + "image": "./assets/transparent.png", + "resizeMode": "contain", + "backgroundColor": "#111813" + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { "supportsTablet": true, "usesAppleSignIn": true, "bundleIdentifier": "com.greenlens.app", "infoPlist": { - "NSCameraUsageDescription": "GreenLens needs camera access to identify plants.", - "NSPhotoLibraryUsageDescription": "GreenLens needs photo library access to identify plants from your gallery.", - "ITSAppUsesNonExemptEncryption": false - } - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#111813" - }, - "package": "com.greenlens.app", - "versionCode": 5, - "permissions": [ - "android.permission.CAMERA", - "android.permission.RECORD_AUDIO" - ] - }, - "runtimeVersion": { - "policy": "appVersion" - }, - "updates": { - "url": "https://u.expo.dev/f0c92b2e-a952-4cfe-9754-7d7222b76969" - }, - "plugins": [ - "expo-dev-client", - "expo-router", - [ - "expo-share-intent", - { - "iosActivationRules": { - "NSExtensionActivationSupportsText": true, - "NSExtensionActivationSupportsWebURLWithMaxCount": 1, - "NSExtensionActivationSupportsWebPageWithMaxCount": 1, - "NSExtensionActivationSupportsImageWithMaxCount": 1 - }, - "androidIntentFilters": ["text/*", "image/*"], - "iosShareExtensionName": "GreenLens Share", - "iosAppGroupIdentifier": "group.com.greenlens.app", - "preprocessorInjectJS": "try{function glAddCandidate(list,value){if(value&&typeof value==='string'&&list.indexOf(value)===-1){list.push(value)}} function glSrcsetCandidate(value){if(!value||typeof value!=='string')return null;var parts=value.split(',').map(function(item){return item.trim().split(/\\s+/)[0]}).filter(Boolean);return parts.length?parts[parts.length-1]:null} function glEach(selector,callback){var nodes=document.querySelectorAll(selector);for(var i=0;i { modelUsed = openAiReview.modelUsed || modelUsed; if (grounded.grounded) modelPath.push('catalog-grounded-review'); } + if (decision.confidence != null) { + // Cross-model agreement bonus: both models named the same species. + result = { ...result, confidence: decision.confidence }; + } modelPath.push('openai-review'); modelPath.push(decision.reason); } else { diff --git a/server/lib/openai.js b/server/lib/openai.js index c2dfd2a..9161dc0 100644 --- a/server/lib/openai.js +++ b/server/lib/openai.js @@ -243,8 +243,11 @@ const buildIdentifyPrompt = (language, mode) => { : '- "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.', - '- Keep confidence <= 0.55 when the image is ambiguous, blurred, or partially visible.', + '- "confidence" must be between 0 and 1 and reflect how certain the species identification is. Calibrate it:', + ' - 0.85-0.95: species clearly recognizable, distinctive features (leaf shape, flower, pattern) plainly visible.', + ' - 0.65-0.84: species very likely, but some distinguishing features are hidden or similar species exist.', + ' - 0.40-0.64: image is ambiguous, blurred, partially visible, or several species fit equally well.', + ' - Below 0.40: mostly guessing; prefer genus-level naming instead.', '- "waterIntervalDays" must be an integer between 1 and 45.', '- Do not include markdown, explanations, or extra keys.', ].join('\n'); diff --git a/server/lib/scanReview.js b/server/lib/scanReview.js index 4413176..99de82e 100644 --- a/server/lib/scanReview.js +++ b/server/lib/scanReview.js @@ -1,5 +1,13 @@ const { normalizeText } = require('./scanGrounding'); +const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); + +// Two DIFFERENT models independently naming the same species is genuine +// evidence beyond either model's self-reported confidence, so agreement +// earns a bonus on top of the better single estimate. +const REVIEW_AGREEMENT_BONUS = 0.2; +const REVIEW_AGREEMENT_CONFIDENCE_CAP = 0.97; + // Agreement must be judged on the RAW model answers, not the grounded ones: // catalog grounding has a genus-level fallback that can collapse two different // species onto the same catalog entry and fake an agreement. @@ -30,6 +38,11 @@ const decideReviewOutcome = ({ primaryResult, reviewResult, agrees }) => { accept: true, replace: reviewConfidence >= primaryConfidence, reason: 'review-confirmed-primary', + confidence: clamp( + Math.max(primaryConfidence, reviewConfidence) + REVIEW_AGREEMENT_BONUS, + 0.05, + REVIEW_AGREEMENT_CONFIDENCE_CAP, + ), }; }