File size: 18,145 Bytes
42693f7
 
 
 
31edf0b
42693f7
699c193
42693f7
8153da1
870c988
 
8153da1
699c193
 
870c988
42693f7
699c193
42693f7
 
 
699c193
e90e953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5a3f6ab
699c193
5a3f6ab
 
699c193
8153da1
699c193
 
 
 
 
5a3f6ab
699c193
 
8153da1
 
 
 
5a3f6ab
 
96e4672
31edf0b
8153da1
31edf0b
 
 
 
 
 
 
 
 
 
 
8153da1
 
 
 
 
 
699c193
 
42693f7
 
699c193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96e4672
 
 
 
 
42693f7
8153da1
96e4672
8153da1
96e4672
4f5a1e9
42693f7
8153da1
 
699c193
 
 
 
 
 
 
8153da1
 
 
 
 
 
699c193
8153da1
 
 
 
42693f7
96e4672
699c193
 
 
8153da1
699c193
 
 
 
8153da1
 
 
 
699c193
8153da1
96e4672
 
699c193
 
 
 
96e4672
699c193
 
96e4672
699c193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96e4672
 
699c193
e90e953
96e4672
4f5a1e9
699c193
96e4672
4f5a1e9
8153da1
4f5a1e9
699c193
4f5a1e9
699c193
 
 
 
 
96e4672
699c193
 
31edf0b
699c193
 
 
 
 
 
31edf0b
4f5a1e9
699c193
96e4672
699c193
 
8153da1
4f5a1e9
8153da1
 
4f5a1e9
699c193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96e4672
8153da1
699c193
 
31edf0b
699c193
 
 
 
 
 
 
 
 
 
 
 
96e4672
699c193
 
8153da1
 
 
 
 
 
 
4f5a1e9
96e4672
699c193
96e4672
4f5a1e9
699c193
 
42693f7
 
96e4672
42693f7
 
96e4672
42693f7
96e4672
 
 
42693f7
8153da1
 
 
 
96e4672
699c193
96e4672
e90e953
699c193
42693f7
699c193
96e4672
e90e953
 
 
96e4672
 
 
4f5a1e9
96e4672
 
8153da1
4f5a1e9
96e4672
4f5a1e9
 
96e4672
4f5a1e9
96e4672
31edf0b
 
e90e953
31edf0b
 
8153da1
 
 
 
 
 
 
 
96e4672
 
4f5a1e9
8153da1
96e4672
4f5a1e9
96e4672
8153da1
4f5a1e9
699c193
96e4672
42693f7
 
4f5a1e9
8153da1
4f5a1e9
 
96e4672
699c193
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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
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)