File size: 16,598 Bytes
6058f1c
 
 
42693f7
05d3a8d
42693f7
 
05d3a8d
6058f1c
05d3a8d
 
 
 
42693f7
7abf422
6058f1c
7abf422
6058f1c
 
42693f7
7abf422
6058f1c
7abf422
e90e953
 
 
 
 
 
 
 
 
6058f1c
 
e90e953
 
7abf422
5a3f6ab
 
7abf422
e8fa023
7abf422
 
e8fa023
 
 
 
 
 
 
5a3f6ab
7abf422
05d3a8d
 
7abf422
 
 
6058f1c
7abf422
96e4672
31edf0b
05d3a8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42693f7
 
7abf422
6058f1c
7abf422
6058f1c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699c193
6058f1c
 
 
 
 
05d3a8d
6058f1c
 
 
 
e8fa023
96e4672
05d3a8d
6058f1c
 
e8fa023
6058f1c
e8fa023
6058f1c
 
05d3a8d
 
 
6058f1c
 
7abf422
05d3a8d
6058f1c
 
 
 
 
 
 
 
 
 
 
 
05d3a8d
 
 
6058f1c
05d3a8d
 
 
 
 
 
6058f1c
 
05d3a8d
 
6058f1c
 
 
e8fa023
 
6058f1c
05d3a8d
e8fa023
05d3a8d
6058f1c
 
05d3a8d
6058f1c
 
 
 
 
 
 
 
e8fa023
6058f1c
 
 
e8fa023
6058f1c
e8fa023
 
6058f1c
 
e8fa023
 
6058f1c
e8fa023
6058f1c
 
 
 
699c193
e8fa023
 
6058f1c
e8fa023
6058f1c
 
 
96e4672
699c193
05d3a8d
6058f1c
 
 
 
4f5a1e9
6058f1c
e8fa023
6058f1c
05d3a8d
e8fa023
96e4672
05d3a8d
 
31edf0b
6058f1c
 
 
 
 
 
e8fa023
6058f1c
05d3a8d
7abf422
6058f1c
e8fa023
05d3a8d
6058f1c
05d3a8d
4f5a1e9
6058f1c
e8fa023
 
6058f1c
e8fa023
6058f1c
 
 
 
 
 
 
e8fa023
 
6058f1c
e8fa023
6058f1c
 
7abf422
e8fa023
 
 
4f5a1e9
7abf422
6058f1c
7abf422
6058f1c
 
 
42693f7
 
e8fa023
7abf422
6058f1c
 
 
 
7abf422
 
42693f7
96e4672
6058f1c
e8fa023
05d3a8d
 
42693f7
7abf422
 
8153da1
7abf422
6058f1c
 
8153da1
96e4672
699c193
6058f1c
 
 
 
42693f7
e8fa023
7abf422
e8fa023
 
 
 
 
 
 
 
4f5a1e9
6058f1c
 
 
 
7abf422
 
 
 
e8fa023
7abf422
6058f1c
 
 
7abf422
8153da1
96e4672
6058f1c
 
4f5a1e9
6058f1c
7abf422
4f5a1e9
96e4672
7abf422
4f5a1e9
699c193
96e4672
42693f7
 
6058f1c
4f5a1e9
8153da1
4f5a1e9
 
7abf422
6058f1c
 
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

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

# ------------------------------
# Config
# ------------------------------
MAX_SENTENCE_LENGTH = 2000  # characters, to avoid huge inputs
SUBORD_DEPS = {"acl:relcl", "advcl", "ccomp", "csubj", "xcomp", "acl", "parataxis"}

# ------------------------------
# Utility dictionaries (Italian)
# ------------------------------
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",
    # spaCy uses many possible entity labels depending on model/lang — fallback to label itself later
}

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",
}

# ------------------------------
# Map dependency labels to Italian labels and explanations
# ------------------------------
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)."},
}

# ------------------------------
# Model load helper (non-blocking)
# ------------------------------
def load_it_model():
    """
    Try to load an Italian spaCy model in order of quality.
    Returns (nlp, model_name, error_message) where nlp may be None.
    """
    if spacy is None:
        return None, None, "La libreria spaCy non è installata. Esegui: 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"
        "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()

# ------------------------------
# Small helper converters
# ------------------------------
def spiega_in_italiano(tag, tipo='pos'):
    if tipo == 'pos':
        return SPIEGAZIONI_POS_IT.get(tag, tag)
    if tipo == 'ent':
        return SPIEGAZIONI_ENT_IT.get(tag, tag)
    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 = []
    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.append(f"{chiave_trad}: {valore_trad}")
    return ", ".join(parti_tradotte) or "Non disponibile"

