perf(ai-scanner): drop confidence scoring to simplify AI menu import
Removes the per-item confidence field end to end (prompt, extraction parsing, ai_import_items writes, API responses, low-confidence badge in the mobile review screen). ai_import_items.confidence column is left in place but unused (nullable, no destructive migration).
This commit is contained in:
@@ -9,7 +9,6 @@ interface ExtractionResult {
|
|||||||
|
|
||||||
const SYSTEM_PROMPT = `Sen profesyonel bir restoran menü analiz ve OCR uzmanısın.
|
const SYSTEM_PROMPT = `Sen profesyonel bir restoran menü analiz ve OCR uzmanısın.
|
||||||
Sana verilen menü görselini analiz et ve menüdeki tüm kategorileri, yemek/içecek isimlerini, açıklamalarını ve fiyatlarını (TL cinsinden sayı olarak) tespit et.
|
Sana verilen menü görselini analiz et ve menüdeki tüm kategorileri, yemek/içecek isimlerini, açıklamalarını ve fiyatlarını (TL cinsinden sayı olarak) tespit et.
|
||||||
Ayrıca her ürünün fiyatının ve isminin doğruluğu için 0.00 ile 1.00 arasında bir confidence (güven) skoru belirle (Örn: net okunanlar için 0.95-0.99, silik veya şüpheli olanlar için 0.65-0.80).
|
|
||||||
|
|
||||||
SADECE aşağıdaki JSON formatında geçerli bir JSON yanıtı ver:
|
SADECE aşağıdaki JSON formatında geçerli bir JSON yanıtı ver:
|
||||||
{
|
{
|
||||||
@@ -20,8 +19,7 @@ SADECE aşağıdaki JSON formatında geçerli bir JSON yanıtı ver:
|
|||||||
{
|
{
|
||||||
"name": "Ürün Adı",
|
"name": "Ürün Adı",
|
||||||
"description": "Ürün açıklaması veya null",
|
"description": "Ürün açıklaması veya null",
|
||||||
"price": 150.0,
|
"price": 150.0
|
||||||
"confidence": 0.98
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -52,7 +50,6 @@ function cleanAndParseJson(text: string): { categories: AiExtractedCategory[] }
|
|||||||
name: item.name || "İsimsiz Ürün",
|
name: item.name || "İsimsiz Ürün",
|
||||||
description: item.description || null,
|
description: item.description || null,
|
||||||
price: typeof item.price === "number" ? item.price : Number(String(item.price).replace(/[^\d.]/g, "")) || 0,
|
price: typeof item.price === "number" ? item.price : Number(String(item.price).replace(/[^\d.]/g, "")) || 0,
|
||||||
confidence: typeof item.confidence === "number" ? Math.min(1, Math.max(0, item.confidence)) : 0.95,
|
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -173,38 +170,38 @@ function extractWithFallback(imageIdentifier: string): ExtractionResult {
|
|||||||
id: "cat-1",
|
id: "cat-1",
|
||||||
name: "Çorbalar",
|
name: "Çorbalar",
|
||||||
items: [
|
items: [
|
||||||
{ id: "item-1-1", name: "Mercimek Çorbası", description: "Taze nane, tereyağlı kıtır kruton ve limon ile", price: 120, confidence: 0.98 },
|
{ id: "item-1-1", name: "Mercimek Çorbası", description: "Taze nane, tereyağlı kıtır kruton ve limon ile", price: 120 },
|
||||||
{ id: "item-1-2", name: "Ezogelin Çorbası", description: "Geleneksel Güneydoğu usulü acılı ezogelin", price: 130, confidence: 0.96 },
|
{ id: "item-1-2", name: "Ezogelin Çorbası", description: "Geleneksel Güneydoğu usulü acılı ezogelin", price: 130 },
|
||||||
{ id: "item-1-3", name: "Kelle Paça Çorbası", description: "Sarımsak ve sirke sosu ile", price: 210, confidence: 0.72 },
|
{ id: "item-1-3", name: "Kelle Paça Çorbası", description: "Sarımsak ve sirke sosu ile", price: 210 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "cat-2",
|
id: "cat-2",
|
||||||
name: "Kebaplar & Izgaralar",
|
name: "Kebaplar & Izgaralar",
|
||||||
items: [
|
items: [
|
||||||
{ id: "item-2-1", name: "Adana Kebap", description: "Közlenmiş biber, domates, sumaklı soğan ve lavaş ile", price: 340, confidence: 0.99 },
|
{ id: "item-2-1", name: "Adana Kebap", description: "Közlenmiş biber, domates, sumaklı soğan ve lavaş ile", price: 340 },
|
||||||
{ id: "item-2-2", name: "Urfa Kebap", description: "Acısız zırh kıyması, lavaş ve köz sebzeler", price: 340, confidence: 0.97 },
|
{ id: "item-2-2", name: "Urfa Kebap", description: "Acısız zırh kıyması, lavaş ve köz sebzeler", price: 340 },
|
||||||
{ id: "item-2-3", name: "Kuzu Şiş", description: "Marine edilmiş taze kuzu eti, bulgur pilavı ile", price: 420, confidence: 0.94 },
|
{ id: "item-2-3", name: "Kuzu Şiş", description: "Marine edilmiş taze kuzu eti, bulgur pilavı ile", price: 420 },
|
||||||
{ id: "item-2-4", name: "Tavuk Şiş", description: "Özel sosla marine edilmiş tavuk göğsü", price: 280, confidence: 0.68 },
|
{ id: "item-2-4", name: "Tavuk Şiş", description: "Özel sosla marine edilmiş tavuk göğsü", price: 280 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "cat-3",
|
id: "cat-3",
|
||||||
name: "Tatlılar",
|
name: "Tatlılar",
|
||||||
items: [
|
items: [
|
||||||
{ id: "item-3-1", name: "Künefe", description: "Hakiki Hatay peynirli, Antep fıstıklı sıcak künefe", price: 190, confidence: 0.98 },
|
{ id: "item-3-1", name: "Künefe", description: "Hakiki Hatay peynirli, Antep fıstıklı sıcak künefe", price: 190 },
|
||||||
{ id: "item-3-2", name: "Fıstıklı Baklava (4 Dilim)", description: "Gaziantep usulü tereyağlı çıtır baklava", price: 240, confidence: 0.91 },
|
{ id: "item-3-2", name: "Fıstıklı Baklava (4 Dilim)", description: "Gaziantep usulü tereyağlı çıtır baklava", price: 240 },
|
||||||
{ id: "item-3-3", name: "Fırın Sütlaç", description: "Kavrulmuş fındık parçaları ile", price: 130, confidence: 0.78 },
|
{ id: "item-3-3", name: "Fırın Sütlaç", description: "Kavrulmuş fındık parçaları ile", price: 130 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "cat-4",
|
id: "cat-4",
|
||||||
name: "İçecekler",
|
name: "İçecekler",
|
||||||
items: [
|
items: [
|
||||||
{ id: "item-4-1", name: "Yayık Ayranı", description: "Bol köpüklü taze köy ayranı", price: 45, confidence: 0.99 },
|
{ id: "item-4-1", name: "Yayık Ayranı", description: "Bol köpüklü taze köy ayranı", price: 45 },
|
||||||
{ id: "item-4-2", name: "Şalgam Suyu", description: "Acılı / Acısız Adana şalgamı", price: 40, confidence: 0.95 },
|
{ id: "item-4-2", name: "Şalgam Suyu", description: "Acılı / Acısız Adana şalgamı", price: 40 },
|
||||||
{ id: "item-4-3", name: "Kola / Meşrubat (330ml)", description: "Soğuk kutu meşrubat çeşitleri", price: 55, confidence: 0.96 },
|
{ id: "item-4-3", name: "Kola / Meşrubat (330ml)", description: "Soğuk kutu meşrubat çeşitleri", price: 55 },
|
||||||
{ id: "item-4-4", name: "Su (500ml)", description: "Doğal kaynak suyu", price: 20, confidence: 0.99 },
|
{ id: "item-4-4", name: "Su (500ml)", description: "Doğal kaynak suyu", price: 20 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ export const aiImportsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
item_name: string;
|
item_name: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: number;
|
price: number;
|
||||||
confidence: number;
|
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
for (const cat of extraction.categories) {
|
for (const cat of extraction.categories) {
|
||||||
@@ -96,7 +95,6 @@ export const aiImportsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
item_name: item.name,
|
item_name: item.name,
|
||||||
description: item.description ?? null,
|
description: item.description ?? null,
|
||||||
price: item.price,
|
price: item.price,
|
||||||
confidence: item.confidence,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,15 +114,9 @@ export const aiImportsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
})
|
})
|
||||||
.eq("id", importRecord.id);
|
.eq("id", importRecord.id);
|
||||||
|
|
||||||
let lowConfidenceCount = 0;
|
|
||||||
let totalItems = 0;
|
let totalItems = 0;
|
||||||
for (const cat of extraction.categories) {
|
for (const cat of extraction.categories) {
|
||||||
for (const itm of cat.items) {
|
totalItems += cat.items.length;
|
||||||
totalItems++;
|
|
||||||
if (itm.confidence < 0.85) {
|
|
||||||
lowConfidenceCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response: AiImportResponse = {
|
const response: AiImportResponse = {
|
||||||
@@ -133,7 +125,6 @@ export const aiImportsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
model: extraction.model,
|
model: extraction.model,
|
||||||
categories: extraction.categories,
|
categories: extraction.categories,
|
||||||
totalItems,
|
totalItems,
|
||||||
lowConfidenceCount,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return reply.code(200).send(response);
|
return reply.code(200).send(response);
|
||||||
@@ -185,11 +176,9 @@ export const aiImportsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
// Group items into categories
|
// Group items into categories
|
||||||
const categoryMap = new Map<string, AiExtractedCategory>();
|
const categoryMap = new Map<string, AiExtractedCategory>();
|
||||||
let totalItems = 0;
|
let totalItems = 0;
|
||||||
let lowConfidenceCount = 0;
|
|
||||||
|
|
||||||
for (const item of rawItems ?? []) {
|
for (const item of rawItems ?? []) {
|
||||||
totalItems++;
|
totalItems++;
|
||||||
if ((item.confidence ?? 1) < 0.85) lowConfidenceCount++;
|
|
||||||
|
|
||||||
const catName = item.category_name || "Genel";
|
const catName = item.category_name || "Genel";
|
||||||
if (!categoryMap.has(catName)) {
|
if (!categoryMap.has(catName)) {
|
||||||
@@ -205,7 +194,6 @@ export const aiImportsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
name: item.item_name,
|
name: item.item_name,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
price: Number(item.price ?? 0),
|
price: Number(item.price ?? 0),
|
||||||
confidence: Number(item.confidence ?? 0.95),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +203,6 @@ export const aiImportsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
model: importRecord.model,
|
model: importRecord.model,
|
||||||
categories: Array.from(categoryMap.values()),
|
categories: Array.from(categoryMap.values()),
|
||||||
totalItems,
|
totalItems,
|
||||||
lowConfidenceCount,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return reply.send(response);
|
return reply.send(response);
|
||||||
|
|||||||
@@ -33,10 +33,6 @@ export default function OnboardingAiReviewStep() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const totalItems = categories.reduce((acc, c) => acc + c.items.length, 0);
|
const totalItems = categories.reduce((acc, c) => acc + c.items.length, 0);
|
||||||
const lowConfidenceItems = categories.reduce(
|
|
||||||
(acc, c) => acc + c.items.filter((i) => i.confidence < 0.85).length,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
function updateItem(categoryId: string, itemId: string, field: "name" | "price" | "description", value: string) {
|
function updateItem(categoryId: string, itemId: string, field: "name" | "price" | "description", value: string) {
|
||||||
setCategories((prev) =>
|
setCategories((prev) =>
|
||||||
@@ -48,7 +44,7 @@ export default function OnboardingAiReviewStep() {
|
|||||||
if (itm.id !== itemId) return itm;
|
if (itm.id !== itemId) return itm;
|
||||||
if (field === "price") {
|
if (field === "price") {
|
||||||
const parsed = Number(value.replace(",", "."));
|
const parsed = Number(value.replace(",", "."));
|
||||||
return { ...itm, price: Number.isNaN(parsed) ? 0 : parsed, confidence: 1 };
|
return { ...itm, price: Number.isNaN(parsed) ? 0 : parsed };
|
||||||
}
|
}
|
||||||
return { ...itm, [field]: value };
|
return { ...itm, [field]: value };
|
||||||
}),
|
}),
|
||||||
@@ -75,7 +71,6 @@ export default function OnboardingAiReviewStep() {
|
|||||||
name: "Yeni Ürün",
|
name: "Yeni Ürün",
|
||||||
price: 100,
|
price: 100,
|
||||||
description: "",
|
description: "",
|
||||||
confidence: 1,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setCategories((prev) =>
|
setCategories((prev) =>
|
||||||
@@ -206,24 +201,6 @@ export default function OnboardingAiReviewStep() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{lowConfidenceItems > 0 ? (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
marginTop: 12,
|
|
||||||
paddingTop: 12,
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: "#F5F5F4",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name="warning-outline" size={16} color="#D97706" />
|
|
||||||
<Text style={{ color: "#D97706", fontSize: 12, fontWeight: "600", flex: 1 }}>
|
|
||||||
{lowConfidenceItems} ürünün fiyatı veya ismi silik çıkmış olabilir, lütfen sarı etiketli ürünleri kontrol edin.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Categories & Items List */}
|
{/* Categories & Items List */}
|
||||||
@@ -269,39 +246,17 @@ export default function OnboardingAiReviewStep() {
|
|||||||
{/* Items in Category */}
|
{/* Items in Category */}
|
||||||
<View style={{ gap: 12 }}>
|
<View style={{ gap: 12 }}>
|
||||||
{category.items.map((item) => {
|
{category.items.map((item) => {
|
||||||
const isLowConfidence = item.confidence < 0.85;
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={item.id}
|
key={item.id}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isLowConfidence ? "#FFFBEB" : "#FAFAFA",
|
backgroundColor: "#FAFAFA",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: isLowConfidence ? "#FDE68A" : "#F4F4F5",
|
borderColor: "#F4F4F5",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLowConfidence ? (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
backgroundColor: "#FEF3C7",
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 2,
|
|
||||||
borderRadius: 6,
|
|
||||||
marginBottom: 6,
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Ionicons name="warning-outline" size={12} color="#B45309" />
|
|
||||||
<Text style={{ color: "#B45309", fontSize: 11, fontWeight: "700" }}>
|
|
||||||
Fiyatı / İsmi Kontrol Edin (%{Math.round(item.confidence * 100)} Güven)
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<View style={{ flexDirection: "row", gap: 8, alignItems: "center", marginBottom: 6 }}>
|
<View style={{ flexDirection: "row", gap: 8, alignItems: "center", marginBottom: 6 }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={item.name}
|
value={item.name}
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export interface AiImportItem {
|
|||||||
itemName: string;
|
itemName: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
price: number | null;
|
price: number | null;
|
||||||
confidence: number | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiExtractedItem {
|
export interface AiExtractedItem {
|
||||||
@@ -86,7 +85,6 @@ export interface AiExtractedItem {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
price: number;
|
price: number;
|
||||||
confidence: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiExtractedCategory {
|
export interface AiExtractedCategory {
|
||||||
@@ -101,7 +99,6 @@ export interface AiImportResponse {
|
|||||||
model: string | null;
|
model: string | null;
|
||||||
categories: AiExtractedCategory[];
|
categories: AiExtractedCategory[];
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
lowConfidenceCount: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AiImportApplyPayload {
|
export interface AiImportApplyPayload {
|
||||||
|
|||||||
Reference in New Issue
Block a user