import gradio as gr from transformers import ( AutoTokenizer, AutoModelForTokenClassification, AutoModelForSequenceClassification, pipeline ) import torch device = "cuda" if torch.cuda.is_available() else "cpu" # === Tokenizery i modele ABSA === aspect_tokenizer = AutoTokenizer.from_pretrained("EfektMotyla/bert-aspect-ner") aspect_model = AutoModelForTokenClassification.from_pretrained("EfektMotyla/bert-aspect-ner").to(device) sentiment_tokenizer = AutoTokenizer.from_pretrained("EfektMotyla/absa-roberta") sentiment_model = AutoModelForSequenceClassification.from_pretrained("EfektMotyla/absa-roberta").to(device) pl_to_en_translator = pipeline("translation", model="Helsinki-NLP/opus-mt-pl-en", device=0 if torch.cuda.is_available() else -1) en_to_pl_translator = pipeline("translation", model="gsarti/opus-mt-tc-en-pl", device=0 if torch.cuda.is_available() else -1) def translate_pl_to_en(texts): return [res["translation_text"] for res in pl_to_en_translator(texts)] def translate_en_to_pl(texts): return [res["translation_text"] for res in en_to_pl_translator(texts)] # === Słownik znanych aspektów (EN → PL) === aspect_aliases = { # JEDZENIE / SMAK "food": "jedzenie", "meal": "jedzenie", "taste": "smak", "flavor": "smak", "dish": "danie", "portion": "porcja", "serving": "porcja", "ingredients": "składniki", "spices": "przyprawy", "salt": "sól", "fat": "tłuszcz", "grease": "tłuszcz", # OBSŁUGA "service": "obsługa", "staff": "obsługa", "waiter": "obsługa", "waitress": "obsługa", "manager": "obsługa", "attitude": "obsługa", # CENY / WARTOŚĆ "price": "cena", "value": "cena", "cost": "cena", # ATMOSFERA / WYSTRÓJ "decor": "wystrój", "interior": "wystrój", "design": "wystrój", "counter": "wystrój", "fridge": "wystrój", "music": "muzyka", "ambience": "klimat", "atmosphere": "klimat", "vibe": "klimat", "climate": "klimat", # MIEJSCE "location": "lokalizacja", "place": "lokalizacja", "entrance": "lokalizacja", "parking": "parking", "toilet": "toaleta", # CZAS / SZYBKOŚĆ "waiting time": "czas oczekiwania", "time": "czas oczekiwania", "delay": "opóźnienie", "speed": "czas oczekiwania", "service time": "czas oczekiwania", "slow": "czas oczekiwania", "fast": "czas oczekiwania", "immediate": "czas oczekiwania", "late": "opóźnienie", # ZAPACH / CZYSTOŚĆ "smell": "zapach", "odor": "zapach", "cleanliness": "czystość", "hygiene": "czystość", # OGÓLNE "experience": "doświadczenie", "visit": "wizyta", "menu": "menu", "variety": "menu", # MIEJSCE / LOKALIZACJA / OTOCZENIE "location": "lokalizacja", "place": "lokalizacja", "entrance": "lokalizacja", "parking": "parking", "view": "lokalizacja", "lake": "lokalizacja", "window": "lokalizacja", "terrace": "lokalizacja", "balcony": "lokalizacja", "outside": "lokalizacja", "area": "lokalizacja", "surroundings": "lokalizacja", "neighborhood": "lokalizacja", "river": "lokalizacja", "garden": "lokalizacja", # NAPOJE "drink": "napoje", "drinks": "napoje", "beverage": "napoje", "coffee": "napoje", "tea": "napoje", "water": "napoje", "juice": "napoje", "alcohol": "napoje", "cocktail": "napoje", "wine": "napoje", #HIGIENA "dirt": "czystość", "dirty": "czystość", "mess": "czystość", "messy": "czystość", "clean": "czystość", "filth": "czystość", #KUCHNIA /JAKOŚĆ "chef": "kuchnia", "kitchen": "kuchnia", "preparation": "kuchnia", "presentation": "prezentacja", "quality": "jakość", "freshness": "jakość", "raw": "jakość", "undercooked": "jakość", "burnt": "jakość", "microwaved": "jakość", # Wyposażenie "seat": "komfort", "seating": "komfort", "chair": "komfort", "table": "komfort", "furniture": "komfort", "light": "komfort", "noise": "komfort", "temperature": "komfort", "air conditioning": "komfort", # OGÓLNE WRAŻENIE / WARTOŚĆ "recommendation": "ogólna ocena", "return": "ogólna ocena", "again": "ogólna ocena", "worth": "cena", "overpriced": "cena", "cheap": "cena", "affordable": "cena", # DZIECI / RODZINA "child": "dzieci", "children": "dzieci", "kid": "dzieci", "kids": "dzieci", "child-friendly": "dzieci", "kids menu": "dzieci", "high chair": "dzieci", "stroller": "dzieci", "family": "rodzina", "families": "rodzina", "parent": "rodzina", "parents": "rodzina", "group": "rodzina", "big group": "rodzina", "baby": "dzieci", # ZWIERZĘTA "dog": "zwierzęta", "dogs": "zwierzęta", "pet": "zwierzęta", "pets": "zwierzęta", "pet-friendly": "zwierzęta", "dog-friendly": "zwierzęta", "animal": "zwierzęta", } def extract_aspects(text): inputs = aspect_tokenizer(text, return_tensors="pt", truncation=True, padding=True).to(device) with torch.no_grad(): outputs = aspect_model(**inputs) preds = torch.argmax(outputs.logits, dim=2)[0].cpu().numpy() tokens = aspect_tokenizer.convert_ids_to_tokens(inputs["input_ids"][0]) labels = [aspect_model.config.id2label[p] for p in preds] aspects = [] current_tokens = [] for token, label in zip(tokens, labels): if label == "B-ASP": if current_tokens: aspects.append(aspect_tokenizer.convert_tokens_to_string(current_tokens).strip()) current_tokens = [] current_tokens = [token] elif label == "I-ASP" and current_tokens: current_tokens.append(token) else: if current_tokens: aspects.append(aspect_tokenizer.convert_tokens_to_string(current_tokens).strip()) current_tokens = [] if current_tokens: aspects.append(aspect_tokenizer.convert_tokens_to_string(current_tokens).strip()) return list(set(aspects)) # usuń duplikaty def analyze(text_pl, progress=gr.Progress()): try: progress(0, desc="Tłumaczenie na angielski...") text_en = translate_pl_to_en([text_pl])[0] progress(0.3, desc="Wykrywanie aspektów...") aspects_en = extract_aspects(text_en) if not aspects_en: return "Nie wykryto żadnych aspektów." unique_aspects = sorted(set([asp.lower() for asp in aspects_en])) results = [] seen_pl_aspects = set() for i, asp in enumerate(unique_aspects): progress(0.4 + i/len(unique_aspects)*0.6, desc=f"Analiza aspektu: {asp}") input_text = f"{text_en} [SEP] {asp}" inputs = sentiment_tokenizer(input_text, return_tensors="pt", truncation=True, padding=True).to(device) with torch.no_grad(): logits = sentiment_model(**inputs).logits predicted_class_id = int(logits.argmax().cpu()) sentiment_label = {0: "negatywny", 1: "neutralny", 2: "pozytywny", 3: "konfliktowy"}[predicted_class_id] # Tłumaczenie aspektu przez słownik lub model if asp in aspect_aliases: asp_pl = aspect_aliases[asp] else: asp_pl = translate_en_to_pl([asp])[0].lower() if asp_pl not in seen_pl_aspects: seen_pl_aspects.add(asp_pl) results.append(f"{asp_pl.capitalize()} → **{sentiment_label}**") return "\n".join(results) except Exception as e: return f"Błąd podczas analizy: {e}" # === Gradio UI === demo = gr.Interface( fn=analyze, inputs=gr.Textbox( label="Komentarz po polsku", placeholder="Np. Pizza była pyszna, ale kelner był nieuprzejmy.", lines=4, max_lines=6 ), outputs=gr.Markdown(label="Wyniki analizy"), title="ABSA – Analiza komentarzy restauracyjnych", description="Wykrywa aspekty i przypisuje im sentymenty (pozytywny / negatywny / neutralny / konfliktowy).", theme="default", allow_flagging="never" ) if __name__ == "__main__": demo.launch()