"""GRAPP — L'app pour choisir ton vin sans te prendre la grappe. Lancer : streamlit run app.py """ from __future__ import annotations import json from pathlib import Path import pandas as pd import streamlit as st from PIL import Image from grapp.carte import ocr_image, trouver_vins from grapp.engine import FAMILLES, OCCASIONS, DicoVino, Grapp, Preferences ROOT = Path(__file__).resolve().parent FAV_FILE = ROOT / "data" / "favoris.json" EMOJI = {"rouge": "🍷", "blanc": "🥂", "rose": "🌸"} st.set_page_config(page_title="GRAPP", page_icon="🍇", layout="centered") # ------------------------------------------------- charte graphique (dossier PFE) st.markdown(""" """, unsafe_allow_html=True) # ------------------------------------------------- chargement (mis en cache) @st.cache_resource(show_spinner="On réveille le sommelier IA… 🍇") def moteur(): return Grapp(), DicoVino() grapp, dico = moteur() noms = grapp.vins["nom"].tolist() _bk = ROOT / "data" / "bouteilles_kaggle.csv" BOUTEILLES = pd.read_csv(_bk) if _bk.exists() else None # optionnel, cf. scripts/enrichir_kaggle.py def lire_favoris() -> list[str]: try: return json.loads(FAV_FILE.read_text(encoding="utf-8")) except (FileNotFoundError, json.JSONDecodeError): return [] def ecrire_favoris(favs: list[str]) -> None: FAV_FILE.write_text(json.dumps(favs, ensure_ascii=False, indent=2), encoding="utf-8") ss = st.session_state ss.setdefault("etape", 0) # 0 = carte, 1..7 = quiz, 8 = résultats ss.setdefault("rep", {}) ss.setdefault("carte", None) # {nom: prix} ou None ss.setdefault("favoris", lire_favoris()) st.markdown('' '

L’app pour choisir ton vin sans te prendre la grappe 🍇

', unsafe_allow_html=True) with st.expander("ℹ️ C'est quoi ce sommelier ?"): st.caption(f"Moteur sémantique : **{grapp.embedder.backend}** · {len(noms)} vins en base · " "OCR Tesseract pour lire la carte · tout tourne en local.") tab_tchin, tab_fav, tab_dico = st.tabs(["🥂 Tchin Tchin !", "💖 Mes p'tits favoris", "💡 Dico Vino"]) # ------------------------------------------------- cartes de vin def carte_vin(r, cle: str): v = r.vin chips = "".join(f'{x}' for x in r.raisons) st.markdown(f"""
{r.match}% match

{EMOJI.get(v['couleur'], '🍷')} {r.nom}

{v['region']} · ~{r.prix:.0f} €

« {v['pitch']} »

