Allergen Labels
Her yemekteki 14 AB alerjenini dört dilde işaretler ve bir şef onaylamadan hiçbir şeyi menüye basmaz.
- Rol
- Kişisel proje — tasarım, kod, testler
- Yıl
- 2026
- Kategori
- Yapay zekâ · Web
- Teknolojiler
- Python 3.12, FastAPI, Jinja2, SQLite

Sorun
AB gıda mevzuatı (1169/2011 sayılı Tüzük, madde 9(1)(c) ve Ek II) 14 alerjen grubunun bildirilmesini ister; madde 44 ise bunu ambalajsız sunulan gıdalar için, yani restoranlar ve otel mutfakları için zorunlu kılar. Bilginin nasıl gösterileceğine her ülke kendisi karar verir. Türkiye'de de Türk Gıda Kodeksi aynı 14 grubu listeler.
Zor olan liste değil. Mutfaklar tarifleri sürekli değiştirir: yeni bir tedarikçinin bulyon tableti, pestoda çam fıstığı yerine kaju, eskiden sıvı yağla yapılan bir sosa tereyağı. Menüdeki alerjen numaraları bir kez yazılır ve eskir. Antalya'da aynı menüyü farklı dillerde turistler okur ve fındık alerjisi olan bir misafir, yemeğin adının yanındaki bir rakama güvenmek zorunda kalır.
Çözüm
- Şef malzemeleri mutfakta yazıldığı gibi girer: İngilizce, Rusça, Türkçe veya Almanca. 2.429 ifadelik, elle hazırlanmış bir sözlük alerjenleri bulur ve her bulgu için hangi kelimeden geldiğini gösterir.
- Çoğul ekleri, Rusça hâl eklerini («сметаной»), Türkçe ekleri («fındığı», «peynirli») ve Almanca birleşik kelimeleri («Haselnusskrokant») tanır; Türkçe karakterlerle de, onlarsız da çalışır.
- Bileşik ürünlerin tipik alerjenleri vardır: «soya sosu» soya ve muhtemelen gluten, «pesto» muhtemelen süt ve sert kabuklu meyve.
pesto (basil, cashews, parmesan)yazınca tahminin yerini gerçek malzemeler alır. - Alerjene benzeyen ama Ek II'de olmayanlar işaretlenmez, açıklanır: hindistan cevizi, çam fıstığı, muskat, karabuğday, glikoz şurubu. «Laktozsuz süt» yine süttür.
- Bilinmeyen ya da iki anlamlı kelimeler («fıstık» yer fıstığı da olabilir antep fıstığı da, «масло» tereyağı da olabilir sıvı yağ da) bir kişi için işaretlenir. API anahtarı varsa Claude kısa bir gerekçeyle grup önerir ve «onay gerekli» diye işaretler; anahtar yoksa bu kelimeler sadece işaretlenir.
- Bir kişi yemeği kendi adıyla onaylamadan hiçbir şey yayınlanmaz. Her onay kayda geçer; malzemeleri değiştirmek onayı sıfırlar; başka biri tarifi değiştirmeden önce açılmış bir sayfa onu onaylayamaz.
- Alerjen numaraları ve açıklamalarıyla dört dilde basılabilir menü ve JSON dışa aktarımı.
Ekran görüntüleri



