File size: 15,558 Bytes
42693f7
05d3a8d
42693f7
 
05d3a8d
 
 
 
 
 
42693f7
7abf422
05d3a8d
7abf422
 
 
05d3a8d
 
 
 
e8fa023
05d3a8d
7abf422
 
 
 
05d3a8d
 
7abf422
 
05d3a8d
7abf422
 
e8fa023
870c988
05d3a8d
42693f7
05d3a8d
7abf422
 
 
 
42693f7
 
 
7abf422
 
 
e90e953
 
 
 
 
 
 
 
 
e8fa023
e90e953
 
7abf422
05d3a8d
7abf422
 
5a3f6ab
 
7abf422
e8fa023
7abf422
 
e8fa023
 
 
 
 
 
 
5a3f6ab
7abf422
05d3a8d
 
7abf422
 
 
 
 
96e4672
31edf0b
05d3a8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42693f7
 
7abf422
05d3a8d
7abf422
699c193
05d3a8d
e8fa023
 
 
96e4672
05d3a8d
 
e8fa023
 
 
05d3a8d
 
 
 
e8fa023
 
7abf422
05d3a8d
e8fa023
 
 
05d3a8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8fa023
 
05d3a8d
 
 
e8fa023
05d3a8d
e8fa023
 
05d3a8d
e8fa023
05d3a8d
e8fa023
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699c193
e8fa023
 
 
 
96e4672
 
05d3a8d
e8fa023
 
699c193
05d3a8d
e8fa023
4f5a1e9
05d3a8d
e8fa023
05d3a8d
 
e8fa023
96e4672
05d3a8d
 
31edf0b
e8fa023
 
 
 
05d3a8d
 
7abf422
05d3a8d
 
 
 
 
e8fa023
 
05d3a8d
 
4f5a1e9
e8fa023
 
 
 
 
 
 
 
 
 
 
 
05d3a8d
7abf422
e8fa023
 
 
4f5a1e9
7abf422
 
 
42693f7
 
e8fa023
7abf422
e8fa023
 
7abf422
 
42693f7
96e4672
e8fa023
05d3a8d
 
42693f7
7abf422
 
8153da1
7abf422
8153da1
96e4672
05d3a8d
e8fa023
7abf422
699c193
42693f7
e8fa023
7abf422
e8fa023
 
 
 
 
 
 
 
4f5a1e9
7abf422
05d3a8d
7abf422
 
 
 
e8fa023
7abf422
e8fa023
 
7abf422
8153da1
96e4672
e8fa023
4f5a1e9
e8fa023
7abf422
4f5a1e9
96e4672
7abf422
4f5a1e9
699c193
96e4672
42693f7
 
4f5a1e9
8153da1
4f5a1e9
 
7abf422
05d3a8d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
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
    suggestion = (
        "Impossibile caricare un modello italiano spaCy. "
        "Installa almeno uno tra: it_core_news_lg / it_core_news_md / it_core_news_sm.\n"
        f"Esempio: python -m spacy download it_core_news_lg\nDettagli 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", "LOC": "Luogo", "ORG": "Organizzazione", "MISC": "Miscellanea"
}

# ------------------------------
# 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)
    if tipo == 'ent': return f"{SPIEGAZIONI_ENT_IT.get(tag, tag)}: {SPIEGAZIONI_ENT_IT.get(tag, {}).get('description', '')}"
    return tag

def traduci_morfologia(morph_str: str) -> str:
    if not morph_str or morph_str == "___": return "Non disponibile"
    parti = morph_str.split('|')
    parti_tradotte = set()
    for parte in parti:
        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.add(f"{chiave_trad}: {valore_trad}")
    return ", ".join(sorted(list(parti_tradotte))) or "Non disponibile"

def ottieni_tipo_complemento_con_dettagli(token):
    case_token = next((child for child in token.children if child.dep_ == 'case'), None)
    if not case_token: return MAPPA_DEP.get("obl")
    preposizione = case_token.text.lower()

    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?"),
    }
    
    for base, (label, desc) in mappa.items():
        if preposizione.startswith(base):
            if base == "da" and any(c.dep_ == "aux:pass" for c in token.head.children):
                return {"label": "Complemento d'Agente", "description": "Indica da chi è compiuta l'azione in una frase passiva."}
            return {"label": label, "description": desc}
            
    return MAPPA_DEP.get("obl")

