Spaces:
Sleeping
Sleeping
outshine84 commited on
Commit ·
69bb39e
1
Parent(s): 5d06145
fix threashold
Browse files- api.py +40 -4
- pwa-app/src/App.jsx +2 -11
api.py
CHANGED
|
@@ -223,6 +223,39 @@ def _normalize_image_path(raw_path: str) -> str:
|
|
| 223 |
# ---------------------------------------------------------------------------
|
| 224 |
|
| 225 |
FAISS_CONFIDENCE_THRESHOLD = float(os.getenv("FAISS_CONFIDENCE_THRESHOLD", "0.82"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
|
| 228 |
def _gpt_vision_identify_plant(
|
|
@@ -1291,11 +1324,13 @@ async def search_similar(
|
|
| 1291 |
gpt_job_status: dict[str, Any] | None = None
|
| 1292 |
gpt_fallback_attempted = False
|
| 1293 |
gpt_fallback_reason = "not_attempted"
|
| 1294 |
-
|
|
|
|
| 1295 |
gpt_fallback_attempted = True
|
| 1296 |
logger.info(
|
| 1297 |
-
|
| 1298 |
-
f"
|
|
|
|
| 1299 |
)
|
| 1300 |
fallback_candidates = [species for species, _, _ in results[:12]]
|
| 1301 |
gpt_species, gpt_fallback_reason = _gpt_vision_identify_plant(
|
|
@@ -1314,7 +1349,7 @@ async def search_similar(
|
|
| 1314 |
results = results[:k]
|
| 1315 |
else:
|
| 1316 |
logger.info(f"GPT fallback attempted but no species accepted: {gpt_fallback_reason}")
|
| 1317 |
-
elif
|
| 1318 |
gpt_fallback_reason = "OPENAI_API_KEY missing"
|
| 1319 |
|
| 1320 |
except RuntimeError as e:
|
|
@@ -1351,6 +1386,7 @@ async def search_similar(
|
|
| 1351 |
"gpt_fallback_attempted": gpt_fallback_attempted if 'gpt_fallback_attempted' in dir() else False,
|
| 1352 |
"gpt_fallback_used": gpt_species is not None if 'gpt_species' in dir() else False,
|
| 1353 |
"gpt_fallback_reason": gpt_fallback_reason if 'gpt_fallback_reason' in dir() else "not_attempted",
|
|
|
|
| 1354 |
"gpt_job_status": gpt_job_status if 'gpt_job_status' in dir() else None,
|
| 1355 |
"species_found": [species for species, _, _ in results],
|
| 1356 |
"scores": [float(score) for _, score, _ in results],
|
|
|
|
| 223 |
# ---------------------------------------------------------------------------
|
| 224 |
|
| 225 |
FAISS_CONFIDENCE_THRESHOLD = float(os.getenv("FAISS_CONFIDENCE_THRESHOLD", "0.82"))
|
| 226 |
+
FAISS_AMBIGUITY_MARGIN = float(os.getenv("FAISS_AMBIGUITY_MARGIN", "0.015"))
|
| 227 |
+
RRF_AMBIGUITY_MARGIN = float(os.getenv("RRF_AMBIGUITY_MARGIN", "0.0015"))
|
| 228 |
+
FORCE_OPENAI_FALLBACK = os.getenv("FORCE_OPENAI_FALLBACK", "0").strip().lower() in {
|
| 229 |
+
"1", "true", "yes", "on"
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _should_trigger_gpt_fallback(top_score: float, results: list[tuple[str, float, list]]) -> tuple[bool, str]:
|
| 234 |
+
"""Decide whether GPT vision fallback should run.
|
| 235 |
+
|
| 236 |
+
Triggers on low FAISS confidence, explicit force flag, or very ambiguous top-vs-second gap.
|
| 237 |
+
"""
|
| 238 |
+
if FORCE_OPENAI_FALLBACK:
|
| 239 |
+
return True, "forced_by_env"
|
| 240 |
+
|
| 241 |
+
if top_score < FAISS_CONFIDENCE_THRESHOLD:
|
| 242 |
+
return True, "low_top_score"
|
| 243 |
+
|
| 244 |
+
if len(results) < 2:
|
| 245 |
+
return False, "single_result"
|
| 246 |
+
|
| 247 |
+
top_result_score = float(results[0][1])
|
| 248 |
+
second_result_score = float(results[1][1])
|
| 249 |
+
gap = max(0.0, top_result_score - second_result_score)
|
| 250 |
+
rrf_like = top_result_score <= 0.1 and second_result_score <= 0.1
|
| 251 |
+
|
| 252 |
+
if rrf_like and gap < RRF_AMBIGUITY_MARGIN:
|
| 253 |
+
return True, "ambiguous_rrf_gap"
|
| 254 |
+
|
| 255 |
+
if (not rrf_like) and gap < FAISS_AMBIGUITY_MARGIN:
|
| 256 |
+
return True, "ambiguous_similarity_gap"
|
| 257 |
+
|
| 258 |
+
return False, "high_confidence"
|
| 259 |
|
| 260 |
|
| 261 |
def _gpt_vision_identify_plant(
|
|
|
|
| 1324 |
gpt_job_status: dict[str, Any] | None = None
|
| 1325 |
gpt_fallback_attempted = False
|
| 1326 |
gpt_fallback_reason = "not_attempted"
|
| 1327 |
+
should_trigger_gpt, gpt_trigger_basis = _should_trigger_gpt_fallback(top_planclef_score, results)
|
| 1328 |
+
if should_trigger_gpt and api_key:
|
| 1329 |
gpt_fallback_attempted = True
|
| 1330 |
logger.info(
|
| 1331 |
+
"Activating GPT-4o vision fallback: "
|
| 1332 |
+
f"basis={gpt_trigger_basis}, top_planclef_score={top_planclef_score:.4f}, "
|
| 1333 |
+
f"threshold={FAISS_CONFIDENCE_THRESHOLD}"
|
| 1334 |
)
|
| 1335 |
fallback_candidates = [species for species, _, _ in results[:12]]
|
| 1336 |
gpt_species, gpt_fallback_reason = _gpt_vision_identify_plant(
|
|
|
|
| 1349 |
results = results[:k]
|
| 1350 |
else:
|
| 1351 |
logger.info(f"GPT fallback attempted but no species accepted: {gpt_fallback_reason}")
|
| 1352 |
+
elif should_trigger_gpt:
|
| 1353 |
gpt_fallback_reason = "OPENAI_API_KEY missing"
|
| 1354 |
|
| 1355 |
except RuntimeError as e:
|
|
|
|
| 1386 |
"gpt_fallback_attempted": gpt_fallback_attempted if 'gpt_fallback_attempted' in dir() else False,
|
| 1387 |
"gpt_fallback_used": gpt_species is not None if 'gpt_species' in dir() else False,
|
| 1388 |
"gpt_fallback_reason": gpt_fallback_reason if 'gpt_fallback_reason' in dir() else "not_attempted",
|
| 1389 |
+
"gpt_trigger_basis": gpt_trigger_basis if 'gpt_trigger_basis' in dir() else "not_evaluated",
|
| 1390 |
"gpt_job_status": gpt_job_status if 'gpt_job_status' in dir() else None,
|
| 1391 |
"species_found": [species for species, _, _ in results],
|
| 1392 |
"scores": [float(score) for _, score, _ in results],
|
pwa-app/src/App.jsx
CHANGED
|
@@ -87,19 +87,10 @@ function normalizeSearchResults(rawResults) {
|
|
| 87 |
const scores = sorted.map((item) => Number(item?.score ?? 0));
|
| 88 |
const maxScore = Math.max(...scores);
|
| 89 |
|
| 90 |
-
// RRF scores are typically very small (around 0.01-0.03), so scale them for UI display.
|
| 91 |
-
const isRrfLike = maxScore <= 0.1;
|
| 92 |
-
|
| 93 |
return sorted.map((item) => {
|
| 94 |
const score = Number(item?.score ?? 0);
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
if (isRrfLike) {
|
| 98 |
-
// Keep relative proportions between RRF scores (no min-max stretching).
|
| 99 |
-
displayScore = maxScore > 0 ? score / maxScore : 0;
|
| 100 |
-
} else {
|
| 101 |
-
displayScore = Math.max(0, Math.min(1, score));
|
| 102 |
-
}
|
| 103 |
|
| 104 |
return {
|
| 105 |
...item,
|
|
|
|
| 87 |
const scores = sorted.map((item) => Number(item?.score ?? 0));
|
| 88 |
const maxScore = Math.max(...scores);
|
| 89 |
|
|
|
|
|
|
|
|
|
|
| 90 |
return sorted.map((item) => {
|
| 91 |
const score = Number(item?.score ?? 0);
|
| 92 |
+
// Keep bars readable by always scaling against the best result in the current list.
|
| 93 |
+
const displayScore = maxScore > 0 ? Math.max(0, Math.min(1, score / maxScore)) : 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
return {
|
| 96 |
...item,
|