Teknolojiler
- Python 3.12
- FastAPI
- Jinja2
- SQLite
- Anthropic API
- pytest
- Docker
Asıl işi sözlük yapar ve internetsiz çalışır. LLM yalnızca sözlüğün çözemediği malzemeleri görür; cevabı, alerjen alanı 14 grup kimliğinden oluşan bir enum olan bir JSON şemasına uyar ve sadece onay formundaki kutuları önceden işaretler. SQLite tetikleyicileri onay kaydını yalnızca eklemeye açık hâle getirir: kayıtlar değiştirilemez, silinemez. Web arayüzü düz HTML formlarıdır, JavaScript gerekmez.
Öne çıkan kod
Sözlükteki her ifade, «katlanmış» bir metin (küçük harf, aksansız, Türkçe «İ/ı» tek biçimde) üzerinde çalışan bir düzenli ifadeye dönüşür. Her dilin kendi kelime biçimleri vardır:
def _word(word: str, language: Language, last: bool, exact: bool) -> str:
escaped = re.escape(word)
if exact:
return escaped
if language is Language.EN:
if not last:
return escaped
if len(word) > 2 and word.endswith("y") and word[-2] not in _VOWELS:
return re.escape(word[:-1]) + "(?:y|ies)"
return escaped + "(?:s|es)?"
if language is Language.DE:
return escaped + _DE_ENDINGS
if language is Language.RU:
return escaped + "[а-я]{0,6}"
# Turkish
stem = escaped
vowels = sum(ch in _VOWELS for ch in word)
if word[-1] in _TR_SOFTEN and vowels >= 2:
stem = f"(?:{escaped}|{re.escape(word[:-1] + _TR_SOFTEN[word[-1]])})"
suffix = "[a-z]{0,10}" if len(word) >= 4 else _TR_SHORT_SUFFIXES
return stem + (f"(?P<sfx>{suffix})" if last else suffix)Ardından eşleştirici, çakışmayan en uzun eşleşmeleri tutar; böylece «peanut butter» «butter»a, «Buchweizen» (karabuğday) da «Weizen»e (buğday) üstün gelir. «-sız» ile başlayan bir Türkçe ek («yumurtasız») «içermez» anlamında raporlanır, asla alerjen olarak sayılmaz.
Bulduğum ve düzelttiğim hata
Almanca sıfatlar bilinmeyen kelime sayılıyordu
Ne bozuldu
Etiketlediğim örnek üzerindeki ilk çalıştırma beklenen bütün alerjenleri buldu, ama 116 satırın 42'sini bir kişinin kontrolüne gönderdi. «gehackt» ve «gerieben» sözlükte olduğu hâlde «gehackte Walnüsse» ve «geriebener Parmesan» işaretleniyordu; «frische Kräuter» bile. Hiçbir şey kaçmadı, ama boş yere alarm veren bir araca kimse kulak asmaz.
Asıl neden
Almanca kelimeler birleşik kelimelerin içinde de aranıyor, bu yüzden kalıplarında kelime
sınırı yok; çekim eklerini de eklememiştim: «gehackt», «gehackte»nin yalnızca bir kısmını
kaplıyordu. Sadece bilinen bir kelimeyle kısmen kaplanan bir kelime bilerek bilinmeyen
sayılır (yoksa «Salat», «Waldorfsalat»ı zararsız gösterirdi). Ekleri ekledikten sonra
«geriebener» yine geçmedi: (?:e|en|er…) içinde düzenli ifade işe yarayan ilk seçeneği alır
ve arkasında sınır olmadığı için «e» her zaman yeter.
Çözüm
Birleşik kelime kalıpları artık ekleri en uzundan başlayarak alıyor. Örnekte kontrole giden satır sayısı 42'den 40'a indi, bulunan alerjenler yine 104'te 104; bir regresyon testi sıfatların yeniden bilinmeyen sayılmasını önlüyor.
_DE_ENDINGS = "(?:e|en|er|ern|n|s|es)?"
if compound:
return re.escape(words[0])_DE_ENDINGS = "(?:ern|en|er|es|e|n|s)?"
if compound:
return re.escape(words[0]) + _DE_ENDINGSSonuç
- 220 otomatik test: 14 grubun her biri dört dilin her birinde bir kelimeyle, morfoloji, yanıltıcı benzer kelimeler, Ek II'nin bütün istisnaları, onay kuralları ve sahte bir HTTP sunucusuna karşı Claude istemcisi.
- Kendi etiketlediğim, dört dilde 116 malzeme satırlık bir örnekte: 104 alerjenin 104'ü şef için önceden işaretlendi, sessiz kaçırma 0; 40 satır bir kişinin kontrolüne gitti, çoğunlukla sözlükte olmayan «tinned» ya da «Bio» gibi kelimeler yüzünden.
- Demo modu internetsiz çalışır: aralarında menemen, mercimek çorbası ve baklavanın da olduğu, adları dört dilde 16 örnek yemek.