Kalibrierte Scan-Confidence + Cross-Model-Bonus
This commit is contained in:
@@ -82,6 +82,35 @@ describe('decideReviewOutcome', () => {
|
|||||||
expect(decision.reason).toBe('review-confirmed-primary');
|
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)', () => {
|
it('replaces with the agreeing review on a confidence tie (stronger model wins ties)', () => {
|
||||||
const decision = decideReviewOutcome({
|
const decision = decideReviewOutcome({
|
||||||
primaryResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.7),
|
primaryResult: ai('Swiss Cheese Plant', 'Monstera deliciosa', 0.7),
|
||||||
|
|||||||
166
app.json
166
app.json
@@ -1,87 +1,87 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "GreenLens",
|
"name": "GreenLens",
|
||||||
"slug": "greenlens",
|
"slug": "greenlens",
|
||||||
"version": "2.2.8",
|
"version": "2.2.9",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"scheme": "greenlens",
|
"scheme": "greenlens",
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/transparent.png",
|
"image": "./assets/transparent.png",
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#111813"
|
"backgroundColor": "#111813"
|
||||||
},
|
},
|
||||||
"assetBundlePatterns": [
|
"assetBundlePatterns": [
|
||||||
"**/*"
|
"**/*"
|
||||||
],
|
],
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"usesAppleSignIn": true,
|
"usesAppleSignIn": true,
|
||||||
"bundleIdentifier": "com.greenlens.app",
|
"bundleIdentifier": "com.greenlens.app",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSCameraUsageDescription": "GreenLens needs camera access to identify plants.",
|
"NSCameraUsageDescription": "GreenLens needs camera access to identify plants.",
|
||||||
"NSPhotoLibraryUsageDescription": "GreenLens needs photo library access to identify plants from your gallery.",
|
"NSPhotoLibraryUsageDescription": "GreenLens needs photo library access to identify plants from your gallery.",
|
||||||
"ITSAppUsesNonExemptEncryption": false
|
"ITSAppUsesNonExemptEncryption": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
"backgroundColor": "#111813"
|
"backgroundColor": "#111813"
|
||||||
},
|
},
|
||||||
"package": "com.greenlens.app",
|
"package": "com.greenlens.app",
|
||||||
"versionCode": 5,
|
"versionCode": 5,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"android.permission.CAMERA",
|
"android.permission.CAMERA",
|
||||||
"android.permission.RECORD_AUDIO"
|
"android.permission.RECORD_AUDIO"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"runtimeVersion": {
|
"runtimeVersion": {
|
||||||
"policy": "appVersion"
|
"policy": "appVersion"
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"url": "https://u.expo.dev/f0c92b2e-a952-4cfe-9754-7d7222b76969"
|
"url": "https://u.expo.dev/f0c92b2e-a952-4cfe-9754-7d7222b76969"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-dev-client",
|
"expo-dev-client",
|
||||||
"expo-router",
|
"expo-router",
|
||||||
[
|
[
|
||||||
"expo-share-intent",
|
"expo-share-intent",
|
||||||
{
|
{
|
||||||
"iosActivationRules": {
|
"iosActivationRules": {
|
||||||
"NSExtensionActivationSupportsText": true,
|
"NSExtensionActivationSupportsText": true,
|
||||||
"NSExtensionActivationSupportsWebURLWithMaxCount": 1,
|
"NSExtensionActivationSupportsWebURLWithMaxCount": 1,
|
||||||
"NSExtensionActivationSupportsWebPageWithMaxCount": 1,
|
"NSExtensionActivationSupportsWebPageWithMaxCount": 1,
|
||||||
"NSExtensionActivationSupportsImageWithMaxCount": 1
|
"NSExtensionActivationSupportsImageWithMaxCount": 1
|
||||||
},
|
},
|
||||||
"androidIntentFilters": ["text/*", "image/*"],
|
"androidIntentFilters": ["text/*", "image/*"],
|
||||||
"iosShareExtensionName": "GreenLens Share",
|
"iosShareExtensionName": "GreenLens Share",
|
||||||
"iosAppGroupIdentifier": "group.com.greenlens.app",
|
"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<nodes.length;i++){callback(nodes[i])}} var glCandidates=[];['og:image','og:image:url','og:image:secure_url','twitter:image','twitter:image:src','image'].forEach(function(key){glAddCandidate(glCandidates,metas[key])}); glEach('meta[itemprop=\"image\"]',function(meta){glAddCandidate(glCandidates,meta.getAttribute('content'))}); glEach('img',function(img){glAddCandidate(glCandidates,img.currentSrc);glAddCandidate(glCandidates,img.src);glAddCandidate(glCandidates,img.getAttribute('data-src'));glAddCandidate(glCandidates,img.getAttribute('data-original'));glAddCandidate(glCandidates,img.getAttribute('data-lazy-src'));glAddCandidate(glCandidates,glSrcsetCandidate(img.getAttribute('srcset')))}); glEach('picture source,source',function(source){glAddCandidate(glCandidates,source.src);glAddCandidate(glCandidates,source.getAttribute('src'));glAddCandidate(glCandidates,glSrcsetCandidate(source.getAttribute('srcset')))}); glEach('video',function(video){glAddCandidate(glCandidates,video.getAttribute('poster'))}); glEach('[style*=\"background-image\"]',function(el){var bg=(el.style&&el.style.backgroundImage)||'';var match=bg.match(/url\\([\"']?([^\"')]+)[\"']?\\)/);if(match){glAddCandidate(glCandidates,match[1])}});metas['greenlens:imageCandidates']=JSON.stringify(glCandidates.slice(0,12));metas['greenlens:imageCandidateCount']=String(glCandidates.length);metas['og:image']=metas['og:image']||glCandidates[0]}catch(e){metas['greenlens:preprocessorError']=String(e)}"
|
"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<nodes.length;i++){callback(nodes[i])}} var glCandidates=[];['og:image','og:image:url','og:image:secure_url','twitter:image','twitter:image:src','image'].forEach(function(key){glAddCandidate(glCandidates,metas[key])}); glEach('meta[itemprop=\"image\"]',function(meta){glAddCandidate(glCandidates,meta.getAttribute('content'))}); glEach('img',function(img){glAddCandidate(glCandidates,img.currentSrc);glAddCandidate(glCandidates,img.src);glAddCandidate(glCandidates,img.getAttribute('data-src'));glAddCandidate(glCandidates,img.getAttribute('data-original'));glAddCandidate(glCandidates,img.getAttribute('data-lazy-src'));glAddCandidate(glCandidates,glSrcsetCandidate(img.getAttribute('srcset')))}); glEach('picture source,source',function(source){glAddCandidate(glCandidates,source.src);glAddCandidate(glCandidates,source.getAttribute('src'));glAddCandidate(glCandidates,glSrcsetCandidate(source.getAttribute('srcset')))}); glEach('video',function(video){glAddCandidate(glCandidates,video.getAttribute('poster'))}); glEach('[style*=\"background-image\"]',function(el){var bg=(el.style&&el.style.backgroundImage)||'';var match=bg.match(/url\\([\"']?([^\"')]+)[\"']?\\)/);if(match){glAddCandidate(glCandidates,match[1])}});metas['greenlens:imageCandidates']=JSON.stringify(glCandidates.slice(0,12));metas['greenlens:imageCandidateCount']=String(glCandidates.length);metas['og:image']=metas['og:image']||glCandidates[0]}catch(e){metas['greenlens:preprocessorError']=String(e)}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-camera",
|
"expo-camera",
|
||||||
"expo-apple-authentication",
|
"expo-apple-authentication",
|
||||||
"expo-image-picker",
|
"expo-image-picker",
|
||||||
"expo-secure-store",
|
"expo-secure-store",
|
||||||
"expo-asset",
|
"expo-asset",
|
||||||
"expo-font",
|
"expo-font",
|
||||||
"expo-notifications",
|
"expo-notifications",
|
||||||
[
|
[
|
||||||
"expo-splash-screen",
|
"expo-splash-screen",
|
||||||
{
|
{
|
||||||
"image": "./assets/transparent.png",
|
"image": "./assets/transparent.png",
|
||||||
"imageWidth": 160,
|
"imageWidth": 160,
|
||||||
"backgroundColor": "#111813"
|
"backgroundColor": "#111813"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"extra": {
|
"extra": {
|
||||||
"router": {},
|
"router": {},
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "f0c92b2e-a952-4cfe-9754-7d7222b76969"
|
"projectId": "f0c92b2e-a952-4cfe-9754-7d7222b76969"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "greenlens",
|
"name": "greenlens",
|
||||||
"version": "2.2.7",
|
"version": "2.2.9",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -822,6 +822,10 @@ app.post('/v1/scan', async (request, response) => {
|
|||||||
modelUsed = openAiReview.modelUsed || modelUsed;
|
modelUsed = openAiReview.modelUsed || modelUsed;
|
||||||
if (grounded.grounded) modelPath.push('catalog-grounded-review');
|
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('openai-review');
|
||||||
modelPath.push(decision.reason);
|
modelPath.push(decision.reason);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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".',
|
: '- "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.',
|
'- "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.").',
|
'- If species is uncertain, prefer genus-level naming (for example: "Calathea sp.").',
|
||||||
'- "confidence" must be between 0 and 1.',
|
'- "confidence" must be between 0 and 1 and reflect how certain the species identification is. Calibrate it:',
|
||||||
'- Keep confidence <= 0.55 when the image is ambiguous, blurred, or partially visible.',
|
' - 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.',
|
'- "waterIntervalDays" must be an integer between 1 and 45.',
|
||||||
'- Do not include markdown, explanations, or extra keys.',
|
'- Do not include markdown, explanations, or extra keys.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
const { normalizeText } = require('./scanGrounding');
|
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:
|
// 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
|
// catalog grounding has a genus-level fallback that can collapse two different
|
||||||
// species onto the same catalog entry and fake an agreement.
|
// species onto the same catalog entry and fake an agreement.
|
||||||
@@ -30,6 +38,11 @@ const decideReviewOutcome = ({ primaryResult, reviewResult, agrees }) => {
|
|||||||
accept: true,
|
accept: true,
|
||||||
replace: reviewConfidence >= primaryConfidence,
|
replace: reviewConfidence >= primaryConfidence,
|
||||||
reason: 'review-confirmed-primary',
|
reason: 'review-confirmed-primary',
|
||||||
|
confidence: clamp(
|
||||||
|
Math.max(primaryConfidence, reviewConfidence) + REVIEW_AGREEMENT_BONUS,
|
||||||
|
0.05,
|
||||||
|
REVIEW_AGREEMENT_CONFIDENCE_CAP,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user