from typing import Dict, Tuple, Optional, TypeAlias import requests import base64 import json from dataclasses import dataclass from utils import get_logger, LANGS from openfoodfacts.api import ProductResource, APIConfig import gradio as gr logger = get_logger() BASE_URL = "https://robotoff.openfoodfacts.org/api/v1" # Prod # BASE_URL = "http://localhost:5500/api/v1" # Dev UPDATE = 1 Annotation: TypeAlias = Tuple[str, str, str, str, str] @dataclass class Authentification: username: str password: str def get_credentials(self) -> str: credentials = f"{self.username}:{self.password}" return base64.b64encode(credentials.encode()).decode() def next_annotation(lang: Optional[str]) -> Annotation: lang_key = LANGS.get(lang) if not lang_key: logger.warning("The provided Lang was not found. 'All' returned by default.") insight = import_random_insight(lang_key=lang_key) logger.info("Imported insight: %s", insight) return ( insight["id"], insight["data"]["original"], insight["data"]["correction"], # Saved for comparison with annotator changes insight["data"]["correction"], get_image_url(insight["barcode"]), get_product_url(insight["barcode"]), ) def submit_correction( insight_id: str, annotator_correction: str, model_correction: str, username: str, password: str, lang: str, update: bool = UPDATE, ): auth = Authentification(username, password) correction = ( annotator_correction if annotator_correction != model_correction else None ) try: submit_to_product_opener( insight_id=insight_id, update=update, annotator_correction=correction, auth=auth, ) except gr.Error as e: gr.Warning(e.message) # We use gr.Warning instead of gr.Error to keep the flow return ( gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), ) # Stay unchanged gr.Info("Product successfuly updated. Many thanks!") return next_annotation(lang=lang) def import_random_insight( insight_type: str = "ingredient_spellcheck", predictor: str = "fine-tuned-mistral-7b", lang_key: Optional[str] = None, ) -> Dict: url = ( f"{BASE_URL}/insights/random?count=1&type={insight_type}&predictor={predictor}" ) if lang_key: url += f"&value_tag={lang_key}" try: response = requests.get(url) response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx) data = response.json() if "insights" in data and data["insights"]: return data["insights"][0] else: gr.Warning("No insights found; fetching default insight instead.") return import_random_insight(insight_type, predictor) except requests.ReadTimeout: gr.Error("There's an issue with the server... Wait a couple of minutes and try again.") except requests.RequestException as e: gr.Error(f"Import product from Product Opener failed: {e}") def submit_to_product_opener( insight_id: str, update: bool, auth: Authentification, annotator_correction: Optional[str] = None, ) -> None: url = f"{BASE_URL}/insights/annotate" headers = { "Authorization": f"Basic {auth.get_credentials()}", "Content-Type": "application/x-www-form-urlencoded", } if annotator_correction: logger.info( "Change from annotator. New insight sent to Product Opener. New correction: %s", annotator_correction, ) payload = { "insight_id": insight_id, "annotation": 2, "update": update, "data": json.dumps({"annotation": annotator_correction}), } else: logger.info( "No change from annotator. Original insight sent to Product Opener." ) payload = { "insight_id": insight_id, "annotation": 1, "update": update, } try: response = requests.post(url, data=payload, headers=headers) response.raise_for_status() except requests.ReadTimeout: gr.Error("There's an issue with the server... Wait a couple of minutes and try again.") except requests.RequestException as e: logger.error(e) logger.error(response.content) raise gr.Error( "Failed to submit to Product Opener. Are your username and password correct?" ) def get_image_url( code: str, user_agent: str = "Spellcheck-Annotate", ) -> str: fields = ["image_ingredients_url"] data = ProductResource(api_config=APIConfig(user_agent=user_agent)).get( code=code, fields=fields, ) image_url = data.get(fields[0]) return image_url def enable_buttons(username, password): # Return the updated button states: interactive if both username and password are filled state = bool(username) and bool(password) return gr.update(interactive=state), gr.update(interactive=state) def get_product_url(barcode: int) -> str: return f"https://world.openfoodfacts.org/product/{barcode}"