def ottieni_tipo_complemento_con_dettagli(token):
    """
    Given a token that is an 'obl' or similar, inspect 'case' (preposition) children to
    return a more precise complement label (e.g. stato in luogo, di termine, ecc.)
    """
    # find child with dep_ == 'case' (a preposition)
    case_token = next((c for c in token.children if c.dep_ == 'case'), None)
    if not case_token:
        # fallback
        return MAPPA_DEP.get("obl", {"label": "Complemento", "description": "Complemento non specificato."})

    prepo = case_token.text.lower()
    # mapping by start of preposition
    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 / Origine", "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?"),
        "sopra": ("Complemento di Luogo", "Risponde alla domanda: dove?"),
        "sotto": ("Complemento di Luogo", "Risponde alla domanda: dove?"),
    }
    for base, (label, desc) in mappa.items():
        if prepo.startswith(base):
            # special-case: 'da' + passive aux => agente
            if base == "da" and any(c.dep_.endswith('agent') or 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", {"label": "Complemento", "description": "Complemento non specificato."})

def get_full_phrase_for_token(token):
    """
    Build a compact phrase for a head token by collecting determiners, amod, case, compounds, and simple modifiers.
    Returns (text, set(indices)).
    """
    # recursive collection but with small scope to avoid over-collecting
    collected = set()

    def collect(t):
        if t.i in collected:
            return
        collected.add(t.i)
        # Collect children that usually belong inside the noun phrase / token phrase
        for child in t.children:
            if child.dep_ in ('det', 'amod', 'case', 'compound', 'nummod', 'appos', 'fixed', 'flat', 'advmod'):
                collect(child)
    collect(token)

    # also include simple coordinated tokens (conj)
    for child in token.children:
        if child.dep_ == 'conj':
            collect(child)
            # include the coordinating conjunction token if present (cc)
            cc = next((c for c in child.children if c.dep_ == 'cc'), None)
            if cc:
                collected.add(cc.i)

    # sort by token index
    tokens = sorted(collected)
    text = " ".join(token.doc[i].text for i in tokens)
    return text, set(tokens)

def costruisci_sintagmi_con_dettagli(tokens_proposizione):
    """
    Build structured analysis for each "major" token in a clause.
    """
    risultato = []
    # tokens_proposizione assumed to be a list of spaCy tokens (no punctuation/space)
    DEPS_DA_SALTARE = {'det', 'amod', 'case', 'aux', 'aux:pass', 'cop', 'mark', 'cc', 'compound', 'appos', 'punct'}
    indici_elaborati = set()

    for token in tokens_proposizione:
        if token.i in indici_elaborati:
            continue
        # skip tokens that are primarily modifiers (we will include them as part of head tokens)
        if token.dep_ in DEPS_DA_SALTARE and token.head.i != token.i:
            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."})

        token_details = {
            "lemma": getattr(token, "lemma_", token.text),
            "pos": f"{getattr(token, 'pos_', token.pos_)}: {spiega_in_italiano(getattr(token, 'pos_', token.pos_), 'pos')}",
            "tag": getattr(token, "tag_", ""),
            "morph": traduci_morfologia(str(getattr(token, "morph", "")))
        }

        risultato.append({
            "text": testo_sintagma,
            "label_info": info_etichetta,
            "token_details": token_details,
            "token_index": token.i
        })

        indici_elaborati.update(indici_usati)

    # include leftover important tokens like copula or coordinating conjunctions if not already included
    for token in tokens_proposizione:
        if token.i not in indici_elaborati and token.dep_ in ('cop', 'cc'):
            risultato.append({
                "text": token.text,
                "label_info": MAPPA_DEP.get(token.dep_, {"label": token.dep_, "description": ""}),
                "token_details": {
                    "lemma": getattr(token, "lemma_", token.text),
                    "pos": f"{getattr(token, 'pos_', token.pos_)}: {spiega_in_italiano(getattr(token, 'pos_', token.pos_), 'pos')}",
                    "tag": getattr(token, "tag_", ""),
                    "morph": traduci_morfologia(str(getattr(token, "morph", "")))
                },
                "token_index": token.i
            })
            indici_elaborati.add(token.i)

    risultato.sort(key=lambda x: x['token_index'])
    return risultato

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)

# ------------------------------
# Flask app
# ------------------------------
app = Flask(__name__)
CORS(app)

@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
    })

@app.route('/api/analyze', methods=['POST'])
def analizza_frase():
    # Basic checks
    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
        if len(frase) > MAX_SENTENCE_LENGTH:
            return jsonify({"errore": "Frase troppo lunga.", "max_length": MAX_SENTENCE_LENGTH}), 400

        doc = nlp(frase)

        proposizioni_subordinate = []
        indici_subordinate = set()

        # detect subordinate clauses via tokens that have dependency in SUBORD_DEPS
        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)
                })

        # main clause tokens are tokens not part of subordinate clause subtrees
        token_principale = [t for t in doc if t.i not in indici_subordinate and not t.is_punct and not t.is_space]

        # named entities (unique)
        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": spiega_in_italiano(ent.label_, 'ent')
                })

        analisi_finale = {
            "full_sentence": frase,
            "model": IT_MODEL,
            "main_clause": {
                "text": " ".join(t.text for t in token_principale).strip(),
                "analysis": analizza_proposizione_con_dettagli(token_principale)
            },
            "subordinate_clauses": proposizioni_subordinate,
            "named_entities": entita_nominate
        }

        return jsonify(analisi_finale)

    except Exception as e:
        # print to server log for debugging but return safe message
        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))
    # Note: debug=False for production; set to True only during development
    app.run(host="0.0.0.0", port=port, debug=False, threaded=True)