import gradio as gr import os import libsql_experimental as libsql from qdrant_client import QdrantClient, models from sentence_transformers import SentenceTransformer from deep_translator import GoogleTranslator from langdetect import detect, DetectorFactory import time import yaml # --- CARICAMENTO ETICHETTE I18N --- def load_labels(filepath="lang.yaml"): """Carica tutte le etichette dal file YAML.""" try: with open(filepath, 'r', encoding='utf-8') as f: return yaml.safe_load(f) except FileNotFoundError: print("Errore: Il file 'lang.yaml' non รจ stato trovato.") return {} except Exception as e: print(f"Errore durante la lettura di 'lang.yaml': {e}") return {} ALL_LABELS = load_labels() DEFAULT_LANG = "it" def L(key, lang_code=DEFAULT_LANG): """Funzione helper per ottenere una label per la lingua specificata.""" return ALL_LABELS.get(lang_code, ALL_LABELS[DEFAULT_LANG]).get(key, f"MISSING_KEY:{key}") DetectorFactory.seed = 0 # --- SETUP DATABASE & AI --- # Funzione sicura per recuperare le chiavi def get_key(k): val = os.environ.get(k) if val: return val try: from google.colab import userdata return userdata.get(k) except: return None print("Caricamento Embedder...") model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", device="cpu") QDRANT_URL = get_key("QDRANT_URL") QDRANT_API_KEY = get_key("QDRANT_API_KEY") TURSO_URL = get_key("TURSO_URL") TURSO_TOKEN = get_key("TURSO_TOKEN") # Connessione resiliente try: client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY) except Exception as e: print(f"Errore Qdrant: {e}") client = None def get_turso_conn(): if not TURSO_URL: return None return libsql.connect(TURSO_URL, auth_token=TURSO_TOKEN) COLLECTION_NAME = "books_collection" VECTOR_SIZE = 256 # --- CSS & DARK MODE --- JS_FORCE_DARK = """ function() { document.body.classList.add('dark'); } """ GLOBAL_CSS = """ body, .gradio-container { background-color: #0b0f19 !important; color: #e5e7eb !important; } /* Stile Card */ .card-force-dark { border: 1px solid #374151; padding: 20px; border-radius: 12px; background-color: #1f2937; box-shadow: 0 4px 6px rgba(0,0,0,0.3); margin-bottom: 15px; } .card-force-dark h3 { color: #60a5fa !important; margin-top: 0 !important; margin-bottom: 5px !important; } .card-force-dark p { color: #d1d5db !important; line-height: 1.5; } .text-meta { font-size: 0.9em; color: #9ca3af !important; margin-bottom: 10px; } /* TASTO TRADUZIONE */ details { margin-top: 10px; border-top: 1px dashed #374151; padding-top: 10px; } summary { cursor: pointer; color: #fbbf24; font-weight: 600; list-style: none; display: inline-flex; align-items: center; font-size: 0.85em; } summary:hover { text-decoration: underline; } summary::before { content: "๐Ÿ”„ "; margin-right: 5px; } details[open] summary { color: #9ca3af; } details[open] summary::before { content: "โœ… "; } """ LOADING_HTML = """

__LOADING_TEXT__

""" # --- LOGICA --- def render_results(ids, scores, ui_lang="it"): if not ids: return L("error_no_results", ui_lang) if not TURSO_URL: return L("error_db_not_configured", ui_lang) conn = get_turso_conn() placeholders = ", ".join(["?"] * len(ids)) rows = conn.execute(f"SELECT id, title, author, year, rating, summary FROM books WHERE id IN ({placeholders})", tuple(ids)).fetchall() conn.close() books_map = {row[0]: row for row in rows} ordered_books = [books_map[uid] for uid in ids if uid in books_map] translator = GoogleTranslator(source='auto', target=ui_lang) html = "
" for row in ordered_books: score = scores.get(row[0], 0.0) title, author, year, rating, summary = row[1], row[2], row[3], row[4], row[5] author = str(author).replace('"', '').replace("[","").replace("]", "") if not summary: summary = "No summary available." # Logica traduzione show_btn = False translated_text = "" try: lang_detected = detect(summary[:100]) if len(summary) > 10 else "en" if lang_detected != ui_lang: show_btn = True translated_text = translator.translate(summary) except: L("translation_not_available", ui_lang) btn_label = L("translation_button_label", ui_lang) translate_block = "" if show_btn: translate_block = f"""
{btn_label}

{translated_text}

""" html += f"""

{title}

โœ๏ธ {author} | ๐Ÿ“… {year} | โญ {rating} | ๐ŸŽฏ Match: {score:.3f}

{summary}

