import os from flask import Flask, request, jsonify from flask_cors import CORS import spacy import traceback # --- CARICAMENTO MODELLO --- try: nlp = spacy.load("it_core_news_lg") except OSError: raise RuntimeError( "Impossibile trovare il modello 'it_core_news_lg'. " "Assicurati che sia elencato e installato nel tuo requirements.txt.\n" "Comando per installarlo localmente: python -m spacy download it_core_news_lg" ) # --- INIZIALIZZAZIONE APP --- app = Flask(__name__) CORS(app) # --- MAPPE DI TRADUZIONE / SPIEGAZIONI --- SPIEGAZIONI_POS_IT = { "ADJ": "Aggettivo", "ADP": "Preposizione", "ADV": "Avverbio", "AUX": "Ausiliare", "CONJ": "Congiunzione", "CCONJ": "Congiunzione Coordinante", "SCONJ": "Congiunzione Subordinante", "DET": "Determinante", "INTJ": "Interiezione", "NOUN": "Sostantivo", "NUM": "Numerale", "PART": "Particella", "PRON": "Pronome", "PROPN": "Nome Proprio", "PUNCT": "Punteggiatura", "SPACE": "Spazio", "SYM": "Simbolo", "VERB": "Verbo", "X": "Altro", } SPIEGAZIONI_ENT_IT = { "PER": "Persona: Nomi di persone reali o fittizie.", "LOC": "Luogo: Nomi di luoghi geografici come paesi, città, stati.", "ORG": "Organizzazione: Nomi di aziende, istituzioni, governi.", "MISC": "Miscellanea: Entità che non rientrano nelle altre categorie (es. eventi, nazionalità, prodotti)." } TRADUZIONI_MORFOLOGIA = { # chiavi "Gender": "Genere", "Number": "Numero", "Mood": "Modo", "Tense": "Tempo", "Person": "Persona", "VerbForm": "Forma del Verbo", "PronType": "Tipo di Pronome", "Clitic": "Clitico", "Definite": "Definizione", "Degree": "Grado", "Case": "Caso", "Poss": "Possessivo", "Reflex": "Riflessivo", "Aspect": "Aspetto", "Voice": "Voce", "Polarity": "Polarità", # valori "Masc": "Maschile", "Fem": "Femminile", "Sing": "Singolare", "Plur": "Plurale", "Ind": "Indicativo", "Sub": "Congiuntivo", "Cnd": "Condizionale", "Imp": "Imperativo", "Pres": "Presente", "Past": "Passato", "Fut": "Futuro", "Pqp": "Trapassato", "Fin": "Finita", "Inf": "Infinito", "Part": "Participio", "Ger": "Gerundio", "Prs": "Personale", "Rel": "Relativo", "Int": "Interrogativo", "Dem": "Dimostrativo", "Art": "Articolativo", "Indf": "Indeterminato", "Yes": "Sì", "No": "No", "Abs": "Assoluto", "Cmp": "Comparativo", "Sup": "Superlativo", "Nom": "Nominativo", "Acc": "Accusativo", "Gen": "Genitivo", "Dat": "Dativo", "Perf": "Perfetto", "Prog": "Progressivo", "Act": "Attiva", "Pass": "Passiva", } MAPPA_DEP = { "nsubj": {"label": "Soggetto", "description": "Indica chi o cosa compie l'azione o si trova in un certo stato."}, "nsubj:pass": {"label": "Soggetto (Passivo)", "description": "Soggetto in una costruzione passiva."}, "ROOT": {"label": "Predicato Verbale", "description": "Esprime l'azione o lo stato del soggetto."}, "obj": {"label": "Complemento Oggetto", "description": "Indica l'oggetto diretto dell'azione del verbo."}, "iobj": {"label": "Complemento di Termine", "description": "Indica a chi o a cosa è destinata l'azione."}, "obl": {"label": "Complemento Indiretto", "description": "Fornisce informazioni aggiuntive come luogo, tempo, modo, causa, etc."}, "nmod": {"label": "Complemento di Specificazione", "description": "Specifica o chiarisce il significato del nome a cui si riferisce."}, "amod": {"label": "Attributo", "description": "Aggettivo che qualifica un nome."}, "advmod": {"label": "Complemento Avverbiale", "description": "Modifica il significato di un verbo, aggettivo o altro avverbio."}, "appos": {"label": "Apposizione", "description": "Nome che ne chiarisce un altro."}, "acl:relcl": {"label": "Proposizione Subordinata Relativa", "description": "Frase che espande un nome, introdotta da un pronome relativo."}, "advcl": {"label": "Proposizione Subordinata Avverbiale", "description": "Frase che funziona come un avverbio, modificando il verbo della principale."}, "ccomp": {"label": "Proposizione Subordinata Oggettiva", "description": "Frase che funge da complemento oggetto del verbo della principale."}, "csubj": {"label": "Proposizione Subordinata Soggettiva", "description": "Frase che funge da soggetto del verbo della principale."}, "xcomp": {"label": "Complemento Predicativo", "description": "Complemento che completa il significato del verbo."}, "acl": {"label": "Modificatore Relativo", "description": "Clausola che modifica un nome."}, "compound": {"label": "Composto", "description": "Parte di un composto nominale."}, "flat": {"label": "Nome Piatto", "description": "Parte di un nome proprio o espressione fissa."}, "conj": {"label": "Congiunzione Coordinata", "description": "Elemento coordinato con un altro."}, "cc": {"label": "Congiunzione Coordinante", "description": "Congiunzione che collega elementi coordinati."}, "parataxis": {"label": "Paratassi", "description": "Frasi coordinate senza connettore esplicito."} } # --- FUNZIONI DI UTILITÀ --- def spiega_in_italiano(tag, tipo='pos'): """ Fornisce spiegazione per POS o entità. - tipo == 'pos' => tag dovrebbe essere token.pos_ (es. VERB, NOUN) - tipo == 'ent' => ent label (PER, LOC, ORG...) """ if tipo == 'pos': key = tag.upper().split(":")[0] # normalizza caso e rimuove eventuali dettagli return SPIEGAZIONI_POS_IT.get(key, key) if tipo == 'ent': return SPIEGAZIONI_ENT_IT.get(tag, tag) return tag def traduci_morfologia_from_token(token): """ Usa token.morph (meglio token.morph.to_dict()) per costruire una descrizione leggibile. """ try: morph_dict = token.morph.to_dict() except Exception: # Fallback alla stringa originale morph_str = str(token.morph) if not morph_str: return "Non disponibile" parti = morph_str.split("|") mappate = [] for parte in parti: if "=" in parte: k, v = parte.split("=", 1) mappate.append(f"{TRADUZIONI_MORFOLOGIA.get(k,k)}: {TRADUZIONI_MORFOLOGIA.get(v, v)}") else: mappate.append(TRADUZIONI_MORFOLOGIA.get(parte, parte)) return ", ".join(mappate) if mappate else "Non disponibile" parti_tradotte = [] for k, v in sorted(morph_dict.items()): k_tr = TRADUZIONI_MORFOLOGIA.get(k, k) if isinstance(v, (list, tuple)): v_str = ", ".join(TRADUZIONI_MORFOLOGIA.get(x, x) for x in v) else: v_str = TRADUZIONI_MORFOLOGIA.get(v, v) parti_tradotte.append(f"{k_tr}: {v_str}") return ", ".join(parti_tradotte) if parti_tradotte else "Non disponibile" def get_verb_phrase(token): """ Costruisce la 'verb phrase' completa: aggiunge ausiliari, negazioni, particelle legate. Ordina i token per indice per mantenere la sequenza corretta. """ verb_related = [] # includi token stesso se è verbo o copula if token.pos_ in ("VERB", "AUX") or token.dep_ in ("cop", "ROOT"): verb_related.append(token) # cerca ausiliari, negazioni e copula fra i figli e nella testa (se la testa è verbo) for t in list(token.children): if t.dep_ in ('aux', 'aux:pass', 'neg', 'cop', 'prt'): verb_related.append(t) # talvolta l'ausiliare è head (in casi particolari); includi ausiliari nella testa che hanno head==token or viceversa # includi anche eventuali elementi nella subtree stretta che sono parte del verbo for t in token.subtree: if t.dep_ in ('aux', 'aux:pass', 'neg', 'cop', 'prt') and t not in verb_related: verb_related.append(t) # rimuovi duplicati e ordina verb_related = sorted(set(verb_related), key=lambda x: x.i) return " ".join(t.text for t in verb_related).strip() or token.text def ottieni_tipo_complemento_con_dettagli(token): preposizione = "" for figlio in token.children: if figlio.dep_ == "case": preposizione = figlio.text.lower() break if not preposizione and token.dep_ == 'obl': for figlio in token.head.children: if figlio.dep_ == "case" and figlio.head == token: preposizione = figlio.text.lower() break mappa_preposizioni = { "di": "Complemento di Specificazione", "del": "Complemento di Specificazione", "dello": "Complemento di Specificazione", "della": "Complemento di Specificazione", "dei": "Complemento di Specificazione", "a": "Complemento di Termine", "al": "Complemento di Termine", "da": "Complemento di Moto da Luogo", "dal": "Complemento di Moto da Luogo", "in": "Complemento di Stato in Luogo", "nel": "Complemento di Stato in Luogo", "con": "Complemento di Compagnia o Mezzo", "su": "Complemento di Argomento o Luogo", "per": "Complemento di Fine o Causa", "tra": "Complemento di Luogo o Tempo (Partitivo)", "fra": "Complemento di Luogo o Tempo (Partitivo)", } label = mappa_preposizioni.get(preposizione, "Complemento Indiretto") description = "Fornisce un'informazione generica non classificata in modo più specifico." if label == "Complemento Indiretto" else "Risponde alla domanda appropriata per il tipo di complemento." if preposizione.startswith("da") and any(figlio.dep_ in ('aux:pass','aux') for figlio in token.head.children): label = "Complemento d'Agente" description = "Indica da chi è compiuta l'azione in una frase passiva." return {"label": label, "description": description} def ottieni_testo_completo(token): """ Raccoglie il sintagma completo includendo determinanti, aggettivi, apposizioni, complementi stretti. """ def raccogli_figli(t): figli = [] for f in t.children: if f.dep_ in ('det', 'amod', 'case', 'advmod', 'nmod', 'appos', 'acl', 'compound', 'flat', 'nummod'): figli.append(f) figli.extend(raccogli_figli(f)) return figli token_sintagma = [token] + raccogli_figli(token) token_sintagma = sorted(set(token_sintagma), key=lambda x: x.i) return " ".join(t.text for t in token_sintagma if not t.is_punct).strip() def costruisci_sintagmi_con_dettagli(lista_token): """ Costruisce la lista di sintagmi (soggetto, predicato, complementi) con dettagli per ogni token significativo. Non scartiamo ausiliari e copula: li rappresentiamo opportunamente. """ mappa_sintagmi = {} # selezioniamo token utili (scartiamo solo punteggiatura e spazi) for token in lista_token: if token.is_punct or token.is_space: continue mappa_sintagmi[token.i] = { "text": ottieni_testo_completo(token), "token_details": { "text": token.text, "lemma": token.lemma_, "pos": token.pos_, "pos_explanation": spiega_in_italiano(token.pos_, 'pos'), "tag": token.tag_, "morph_raw": str(token.morph), "morph": traduci_morfologia_from_token(token) }, "label_info": {}, "token": token } risultato_analisi = [] indici_elaborati = set() for indice, sintagma in sorted(mappa_sintagmi.items()): if indice in indici_elaborati: continue token = sintagma['token'] dep = token.dep_ info_etichetta = MAPPA_DEP.get(dep, {"label": dep, "description": "Relazione non mappata."}) # gestione ROOT e predicato nominale con copula if dep == "ROOT": # trova eventuali copule collegate (cop o aux:cop) copula_children = [c for c in token.children if c.dep_ in ('cop', 'aux:cop')] if copula_children: cop = copula_children[0] # copula come verbo risultato_analisi.append({ "text": cop.text, "label_info": {"label": "Copula", "description": "Verbo 'essere' che collega soggetto e parte nominale."}, "token_details": { "lemma": cop.lemma_, "pos": cop.pos_, "pos_explanation": spiega_in_italiano(cop.pos_, 'pos'), "tag": cop.tag_, "morph": traduci_morfologia_from_token(cop), "verb_phrase": get_verb_phrase(cop) } }) # parte nominale (il nome/aggettivo che segue) risultato_analisi.append({ "text": sintagma["text"], "label_info": {"label": "Parte Nominale del Predicato", "description": "Parte nominale che descrive il soggetto."}, "token_details": sintagma["token_details"] }) indici_elaborati.add(indice) continue else: # ROOT come verbo principale: mostriamo verb phrase completa sintagma_da_aggiungere = { "text": sintagma['text'], "label_info": info_etichetta, "token_details": dict(sintagma['token_details']) } # se è verbo, aggiungi la verb_phrase if token.pos_ in ("VERB", "AUX"): sintagma_da_aggiungere["token_details"]["verb_phrase"] = get_verb_phrase(token) sintagma_da_aggiungere["token_details"]["verb_morph"] = traduci_morfologia_from_token(token) risultato_analisi.append(sintagma_da_aggiungere) indici_elaborati.add(indice) continue # gestione complementi obl e agent if dep in ('obl', 'obl:agent', 'obl:mod'): info_etichetta = ottieni_tipo_complemento_con_dettagli(token) if dep == 'nsubj:pass': info_etichetta = MAPPA_DEP.get('nsubj:pass', MAPPA_DEP['nsubj']) sintagma_da_aggiungere = { "text": sintagma['text'], "label_info": info_etichetta } if sintagma.get("token_details"): # se è un verbo o ausiliare aggiungiamo la verb_phrase if token.pos_ in ("VERB", "AUX"): sintagma['token_details']["verb_phrase"] = get_verb_phrase(token) sintagma['token_details']["verb_morph"] = traduci_morfologia_from_token(token) sintagma_da_aggiungere["token_details"] = sintagma["token_details"] risultato_analisi.append(sintagma_da_aggiungere) indici_elaborati.add(indice) # rimuovi duplicati (testo) risultato_unico = [] testi_visti = set() for item in risultato_analisi: if item['text'] not in testi_visti: risultato_unico.append(item) testi_visti.add(item['text']) return risultato_unico def analizza_proposizione_con_dettagli(token_proposizione): token_nella_proposizione = [t for t in token_proposizione if not t.is_punct and not t.is_space] return costruisci_sintagmi_con_dettagli(token_nella_proposizione) # --- ENDPOINT --- @app.route("/") def home(): return jsonify({"messaggio": "L'API per l'analisi logica è in esecuzione. Usa l'endpoint /api/analyze."}) @app.route('/api/analyze', methods=['POST']) def analizza_frase(): try: dati = request.get_json() if not dati or 'sentence' not in dati: return jsonify({"errore": "Frase non fornita"}), 400 frase = dati['sentence'].strip() if not frase: return jsonify({"errore": "Frase vuota"}), 400 doc = nlp(frase) proposizioni_subordinate = [] indici_subordinate = set() for token in doc: if token.dep_ in ["acl:relcl", "advcl", "ccomp", "csubj", "xcomp", "acl", "parataxis", "parataxis:rel"]: token_proposizione_subordinata = list(token.subtree) for t in token_proposizione_subordinata: indici_subordinate.add(t.i) info_tipo_subordinata = MAPPA_DEP.get(token.dep_, {"label": "Proposizione Subordinata", "description": "Una frase che dipende da un'altra."}) marcatore = [figlio for figlio in token.children if figlio.dep_ == 'mark'] intro = marcatore[0].text if marcatore else "" proposizioni_subordinate.append({ "type_info": info_tipo_subordinata, "text": " ".join(t.text for t in token_proposizione_subordinata if not t.is_punct and not t.is_space).strip(), "intro": intro, "analysis": analizza_proposizione_con_dettagli(token_proposizione_subordinata) }) token_proposizione_principale = [token for token in doc if token.i not in indici_subordinate] entita_nominate = [{ "text": ent.text, "label": ent.label_, "explanation": spiega_in_italiano(ent.label_, 'ent') } for ent in doc.ents] # Rimuovi duplicati dalle entità entita_unica = [] testi_ent_visti = set() for ent in entita_nominate: if ent['text'] not in testi_ent_visti: entita_unica.append(ent) testi_ent_visti.add(ent['text']) analisi_finale = { "full_sentence": frase, "main_clause": { "text": " ".join(t.text for t in token_proposizione_principale if not t.is_punct and not t.is_space).strip(), "analysis": analizza_proposizione_con_dettagli(token_proposizione_principale) }, "subordinate_clauses": proposizioni_subordinate, "named_entities": entita_unica } return jsonify(analisi_finale) except Exception as e: traceback.print_exc() return jsonify({"errore": "Si è verificato un errore interno.", "dettagli": str(e)}), 500 if __name__ == '__main__': porta = int(os.environ.get("PORT", 8080)) app.run(host="0.0.0.0", port=porta, debug=False, threaded=True)