analysis_tool / app.py
devusman's picture
Update app.py
699c193 verified
raw
history blame
18.1 kB
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)