{translate_block}
""" html += "
" return html # --- FUNZIONE DI AGGIORNAMENTO UI --- def update_ui_labels(new_lang): """Aggiorna tutti i componenti Gradio in base alla lingua selezionata.""" # Aggiorna il titolo del blocco (gr.Markdown) title_update = gr.Markdown(value=f"## {L('app_title', new_lang)}") # Aggiornamenti dei componenti return [ title_update, gr.Radio(label=L("language_label", new_lang)), gr.Slider(label=L("results_label", new_lang)), gr.Tab(label=L("plot_search_tab", new_lang)), gr.Textbox(placeholder=L("plot_search_placeholder", new_lang)), gr.Button(value=L("plot_search_button", new_lang)), gr.Tab(label=L("title_search_tab", new_lang), visible = True), gr.Textbox(placeholder=L("title_search_placeholder", new_lang)), gr.Button(value=L("title_search_button", new_lang)), gr.Dataset( label=L("dataset_label", new_lang), headers=[ L("dataset_header_title", new_lang), L("dataset_header_author", new_lang), L("dataset_header_year", new_lang) ] ), gr.HTML(label=L("results_label", new_lang)) ] def search_logic(query, max_res, lang_pref): loading_html_dynamic = LOADING_HTML.replace("__LOADING_TEXT__", L("search_loading_message", lang_pref)) yield gr.update(loading_html_dynamic, visible=True), gr.update(visible=False) if not query: yield gr.update(visible=False), L("error_no_query", lang_pref) return try: q_emb = GoogleTranslator(source='auto', target='en').translate(query) if lang_pref == "it" else query vec = model.encode(q_emb)[:VECTOR_SIZE] hits = client.query_points(COLLECTION_NAME, query=vec, limit=max_res, search_params=models.SearchParams(quantization=models.QuantizationSearchParams(rescore=True))).points ids = [h.id for h in hits] scores = {h.id: h.score for h in hits} yield gr.update(visible=False), gr.update(value=render_results(ids, scores, lang_pref), visible=True) except Exception as e: yield gr.update(visible=False), f"Errore: {e}" def find_logic(title): loading_html_dynamic = LOADING_HTML.replace("__LOADING_TEXT__", L("search_loading_message", "en")) yield gr.update(loading_html_dynamic, visible=True), gr.update(visible=False), [], gr.update(visible=False) conn = get_turso_conn() rows = conn.execute(f"SELECT id, title, author, year FROM books WHERE title LIKE '%{title}%' LIMIT 10").fetchall() conn.close() # DATI PER LA TABELLA data = [[str(r[1]), str(r[2]), str(r[3])] for r in rows] state = [{"id": r[0]} for r in rows] yield gr.update(visible=False), gr.update(samples=data, visible=True), state, gr.update(visible=False) def on_click_logic(idx, state, max_res, lang_pref): yield gr.update(visible=True), gr.update(visible=False) if idx >= len(state): return uid = state[idx]["id"] vec = client.retrieve(COLLECTION_NAME, [uid], with_vectors=True)[0].vector hits = client.query_points(COLLECTION_NAME, query=vec, limit=max_res, query_filter=models.Filter(must_not=[models.HasIdCondition(has_id=[uid])])).points ids = [h.id for h in hits] scores = {h.id: h.score for h in hits} yield gr.update(visible=False), gr.update(value=render_results(ids, scores, lang_pref), visible=True) # --- CLEANERS --- def clean_tab1(): return gr.update(value=""), gr.update(value="", visible=False) def clean_tab2(): return gr.update(value=""), gr.update(visible=False), gr.update(value="", visible=False) # --- UI --- with gr.Blocks(theme=gr.themes.Ocean()) as demo: app_title_md = gr.Markdown(f"# {L('app_title')}") books_state = gr.State([]) with gr.Row(): lang_selector = gr.Radio(choices=["it", "en", "es", "fr", "de", "ru"],value=DEFAULT_LANG, label=L("language_label")) num_results = gr.Slider(1, 10, value=5, step=1, label=L("results_label"), buttons = None) with gr.Tabs() as tabs: t1 = gr.Tab(label=L("plot_search_tab")) with t1: with gr.Row(): txt_in = gr.Textbox(placeholder=L("plot_search_placeholder"), show_label=False, scale=4) btn_search = gr.Button(L("plot_search_button"), variant="primary", scale=1) t2 = gr.Tab(label=L("title_search_tab")) with t2: with gr.Row(): txt_tit = gr.Textbox(placeholder=L("title_search_placeholder"), show_label=False, scale=4) btn_find = gr.Button(L("title_search_button"), scale=1) loader_list = gr.HTML(value=LOADING_HTML.replace("__LOADING_TEXT__", L("search_loading_message")), visible=False) # --- CORREZIONE TABELLA QUI SOTTO --- dataset = gr.Dataset( elem_id="book_cards", components=[gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False)], headers=[L("dataset_header_title"), L("dataset_header_author"), L("dataset_header_year")], label=L("dataset_label"), samples=[], type="index" ) loader = gr.HTML(value=LOADING_HTML.replace("__LOADING_TEXT__", L("search_loading_message")), visible=False) results = gr.HTML(label=L("results_label"), visible=True) # EVENTI # Elenco dei componenti da aggiornare in ordine # LISTA ORDINATA DEI COMPONENTI DA AGGIORNARE components_to_update = [ app_title_md, # 1. Titolo lang_selector, # 2. Radio num_results, # 3. Slider t1, # 4. Tab 1 txt_in, # 5. Textbox Trama btn_search, # 6. Bottone Cerca t2, # 7. Tab 2 txt_tit, # 8. Textbox Titolo btn_find, # 9. Bottone Trova dataset, # 10. Dataset results # 11. HTML Risultati ] # Evento di cambio lingua: lang_selector.change( fn=update_ui_labels, inputs=[lang_selector], outputs=components_to_update ) btn_search.click(search_logic, [txt_in, num_results, lang_selector], [loader, results]) txt_in.submit(search_logic, [txt_in, num_results, lang_selector], [loader, results]) btn_find.click(find_logic, [txt_tit], [loader_list, dataset, books_state, results]) txt_tit.submit(find_logic, [txt_tit], [loader_list, dataset, books_state, results]) dataset.click(on_click_logic, [dataset, books_state, num_results, lang_selector], [loader, results]) t1.select(clean_tab2, None, [txt_tit, dataset, results]) t2.select(clean_tab1, None, [txt_in, results]) demo.launch(css=GLOBAL_CSS, js=JS_FORCE_DARK)