Spaces:
Sleeping
Sleeping
| import os | |
| import traceback | |
| from flask import Flask, request, jsonify | |
| from flask_cors import CORS | |
| # Try to import spacy lazily and handle missing models gracefully | |
| try: | |
| import spacy | |
| except Exception: | |
| spacy = None | |
| # ------------------------------ | |
| # Caricamento modello spaCy (con fallback non-bloccante) | |
| # ------------------------------ | |
| def load_it_model(): | |
| """ | |
| Prova a caricare un modello italiano in ordine di qualità. | |
| Se nessun modello è installato, restituisce (None, None) e una istruzione per l'utente. | |
| """ | |
| if spacy is None: | |
| return None, None, ( | |
| "La libreria spaCy non è installata. " | |
| "Installa spaCy: pip install spacy" | |
| ) | |
| candidates = ["it_core_news_lg", "it_core_news_md", "it_core_news_sm"] | |
| last_err = None | |
| for name in candidates: | |
| try: | |
| nlp = spacy.load(name) | |
| return nlp, name, None | |
| except Exception as e: | |
| last_err = e | |
| # nessun modello trovato -> non fallare l'import, ma restituire messaggio utile | |
| suggestion = ( | |
| "Impossibile caricare un modello italiano spaCy. " | |
| "Installa almeno uno tra: it_core_news_lg / it_core_news_md / it_core_news_sm.\n" | |
| "Esempio: python -m spacy download it_core_news_lg\n" | |
| f"Dettagli ultimo errore: {last_err}" | |
| ) | |
| return None, None, suggestion | |
| nlp, IT_MODEL, MODEL_LOAD_ERROR = load_it_model() | |
| # ------------------------------ | |
| # Flask App | |
| # ------------------------------ | |
| app = Flask(__name__) | |
| CORS(app) | |
| # ------------------------------ | |
| # Tabelle di spiegazione POS / NER | |
| # ------------------------------ | |
| 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 (eventi, nazionalità, prodotti)." | |
| } | |
| # ------------------------------ | |
| # Traduzioni Morfologia (UD) | |
| # ------------------------------ | |
| KEY_MAP = { | |
| "Gender": "Genere", "Number": "Numero", "Mood": "Modo", "Tense": "Tempo", | |
| "Person": "Persona", "VerbForm": "Forma del Verbo", "PronType": "Tipo di Pronome", | |
| "Clitic": "Clitico", "Definite": "Definitezza", "Degree": "Grado", | |
| "Case": "Caso", "Poss": "Possessivo", "Reflex": "Riflessivo", | |
| "Aspect": "Aspetto", "Voice": "Voce", | |
| } | |
| VALUE_MAP = { | |
| "Masc": "Maschile", "Fem": "Femminile", "Sing": "Singolare", "Plur": "Plurale", | |
| "Cnd": "Condizionale", "Sub": "Congiuntivo", "Ind": "Indicativo", "Imp": "Imperfetto", | |
| "Inf": "Infinito", "Part": "Participio", "Ger": "Gerundio", "Fin": "Finita", | |
| "Pres": "Presente", "Past": "Passato", "Fut": "Futuro", "Pqp": "Trapassato", | |
| "1": "1ª", "2": "2ª", "3": "3ª", "Prs": "Personale", "Rel": "Relativo", "Int": "Interrogativo", | |
| "Dem": "Dimostrativo", "Art": "Articolativo", "Yes": "Sì", "No": "No", "Def": "Determinato", | |
| "Indef": "Indefinito", "Abs": "Assoluto", "Cmp": "Comparativo", "Sup": "Superlativo", | |
| "Nom": "Nominativo", "Acc": "Accusativo", "Gen": "Genitivo", "Dat": "Dativo", | |
| "Perf": "Perfetto", "Prog": "Progressivo", "Act": "Attiva", "Pass": "Passiva", | |
| } | |
| PAIR_VALUE_MAP = { | |
| ("Mood", "Imp"): "Imperativo", ("Tense", "Imp"): "Imperfetto", | |
| ("Mood", "Ind"): "Indicativo", ("Definite", "Ind"): "Indeterminato", | |
| } | |
| # ------------------------------ | |
| # Mappature Dependency → Etichette italiane | |
| # ------------------------------ | |
| 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 di una frase in forma passiva."}, | |
| "ROOT": {"label": "Predicato Verbale", "description": "Esprime l'azione, l'esistenza o lo stato del soggetto."}, | |
| "obj": {"label": "Complemento Oggetto", "description": "Indica l'oggetto diretto dell'azione. Risponde alla domanda: chi? / che cosa?"}, | |
| "iobj": {"label": "Complemento di Termine", "description": "Indica a chi o a cosa è destinata l'azione. Risponde alla domanda: a chi? / a che cosa?"}, | |
| "obl": {"label": "Complemento Indiretto", "description": "Fornisce informazioni aggiuntive (luogo, tempo, modo, causa, ecc.)."}, | |
| "obl:agent": {"label": "Complemento d'Agente", "description": "Indica chi compie l'azione in una frase passiva. Risponde alla domanda: da chi?"}, | |
| "nmod": {"label": "Complemento di Specificazione", "description": "Specifica o definisce un altro nome. Risponde alla domanda: di chi? / di che cosa?"}, | |
| "amod": {"label": "Attributo", "description": "Aggettivo che qualifica o descrive un nome a cui si riferisce."}, | |
| "advmod": {"label": "Complemento Avverbiale", "description": "Modifica o precisa il significato di un verbo, aggettivo o altro avverbio."}, | |
| "appos": {"label": "Apposizione", "description": "Sostantivo che si affianca a un altro per meglio identificarlo."}, | |
| "acl:relcl": {"label": "Proposizione Subordinata Relativa", "description": "Frase introdotta da un pronome relativo che espande un nome."}, | |
| "advcl": {"label": "Proposizione Subordinata Avverbiale", "description": "Frase che funziona come un complemento avverbiale per la 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": "Completa il significato del verbo riferendosi al soggetto o all'oggetto."}, | |
| "conj": {"label": "Elemento Coordinato", "description": "Elemento collegato a un altro con la stessa funzione logica."}, | |
| "cc": {"label": "Congiunzione Coordinante", "description": "Congiunzione (es. e, ma, o) che collega elementi con la stessa funzione."}, | |
| "cop": {"label": "Copula", "description": "Verbo 'essere' che collega il soggetto a un nome o aggettivo (parte nominale)."}, | |
| } | |
| # ------------------------------ | |
| # Utilità di Analisi | |
| # ------------------------------ | |
| def spiega_in_italiano(tag, tipo='pos'): | |
| if tipo == 'pos': | |
| return SPIEGAZIONI_POS_IT.get(tag, tag) | |
| return SPIEGAZIONI_ENT_IT.get(tag, tag) | |
| def traduci_morfologia(morph_str: str) -> str: | |
| if not morph_str or morph_str == "___": return "Non disponibile" | |
| parti_tradotte = [] | |
| for parte in morph_str.split('|'): | |
| if '=' not in parte: continue | |
| chiave, valore = parte.split('=', 1) | |
| chiave_trad = KEY_MAP.get(chiave, chiave) | |
| valore_trad = PAIR_VALUE_MAP.get((chiave, valore), VALUE_MAP.get(valore, valore)) | |
| parti_tradotte.append(f"{chiave_trad}: {valore_trad}") | |
| return ", ".join(sorted(list(set(parti_tradotte)))) or "Non disponibile" | |
| def ottieni_tipo_complemento_con_dettagli(token): | |
| preposizione = next((t.text.lower() for t in token.children if t.dep_ == "case"), None) | |
| if not preposizione: return MAPPA_DEP.get("obl") | |
| mappa = { | |
| "di": ("Complemento di Specificazione", "Risponde alla domanda: di chi? / di che cosa?"), | |
| "a": ("Complemento di Termine", "Risponde alla domanda: a chi? / a che cosa?"), | |
| "da": ("Complemento di Moto da Luogo", "Risponde alla domanda: da dove?"), | |
| "in": ("Complemento di Stato in Luogo", "Risponde alla domanda: dove?"), | |
| "con": ("Complemento di Compagnia o Mezzo", "Risponde alla domanda: con chi? / con che cosa?"), | |
| "su": ("Complemento di Argomento o Luogo", "Risponde alla domanda: su chi? / su che cosa? / dove?"), | |
| "per": ("Complemento di Fine o Causa", "Risponde alla domanda: per quale fine? / per quale causa?"), | |
| "tra": ("Complemento Partitivo / Luogo", "Risponde alla domanda: tra chi? / tra cosa?"), | |
| "fra": ("Complemento Partitivo / Luogo", "Risponde alla domanda: fra chi? / fra cosa?"), | |
| } | |
| # Gestione preposizioni articolate | |
| for base, (label, desc) in mappa.items(): | |
| if preposizione.startswith(base): | |
| label_final = label | |
| desc_final = desc | |
| # Check per complemento d'agente | |
| if base == "da" and any(c.dep_ == "aux:pass" for c in token.head.children): | |
| label_final = "Complemento d'Agente" | |
| desc_final = "Indica da chi è compiuta l'azione in una frase passiva." | |
| return {"label": label_final, "description": desc_final} | |
| return MAPPA_DEP.get("obl") | |
| def costruisci_sintagmi_con_dettagli(tokens_proposizione): | |
| """ | |
| Costruisce una lista di componenti logici (sintagmi) da una lista di token spaCy. | |
| Questa versione è più precisa e robusta, evitando di raggruppare erroneamente i componenti. | |
| """ | |
| def get_phrase_and_indices(token): | |
| """Costruisce il testo di un sintagma e restituisce gli indici dei token usati.""" | |
| # Raccoglie ricorsivamente i token coordinati (es. "libri e quaderni") | |
| conjuncts = [c for c in token.children if c.dep_ == 'conj'] | |
| tokens_nel_sintagma = [token] | |
| for conj in conjuncts: | |
| tokens_nel_sintagma.append(conj) | |
| # Aggiunge anche le congiunzioni (es. "e", "o") | |
| cc = next((c for c in conj.children if c.dep_ == 'cc'), None) | |
| if cc: tokens_nel_sintagma.append(cc) | |
| # Per ogni token principale, raccoglie i suoi modificatori diretti (articoli, aggettivi, etc.) | |
| all_phrase_tokens = [] | |
| for t in tokens_nel_sintagma: | |
| subtree = list(t.subtree) | |
| # Filtra per tenere solo i modificatori strettamente legati | |
| modificatori = [n for n in subtree if n.head == t and n.dep_ in ('det', 'amod', 'advmod', 'case', 'compound', 'appos', 'nmod')] | |
| all_phrase_tokens.extend([t] + modificatori) | |
| # Assicura che la congiunzione sia inclusa se presente | |
| all_phrase_tokens.extend(c for c in token.children if c.dep_ == 'cc') | |
| # Ordina i token e crea la stringa finale | |
| tokens_ordinati = sorted(list(set(all_phrase_tokens)), key=lambda x: x.i) | |
| testo_sintagma = " ".join(t.text for t in tokens_ordinati) | |
| indici_usati = {t.i for t in tokens_ordinati} | |
| return testo_sintagma, indici_usati | |
| risultato_analisi = [] | |
| indici_elaborati = set() | |
| # Token da non processare come "teste" di un sintagma (verranno inclusi dai loro "genitori") | |
| SKIP_DEPS = {'det', 'case', 'punct', 'aux', 'cop', 'mark', 'cc', 'aux:pass', 'amod', 'advmod'} | |
| for token in tokens_proposizione: | |
| if token.i in indici_elaborati or token.dep_ in SKIP_DEPS: | |
| continue | |
| # Gestione speciale per la copula in predicati nominali | |
| if token.dep_ == "ROOT" and any(c.dep_ == 'cop' for c in token.children): | |
| cop_token = next(c for c in token.children if c.dep_ == 'cop') | |
| # 1. Aggiungi il soggetto | |
| soggetto = next((s for s in token.head.children if s.dep_.startswith('nsubj')), None) | |
| if soggetto and soggetto.i not in indici_elaborati: | |
| s_text, s_indices = get_phrase_and_indices(soggetto) | |
| risultato_analisi.append({ | |
| "text": s_text, "label_info": MAPPA_DEP['nsubj'], | |
| "token_details": {"lemma": soggetto.lemma_, "pos": spiega_in_italiano(soggetto.pos_), "morph": traduci_morfologia(str(soggetto.morph))} | |
| }) | |
| indici_elaborati.update(s_indices) | |
| # 2. Aggiungi la copula | |
| risultato_analisi.append({ | |
| "text": cop_token.text, "label_info": MAPPA_DEP['cop'], | |
| "token_details": {"lemma": cop_token.lemma_, "pos": spiega_in_italiano(cop_token.pos_), "morph": traduci_morfologia(str(cop_token.morph))} | |
| }) | |
| indici_elaborati.add(cop_token.i) | |
| # 3. Aggiungi la parte nominale | |
| pn_text, pn_indices = get_phrase_and_indices(token) | |
| risultato_analisi.append({ | |
| "text": pn_text, "label_info": {"label": "Parte Nominale del Predicato", "description": "Aggettivo o nome che descrive il soggetto."}, | |
| "token_details": {"lemma": token.lemma_, "pos": spiega_in_italiano(token.pos_), "morph": traduci_morfologia(str(token.morph))} | |
| }) | |
| indici_elaborati.update(pn_indices) | |
| continue | |
| # Logica standard per tutti gli altri componenti | |
| testo_sintagma, indici_usati = get_phrase_and_indices(token) | |
| dep = token.dep_ | |
| if dep in ('obl', 'obl:agent'): | |
| info_etichetta = ottieni_tipo_complemento_con_dettagli(token) | |
| else: | |
| info_etichetta = MAPPA_DEP.get(dep, {"label": dep.capitalize(), "description": "Relazione non mappata."}) | |
| risultato_analisi.append({ | |
| "text": testo_sintagma, | |
| "label_info": info_etichetta, | |
| "token_details": { | |
| "lemma": token.lemma_, | |
| "pos": f"{token.pos_}: {spiega_in_italiano(token.pos_)}", | |
| "tag": f"{token.tag_}: {spiega_in_italiano(token.tag_)}", | |
| "morph": traduci_morfologia(str(token.morph)) | |
| } | |
| }) | |
| indici_elaborati.update(indici_usati) | |
| # Ordina i risultati in base alla loro apparizione nella frase | |
| risultato_analisi.sort(key=lambda x: x['text'].split()[0] in [t.text for t in tokens_proposizione] and [t.text for t in tokens_proposizione].index(x['text'].split()[0])) | |
| return risultato_analisi | |
| def analizza_proposizione_con_dettagli(token_proposizione): | |
| token_validi = [t for t in token_proposizione if not t.is_punct and not t.is_space] | |
| return costruisci_sintagmi_con_dettagli(token_validi) | |
| # ------------------------------ | |
| # Routes | |
| # ------------------------------ | |
| def home(): | |
| status = "ok" if nlp is not None else "model_missing" | |
| return jsonify({ | |
| "messaggio": "API analisi logica in esecuzione", | |
| "modello_spacy": IT_MODEL if IT_MODEL else "Nessuno", | |
| "model_status": status, | |
| "model_error": MODEL_LOAD_ERROR, | |
| "endpoint": "/api/analyze" | |
| }) | |
| def analizza_frase(): | |
| if nlp is None: | |
| return jsonify({"errore": "Modello spaCy non caricato.", "dettagli": MODEL_LOAD_ERROR}), 503 | |
| try: | |
| dati = request.get_json(silent=True) or {} | |
| frase = (dati.get('sentence') or "").strip() | |
| if not frase: | |
| return jsonify({"errore": "Frase non fornita o vuota."}), 400 | |
| doc = nlp(frase) | |
| proposizioni_subordinate = [] | |
| indici_subordinate = set() | |
| SUBORD_DEPS = {"acl:relcl", "advcl", "ccomp", "csubj", "xcomp", "acl", "parataxis"} | |
| for token in doc: | |
| if token.dep_ in SUBORD_DEPS: | |
| subtree = list(token.subtree) | |
| subtree_indices = {t.i for t in subtree} | |
| if not indici_subordinate.intersection(subtree_indices): | |
| indici_subordinate.update(subtree_indices) | |
| info_tipo = MAPPA_DEP.get(token.dep_, {"label": "Proposizione Subordinata", "description": "Frase che dipende da un'altra."}) | |
| proposizioni_subordinate.append({ | |
| "type_info": info_tipo, | |
| "text": " ".join(t.text for t in subtree), | |
| "analysis": analizza_proposizione_con_dettagli(subtree) | |
| }) | |
| token_principale = [t for t in doc if t.i not in indici_subordinate] | |
| entita_nominate = [] | |
| visti = set() | |
| for ent in doc.ents: | |
| if ent.text not in visti: | |
| entita_nominate.append({ | |
| "text": ent.text, | |
| "label": ent.label_, | |
| "explanation": spiega_in_italiano(ent.label_, 'ent') | |
| }) | |
| visti.add(ent.text) | |
| analisi_finale = { | |
| "full_sentence": frase, | |
| "model": IT_MODEL, | |
| "main_clause": { | |
| "text": " ".join(t.text for t in token_principale), | |
| "analysis": analizza_proposizione_con_dettagli(token_principale) | |
| }, | |
| "subordinate_clauses": proposizioni_subordinate, | |
| "named_entities": entita_nominate | |
| } | |
| 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__': | |
| port = int(os.environ.get("PORT", 8080)) | |
| app.run(host="0.0.0.0", port=port, debug=False, threaded=True) |