Ça sent : {v['aromes']} · Avec : {v['accords']}
{chips}
""", unsafe_allow_html=True) c1, c2 = st.columns(2) deja = r.nom in ss.favoris if c1.button("💔 Retirer" if deja else "❤️ Mes fav", key=f"fav_{cle}"): ss.favoris = [f for f in ss.favoris if f != r.nom] if deja else ss.favoris + [r.nom] ecrire_favoris(ss.favoris) st.rerun() with c2.popover("🔁 Dans le même style"): for s in grapp.similaires(r.nom): st.write(f"• {s}") if BOUTEILLES is not None: b = BOUTEILLES[BOUTEILLES["nom"] == r.nom] if not b.empty: with st.expander("🍾 Vraies bouteilles bien notées (Wine Enthusiast)"): for _, x in b.iterrows(): st.markdown(f"**{x.bouteille}** · {x.points}/100 · {x.prix_usd:.0f} $ \n*{x.avis}*") # ------------------------------------------------- onglet 1 : Tchin Tchin QUESTIONS = [ ("Alors, on trinque ?", "C'est quoi le plan du jour ?", "occasion", list(OCCASIONS.values())), ("Verre ou bouteille ?", "Tu la joues solo ou on partage ?", "format", ["Juste un p'tit verre (ou 2)", "On prend une bouteille"]), ("La vie en couleur !", "T'es dans quelle team ?", "couleur", ["Rouge", "Blanc", "Rosé", "Surprends-moi"]), ("Plus ça pétille… ", "Un peu de pep's dans ton verre ?", "bulles", ["J'adore le pétillant", "Sans bulles", "Peu importe"]), ("Sucré ou pas ?", "Sois honnête, personne ne juge.", "sucre", ["Sec", "Presque sec", "Une pointe de douceur", "Doux", "Team dessert"]), ("Léger ou costaud ?", "Plutôt plume ou plutôt haltère ?", "corps", ["Tout léger", "Léger", "Entre les deux", "Costaud", "Ultra costaud"]), ("C'est parfait !", "Dernière ligne droite : tes goûts et ton budget.", "final", None), ] def construire_prefs(rep: dict) -> Preferences: occ = {v: k for k, v in OCCASIONS.items()} return Preferences( occasion=occ.get(rep.get("occasion"), "apero"), format="verre" if str(rep.get("format", "")).startswith("Juste") else "bouteille", couleur={"Rouge": "rouge", "Blanc": "blanc", "Rosé": "rose"}.get(rep.get("couleur"), "peu_importe"), bulles={"J'adore le pétillant": "oui", "Sans bulles": "non"}.get(rep.get("bulles"), "peu_importe"), sucre=QUESTIONS[4][3].index(rep.get("sucre", "Sec")) + 1, corps=QUESTIONS[5][3].index(rep.get("corps", "Entre les deux")) + 1, familles=[k for k, v in FAMILLES.items() if v in rep.get("familles", [])], budget=float(rep.get("budget", 30)), envie=rep.get("envie", ""), ) with tab_tchin: e = ss.etape if e == 0: st.subheader("Étape 1 · Scanne la carte du resto 📸") st.write("GRAPP lit la carte et ne te propose **que des vins qui y sont**. " "Pas de carte ? Pas de souci, passe directement au quiz.") source = st.radio("Comment tu nous montres la carte ?", ["📷 Photo / fichier", "🧾 Carte d'exemple (démo)", "⌨️ Coller le texte"], horizontal=True) texte = "" if source.startswith("📷"): f = st.file_uploader("Photo de la carte des vins", type=["png", "jpg", "jpeg"]) if f: img = Image.open(f) st.image(img, width=300) elif source.startswith("🧾"): img = Image.open(ROOT / "assets" / "carte_exemple.png") st.image(img, width=300) else: img = None texte = st.text_area("Colle ici le texte de la carte", height=160) if st.button("🔍 Lire la carte", type="primary"): try: if not source.startswith("⌨️"): if source.startswith("📷") and not f: st.warning("Ajoute d'abord une photo 😉") st.stop() with st.spinner("L'IA déchiffre la carte…"): try: texte = ocr_image(img) except RuntimeError: # Version web (navigateur) : pas de Tesseract. Pour la carte # d'exemple, on reprend le texte que Tesseract en a extrait. if not source.startswith("🧾"): raise texte = (ROOT / "data" / "carte_exemple.txt").read_text(encoding="utf-8") trouves = trouver_vins(texte, noms) except RuntimeError as err: st.error(f"Lecture impossible : {err}. Colle plutôt le texte de la carte.") trouves = [] if trouves: ss.carte = {t["nom"]: t["prix_carte"] for t in trouves} ss.etape = 1 st.rerun() elif texte: st.warning("Je n'ai reconnu aucun vin de ma base sur cette carte 🤔") st.divider() if st.button("Pas de carte, on passe au quiz ➡️"): ss.carte, ss.etape = None, 1 st.rerun() elif 1 <= e <= 7: if ss.carte: st.success(f"🧾 {len(ss.carte)} vins repérés sur la carte : " + ", ".join(ss.carte)) titre, sous, cle, options = QUESTIONS[e - 1] st.progress(e / 7, text=f"{e}/7") st.subheader(titre) st.caption(sous) if options: idx = options.index(ss.rep[cle]) if ss.rep.get(cle) in options else 0 choix = st.radio(" ", options, index=idx, key=f"q{e}", label_visibility="collapsed") else: fam = st.multiselect("Les saveurs qui te font kiffer", list(FAMILLES.values()), default=ss.rep.get("familles", [])) fmt = "le verre" if str(ss.rep.get("format", "")).startswith("Juste") else "la bouteille" bud = st.slider(f"Budget max pour {fmt} (€)", 3, 80, int(ss.rep.get("budget", 8 if fmt == "le verre" else 30))) env = st.text_input("Ton envie en une phrase (facultatif)", value=ss.rep.get("envie", ""), placeholder="ex : un truc frais pour un barbecue entre potes") c1, c2 = st.columns(2) if c1.button("⬅️ Retour"): ss.etape -= 1 st.rerun() if c2.button("Suivant ➡️" if e < 7 else "🍇 Trouve mon vin !", type="primary"): if options: ss.rep[cle] = choix else: ss.rep.update(familles=fam, budget=bud, envie=env) ss.etape += 1 st.rerun() else: prefs = construire_prefs(ss.rep) recos = grapp.recommander(prefs, k=3, carte=ss.carte) st.subheader("Plus qu'à kiffer ✨") if not recos: st.warning("Aucun vin ne colle à 100 % à tes critères" + (" sur cette carte" if ss.carte else "") + ". Essaie « Surprends-moi » ou « Peu importe » !") for i, r in enumerate(recos): carte_vin(r, f"reco{i}") if st.button("🔄 Recommencer"): ss.etape, ss.rep, ss.carte = 0, {}, None st.rerun() # ------------------------------------------------- onglet 2 : favoris with tab_fav: st.subheader("Mes vins pépites 💖") if not ss.favoris: st.info("Pas encore de favori. Fais le quiz et clique sur ❤️ !") for nom in ss.favoris: v = grapp.vins[grapp.vins["nom"] == nom] if v.empty: continue v = v.iloc[0] c1, c2 = st.columns([4, 1]) c1.markdown(f"**{EMOJI.get(v.couleur, '🍷')} {nom}** · {v.region} · ~{v.prix_bouteille} € \n" f"*{v.pitch}* \n🔁 Si tu aimes, essaie : {', '.join(grapp.similaires(nom))}") if c2.button("🗑️", key=f"del_{nom}"): ss.favoris.remove(nom) ecrire_favoris(ss.favoris) st.rerun() # ------------------------------------------------- onglet 3 : Dico Vino with tab_dico: st.subheader("Je décrypte le vin ! 💡") st.write("Un mot bizarre sur la carte ? Pose ta question comme tu veux.") q = st.text_input("Ta question", placeholder="ex : pourquoi j'ai la bouche qui gratte avec ce rouge ?") if q: for _, d in dico.chercher(q).iterrows(): st.markdown(f'

{d.terme}

{d.definition}

' f'

👉 {d.exemple}

', unsafe_allow_html=True) with st.expander("📖 Tout le dico"): for _, d in dico.df.sort_values("terme").iterrows(): st.markdown(f"**{d.terme}** : {d.definition}")