def get_full_phrase_for_token(token):
    """
    FIXED: Costruisce un sintagma in modo preciso, raccogliendo solo i modificatori
    strettamente collegati e gli elementi coordinati.
    """
    phrase_tokens = []
    
    # Funzione interna per raccogliere i token di un singolo elemento e i suoi figli diretti
    def collect_children(t):
        # Raccoglie i modificatori diretti (articoli, aggettivi, preposizioni)
        children = [t]
        for child in t.children:
            if child.dep_ in ('det', 'amod', 'case', 'compound', 'advmod', 'appos'):
                children.extend(collect_children(child)) # Raccoglie anche i figli dei figli (es. avverbi di aggettivi)
        return children

    # Raccoglie i token per il token principale
    phrase_tokens.extend(collect_children(token))

    # Gestisce la coordinazione (es. "libri e quaderni")
    for child in token.children:
        if child.dep_ == 'conj':
            # Aggiunge la congiunzione (es. "e", "o")
            cc = next((c for c in child.children if c.dep_ == 'cc'), None)
            if cc:
                phrase_tokens.append(cc)
            # Aggiunge l'intero sintagma coordinato
            phrase_tokens.extend(get_full_phrase_for_token(child))

    # Ordina i token in base alla loro posizione originale e rimuove duplicati
    unique_tokens = sorted(list(set(phrase_tokens)), key=lambda t: t.i)
    text = " ".join(t.text for t in unique_tokens)
    indices = {t.i for t in unique_tokens}
    return text, indices

def costruisci_sintagmi_con_dettagli(tokens_proposizione):
    """
    FIXED: L'algoritmo ora processa ogni componente logico separatamente e con precisione.
    """
    risultato_analisi = []
    indici_elaborati = set()
    
    # Definisce le dipendenze che non sono "teste" di un sintagma ma parti di esso
    DEPS_DA_SALTARE = {'det', 'amod', 'case', 'aux', 'aux:pass', 'cop', 'mark', 'cc', 'advmod', 'compound', 'appos'}

    for token in tokens_proposizione:
        if token.i in indici_elaborati or token.dep_ in DEPS_DA_SALTARE:
            continue
            
        testo_sintagma, indici_usati = get_full_phrase_for_token(token)
        
        dep = token.dep_
        if dep in ('obl', 'obl:agent', 'nmod'):
            info_etichetta = ottieni_tipo_complemento_con_dettagli(token)
        else:
            info_etichetta = MAPPA_DEP.get(dep, {"label": dep.capitalize(), "description": "Relazione non mappata."})

        # Caso speciale per predicato nominale
        if dep == "ROOT" and any(c.dep_ == 'cop' for c in token.children):
            info_etichetta = {"label": "Parte Nominale del Predicato", "description": "Aggettivo o nome che descrive il soggetto."}

        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))
            },
            "token_index": token.i
        })
        indici_elaborati.update(indici_usati)

    # Aggiungi componenti saltati (es. copula, congiunzioni) che sono importanti
    for token in tokens_proposizione:
        if token.i not in indici_elaborati and token.dep_ in ('cop', 'cc'):
            risultato_analisi.append({
                "text": token.text,
                "label_info": MAPPA_DEP.get(token.dep_),
                "token_details": { "lemma": token.lemma_, "pos": f"{token.pos_}: {spiega_in_italiano(token.pos_)}", "morph": traduci_morfologia(str(token.morph)) },
                "token_index": token.i
            })

    # Ordina i risultati finali in base all'indice del token principale
    risultato_analisi.sort(key=lambda x: x['token_index'])
    return risultato_analisi

def analizza_proposizione_con_dettagli(tokens):
    tokens_validi = [t for t in tokens if not t.is_punct and not t.is_space]
    return costruisci_sintagmi_con_dettagli(tokens_validi)

# ------------------------------
# Routes
# ------------------------------
@app.route("/")
def home():
    status = "ok" if nlp else "model_missing"
    return jsonify({
        "messaggio": "API analisi logica in esecuzione", "modello_spacy": IT_MODEL or "Nessuno",
        "model_status": status, "model_error": MODEL_LOAD_ERROR, "endpoint": "/api/analyze"
    })

@app.route('/api/analyze', methods=['POST'])
def analizza_frase():
    if not nlp:
        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 and token.i not in indici_subordinate:
                subtree = list(token.subtree)
                indici_subtree = {t.i for t in subtree}
                indici_subordinate.update(indici_subtree)
                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 if not t.is_punct).strip(),
                    "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:
                visti.add(ent.text)
                entita_nominate.append({
                    "text": ent.text, "label": ent.label_,
                    "explanation": f"{SPIEGAZIONI_ENT_IT.get(ent.label_, ent.label_)}"
                })

        analisi_finale = {
            "full_sentence": frase, "model": IT_MODEL,
            "main_clause": {
                "text": " ".join(t.text for t in token_principale if not t.is_punct).strip(),
                "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)