devusman commited on
Commit
7abf422
·
verified ·
1 Parent(s): 699c193

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +226 -212
app.py CHANGED
@@ -4,21 +4,39 @@ from flask_cors import CORS
4
  import spacy
5
  import traceback
6
 
7
- # --- CARICAMENTO MODELLO ---
8
- try:
9
- nlp = spacy.load("it_core_news_lg")
10
- except OSError:
 
 
 
 
 
 
 
 
 
 
 
11
  raise RuntimeError(
12
- "Impossibile trovare il modello 'it_core_news_lg'. "
13
- "Assicurati che sia elencato e installato nel tuo requirements.txt.\n"
14
- "Comando per installarlo localmente: python -m spacy download it_core_news_lg"
 
15
  )
16
 
17
- # --- INIZIALIZZAZIONE APP ---
 
 
 
 
18
  app = Flask(__name__)
19
  CORS(app)
20
 
21
- # --- MAPPE DI TRADUZIONE / SPIEGAZIONI ---
 
 
22
  SPIEGAZIONI_POS_IT = {
23
  "ADJ": "Aggettivo", "ADP": "Preposizione", "ADV": "Avverbio", "AUX": "Ausiliare",
24
  "CONJ": "Congiunzione", "CCONJ": "Congiunzione Coordinante", "SCONJ": "Congiunzione Subordinante",
@@ -31,124 +49,120 @@ SPIEGAZIONI_ENT_IT = {
31
  "PER": "Persona: Nomi di persone reali o fittizie.",
32
  "LOC": "Luogo: Nomi di luoghi geografici come paesi, città, stati.",
33
  "ORG": "Organizzazione: Nomi di aziende, istituzioni, governi.",
34
- "MISC": "Miscellanea: Entità che non rientrano nelle altre categorie (es. eventi, nazionalità, prodotti)."
35
  }
36
 
37
- TRADUZIONI_MORFOLOGIA = {
38
- # chiavi
 
 
39
  "Gender": "Genere", "Number": "Numero", "Mood": "Modo", "Tense": "Tempo",
40
  "Person": "Persona", "VerbForm": "Forma del Verbo", "PronType": "Tipo di Pronome",
41
- "Clitic": "Clitico", "Definite": "Definizione", "Degree": "Grado",
42
  "Case": "Caso", "Poss": "Possessivo", "Reflex": "Riflessivo",
43
- "Aspect": "Aspetto", "Voice": "Voce", "Polarity": "Polarità",
44
- # valori
45
- "Masc": "Maschile", "Fem": "Femminile", "Sing": "Singolare", "Plur": "Plurale",
46
- "Ind": "Indicativo", "Sub": "Congiuntivo", "Cnd": "Condizionale", "Imp": "Imperativo",
 
 
 
 
 
47
  "Pres": "Presente", "Past": "Passato", "Fut": "Futuro", "Pqp": "Trapassato",
48
- "Fin": "Finita", "Inf": "Infinito", "Part": "Participio", "Ger": "Gerundio",
49
  "Prs": "Personale", "Rel": "Relativo", "Int": "Interrogativo", "Dem": "Dimostrativo",
50
- "Art": "Articolativo", "Indf": "Indeterminato", "Yes": "Sì", "No": "No",
 
51
  "Abs": "Assoluto", "Cmp": "Comparativo", "Sup": "Superlativo",
52
  "Nom": "Nominativo", "Acc": "Accusativo", "Gen": "Genitivo", "Dat": "Dativo",
53
  "Perf": "Perfetto", "Prog": "Progressivo",
54
  "Act": "Attiva", "Pass": "Passiva",
55
  }
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  MAPPA_DEP = {
58
  "nsubj": {"label": "Soggetto", "description": "Indica chi o cosa compie l'azione o si trova in un certo stato."},
59
  "nsubj:pass": {"label": "Soggetto (Passivo)", "description": "Soggetto in una costruzione passiva."},
60
  "ROOT": {"label": "Predicato Verbale", "description": "Esprime l'azione o lo stato del soggetto."},
61
  "obj": {"label": "Complemento Oggetto", "description": "Indica l'oggetto diretto dell'azione del verbo."},
62
  "iobj": {"label": "Complemento di Termine", "description": "Indica a chi o a cosa è destinata l'azione."},
63
- "obl": {"label": "Complemento Indiretto", "description": "Fornisce informazioni aggiuntive come luogo, tempo, modo, causa, etc."},
64
- "nmod": {"label": "Complemento di Specificazione", "description": "Specifica o chiarisce il significato del nome a cui si riferisce."},
 
65
  "amod": {"label": "Attributo", "description": "Aggettivo che qualifica un nome."},
66
- "advmod": {"label": "Complemento Avverbiale", "description": "Modifica il significato di un verbo, aggettivo o altro avverbio."},
67
  "appos": {"label": "Apposizione", "description": "Nome che ne chiarisce un altro."},
68
- "acl:relcl": {"label": "Proposizione Subordinata Relativa", "description": "Frase che espande un nome, introdotta da un pronome relativo."},
69
- "advcl": {"label": "Proposizione Subordinata Avverbiale", "description": "Frase che funziona come un avverbio, modificando il verbo della principale."},
70
- "ccomp": {"label": "Proposizione Subordinata Oggettiva", "description": "Frase che funge da complemento oggetto del verbo della principale."},
71
- "csubj": {"label": "Proposizione Subordinata Soggettiva", "description": "Frase che funge da soggetto del verbo della principale."},
72
- "xcomp": {"label": "Complemento Predicativo", "description": "Complemento che completa il significato del verbo."},
73
  "acl": {"label": "Modificatore Relativo", "description": "Clausola che modifica un nome."},
74
  "compound": {"label": "Composto", "description": "Parte di un composto nominale."},
75
- "flat": {"label": "Nome Piatto", "description": "Parte di un nome proprio o espressione fissa."},
76
  "conj": {"label": "Congiunzione Coordinata", "description": "Elemento coordinato con un altro."},
77
- "cc": {"label": "Congiunzione Coordinante", "description": "Congiunzione che collega elementi coordinati."},
78
- "parataxis": {"label": "Paratassi", "description": "Frasi coordinate senza connettore esplicito."}
79
  }
80
 
81
- # --- FUNZIONI DI UTILITÀ ---
82
-
83
- def spiega_in_italiano(tag, tipo='pos'):
84
- """
85
- Fornisce spiegazione per POS o entità.
86
- - tipo == 'pos' => tag dovrebbe essere token.pos_ (es. VERB, NOUN)
87
- - tipo == 'ent' => ent label (PER, LOC, ORG...)
88
- """
89
- if tipo == 'pos':
90
- key = tag.upper().split(":")[0] # normalizza caso e rimuove eventuali dettagli
91
- return SPIEGAZIONI_POS_IT.get(key, key)
92
- if tipo == 'ent':
93
- return SPIEGAZIONI_ENT_IT.get(tag, tag)
94
- return tag
95
 
96
- def traduci_morfologia_from_token(token):
97
- """
98
- Usa token.morph (meglio token.morph.to_dict()) per costruire una descrizione leggibile.
99
- """
100
- try:
101
- morph_dict = token.morph.to_dict()
102
- except Exception:
103
- # Fallback alla stringa originale
104
- morph_str = str(token.morph)
105
- if not morph_str:
106
- return "Non disponibile"
107
- parti = morph_str.split("|")
108
- mappate = []
109
- for parte in parti:
110
- if "=" in parte:
111
- k, v = parte.split("=", 1)
112
- mappate.append(f"{TRADUZIONI_MORFOLOGIA.get(k,k)}: {TRADUZIONI_MORFOLOGIA.get(v, v)}")
113
- else:
114
- mappate.append(TRADUZIONI_MORFOLOGIA.get(parte, parte))
115
- return ", ".join(mappate) if mappate else "Non disponibile"
116
-
117
- parti_tradotte = []
118
- for k, v in sorted(morph_dict.items()):
119
- k_tr = TRADUZIONI_MORFOLOGIA.get(k, k)
120
- if isinstance(v, (list, tuple)):
121
- v_str = ", ".join(TRADUZIONI_MORFOLOGIA.get(x, x) for x in v)
122
- else:
123
- v_str = TRADUZIONI_MORFOLOGIA.get(v, v)
124
- parti_tradotte.append(f"{k_tr}: {v_str}")
125
- return ", ".join(parti_tradotte) if parti_tradotte else "Non disponibile"
126
-
127
- def get_verb_phrase(token):
128
- """
129
- Costruisce la 'verb phrase' completa: aggiunge ausiliari, negazioni, particelle legate.
130
- Ordina i token per indice per mantenere la sequenza corretta.
131
- """
132
- verb_related = []
133
- # includi token stesso se è verbo o copula
134
- if token.pos_ in ("VERB", "AUX") or token.dep_ in ("cop", "ROOT"):
135
- verb_related.append(token)
136
- # cerca ausiliari, negazioni e copula fra i figli e nella testa (se la testa è verbo)
137
- for t in list(token.children):
138
- if t.dep_ in ('aux', 'aux:pass', 'neg', 'cop', 'prt'):
139
- verb_related.append(t)
140
- # talvolta l'ausiliare è head (in casi particolari); includi ausiliari nella testa che hanno head==token or viceversa
141
- # includi anche eventuali elementi nella subtree stretta che sono parte del verbo
142
- for t in token.subtree:
143
- if t.dep_ in ('aux', 'aux:pass', 'neg', 'cop', 'prt') and t not in verb_related:
144
- verb_related.append(t)
145
-
146
- # rimuovi duplicati e ordina
147
- verb_related = sorted(set(verb_related), key=lambda x: x.i)
148
- return " ".join(t.text for t in verb_related).strip() or token.text
149
 
150
  def ottieni_tipo_complemento_con_dettagli(token):
151
  preposizione = ""
 
152
  for figlio in token.children:
153
  if figlio.dep_ == "case":
154
  preposizione = figlio.text.lower()
@@ -160,63 +174,81 @@ def ottieni_tipo_complemento_con_dettagli(token):
160
  break
161
 
162
  mappa_preposizioni = {
163
- "di": "Complemento di Specificazione",
164
- "del": "Complemento di Specificazione", "dello": "Complemento di Specificazione",
165
- "della": "Complemento di Specificazione", "dei": "Complemento di Specificazione",
166
- "a": "Complemento di Termine", "al": "Complemento di Termine",
167
- "da": "Complemento di Moto da Luogo", "dal": "Complemento di Moto da Luogo",
168
- "in": "Complemento di Stato in Luogo", "nel": "Complemento di Stato in Luogo",
169
- "con": "Complemento di Compagnia o Mezzo", "su": "Complemento di Argomento o Luogo",
170
- "per": "Complemento di Fine o Causa", "tra": "Complemento di Luogo o Tempo (Partitivo)",
171
- "fra": "Complemento di Luogo o Tempo (Partitivo)",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
 
174
  label = mappa_preposizioni.get(preposizione, "Complemento Indiretto")
175
- 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."
 
 
 
176
 
177
- if preposizione.startswith("da") and any(figlio.dep_ in ('aux:pass','aux') for figlio in token.head.children):
 
178
  label = "Complemento d'Agente"
179
  description = "Indica da chi è compiuta l'azione in una frase passiva."
180
 
181
  return {"label": label, "description": description}
182
 
 
183
  def ottieni_testo_completo(token):
184
- """
185
- Raccoglie il sintagma completo includendo determinanti, aggettivi, apposizioni, complementi stretti.
186
- """
187
  def raccogli_figli(t):
188
- figli = []
189
- for f in t.children:
190
- if f.dep_ in ('det', 'amod', 'case', 'advmod', 'nmod', 'appos', 'acl', 'compound', 'flat', 'nummod'):
191
- figli.append(f)
192
  figli.extend(raccogli_figli(f))
193
  return figli
194
 
195
  token_sintagma = [token] + raccogli_figli(token)
196
- token_sintagma = sorted(set(token_sintagma), key=lambda x: x.i)
197
- return " ".join(t.text for t in token_sintagma if not t.is_punct).strip()
 
 
 
 
 
 
 
198
 
199
  def costruisci_sintagmi_con_dettagli(lista_token):
200
- """
201
- Costruisce la lista di sintagmi (soggetto, predicato, complementi) con dettagli per ogni token significativo.
202
- Non scartiamo ausiliari e copula: li rappresentiamo opportunamente.
203
- """
204
  mappa_sintagmi = {}
205
 
206
- # selezioniamo token utili (scartiamo solo punteggiatura e spazi)
 
 
207
  for token in lista_token:
208
- if token.is_punct or token.is_space:
209
  continue
210
  mappa_sintagmi[token.i] = {
211
  "text": ottieni_testo_completo(token),
212
  "token_details": {
213
- "text": token.text,
214
  "lemma": token.lemma_,
215
- "pos": token.pos_,
216
- "pos_explanation": spiega_in_italiano(token.pos_, 'pos'),
217
- "tag": token.tag_,
218
- "morph_raw": str(token.morph),
219
- "morph": traduci_morfologia_from_token(token)
220
  },
221
  "label_info": {},
222
  "token": token
@@ -228,75 +260,50 @@ def costruisci_sintagmi_con_dettagli(lista_token):
228
  for indice, sintagma in sorted(mappa_sintagmi.items()):
229
  if indice in indici_elaborati:
230
  continue
231
-
232
  token = sintagma['token']
233
  dep = token.dep_
234
  info_etichetta = MAPPA_DEP.get(dep, {"label": dep, "description": "Relazione non mappata."})
235
 
236
- # gestione ROOT e predicato nominale con copula
237
  if dep == "ROOT":
238
- # trova eventuali copule collegate (cop o aux:cop)
239
- copula_children = [c for c in token.children if c.dep_ in ('cop', 'aux:cop')]
240
- if copula_children:
241
- cop = copula_children[0]
242
- # copula come verbo
243
  risultato_analisi.append({
244
  "text": cop.text,
245
- "label_info": {"label": "Copula", "description": "Verbo 'essere' che collega soggetto e parte nominale."},
246
  "token_details": {
247
  "lemma": cop.lemma_,
248
- "pos": cop.pos_,
249
- "pos_explanation": spiega_in_italiano(cop.pos_, 'pos'),
250
- "tag": cop.tag_,
251
- "morph": traduci_morfologia_from_token(cop),
252
- "verb_phrase": get_verb_phrase(cop)
253
  }
254
  })
255
- # parte nominale (il nome/aggettivo che segue)
256
  risultato_analisi.append({
257
- "text": sintagma["text"],
258
- "label_info": {"label": "Parte Nominale del Predicato", "description": "Parte nominale che descrive il soggetto."},
259
  "token_details": sintagma["token_details"]
260
  })
261
  indici_elaborati.add(indice)
262
  continue
263
- else:
264
- # ROOT come verbo principale: mostriamo verb phrase completa
265
- sintagma_da_aggiungere = {
266
- "text": sintagma['text'],
267
- "label_info": info_etichetta,
268
- "token_details": dict(sintagma['token_details'])
269
- }
270
- # se è verbo, aggiungi la verb_phrase
271
- if token.pos_ in ("VERB", "AUX"):
272
- sintagma_da_aggiungere["token_details"]["verb_phrase"] = get_verb_phrase(token)
273
- sintagma_da_aggiungere["token_details"]["verb_morph"] = traduci_morfologia_from_token(token)
274
- risultato_analisi.append(sintagma_da_aggiungere)
275
- indici_elaborati.add(indice)
276
- continue
277
-
278
- # gestione complementi obl e agent
279
- if dep in ('obl', 'obl:agent', 'obl:mod'):
280
  info_etichetta = ottieni_tipo_complemento_con_dettagli(token)
281
-
282
- if dep == 'nsubj:pass':
283
  info_etichetta = MAPPA_DEP.get('nsubj:pass', MAPPA_DEP['nsubj'])
284
 
 
285
  sintagma_da_aggiungere = {
286
  "text": sintagma['text'],
287
- "label_info": info_etichetta
 
288
  }
289
- if sintagma.get("token_details"):
290
- # se è un verbo o ausiliare aggiungiamo la verb_phrase
291
- if token.pos_ in ("VERB", "AUX"):
292
- sintagma['token_details']["verb_phrase"] = get_verb_phrase(token)
293
- sintagma['token_details']["verb_morph"] = traduci_morfologia_from_token(token)
294
- sintagma_da_aggiungere["token_details"] = sintagma["token_details"]
295
-
296
  risultato_analisi.append(sintagma_da_aggiungere)
297
  indici_elaborati.add(indice)
298
 
299
- # rimuovi duplicati (testo)
300
  risultato_unico = []
301
  testi_visti = set()
302
  for item in risultato_analisi:
@@ -305,73 +312,79 @@ def costruisci_sintagmi_con_dettagli(lista_token):
305
  testi_visti.add(item['text'])
306
  return risultato_unico
307
 
 
308
  def analizza_proposizione_con_dettagli(token_proposizione):
309
- token_nella_proposizione = [t for t in token_proposizione if not t.is_punct and not t.is_space]
310
  return costruisci_sintagmi_con_dettagli(token_nella_proposizione)
311
 
312
- # --- ENDPOINT ---
313
-
 
314
  @app.route("/")
315
  def home():
316
- return jsonify({"messaggio": "L'API per l'analisi logica è in esecuzione. Usa l'endpoint /api/analyze."})
 
 
 
 
 
317
 
318
  @app.route('/api/analyze', methods=['POST'])
319
  def analizza_frase():
320
  try:
321
- dati = request.get_json()
322
- if not dati or 'sentence' not in dati:
323
- return jsonify({"errore": "Frase non fornita"}), 400
324
-
325
- frase = dati['sentence'].strip()
326
  if not frase:
327
- return jsonify({"errore": "Frase vuota"}), 400
328
 
329
  doc = nlp(frase)
330
 
 
331
  proposizioni_subordinate = []
332
  indici_subordinate = set()
 
333
 
334
  for token in doc:
335
- if token.dep_ in ["acl:relcl", "advcl", "ccomp", "csubj", "xcomp", "acl", "parataxis", "parataxis:rel"]:
336
- token_proposizione_subordinata = list(token.subtree)
337
- for t in token_proposizione_subordinata:
338
  indici_subordinate.add(t.i)
339
 
340
- info_tipo_subordinata = MAPPA_DEP.get(token.dep_, {"label": "Proposizione Subordinata", "description": "Una frase che dipende da un'altra."})
341
- marcatore = [figlio for figlio in token.children if figlio.dep_ == 'mark']
342
- intro = marcatore[0].text if marcatore else ""
343
 
344
  proposizioni_subordinate.append({
345
- "type_info": info_tipo_subordinata,
346
- "text": " ".join(t.text for t in token_proposizione_subordinata if not t.is_punct and not t.is_space).strip(),
347
  "intro": intro,
348
- "analysis": analizza_proposizione_con_dettagli(token_proposizione_subordinata)
349
  })
350
 
351
- token_proposizione_principale = [token for token in doc if token.i not in indici_subordinate]
352
-
353
- entita_nominate = [{
354
- "text": ent.text,
355
- "label": ent.label_,
356
- "explanation": spiega_in_italiano(ent.label_, 'ent')
357
- } for ent in doc.ents]
358
-
359
- # Rimuovi duplicati dalle entità
360
- entita_unica = []
361
- testi_ent_visti = set()
362
- for ent in entita_nominate:
363
- if ent['text'] not in testi_ent_visti:
364
- entita_unica.append(ent)
365
- testi_ent_visti.add(ent['text'])
366
 
367
  analisi_finale = {
368
  "full_sentence": frase,
 
369
  "main_clause": {
370
- "text": " ".join(t.text for t in token_proposizione_principale if not t.is_punct and not t.is_space).strip(),
371
- "analysis": analizza_proposizione_con_dettagli(token_proposizione_principale)
372
  },
373
  "subordinate_clauses": proposizioni_subordinate,
374
- "named_entities": entita_unica
375
  }
376
 
377
  return jsonify(analisi_finale)
@@ -380,6 +393,7 @@ def analizza_frase():
380
  traceback.print_exc()
381
  return jsonify({"errore": "Si è verificato un errore interno.", "dettagli": str(e)}), 500
382
 
 
383
  if __name__ == '__main__':
384
- porta = int(os.environ.get("PORT", 8080))
385
- app.run(host="0.0.0.0", port=porta, debug=False, threaded=True)
 
4
  import spacy
5
  import traceback
6
 
7
+ # ------------------------------
8
+ # Caricamento modello spaCy (con fallback)
9
+ # ------------------------------
10
+
11
+ def load_it_model():
12
+ """Prova a caricare un modello italiano in ordine di qualità.
13
+ Suggerisce il comando di installazione se non disponibile.
14
+ """
15
+ candidates = ["it_core_news_lg", "it_core_news_md", "it_core_news_sm"]
16
+ last_err = None
17
+ for name in candidates:
18
+ try:
19
+ return spacy.load(name), name
20
+ except Exception as e:
21
+ last_err = e
22
  raise RuntimeError(
23
+ "Impossibile caricare un modello italiano spaCy. "
24
+ "Installa almeno uno tra: it_core_news_lg / it_core_news_md / it_core_news_sm.\n"
25
+ "Esempio: python -m spacy download it_core_news_lg\n"
26
+ f"Dettagli ultimo errore: {last_err}"
27
  )
28
 
29
+ nlp, IT_MODEL = load_it_model()
30
+
31
+ # ------------------------------
32
+ # Flask App
33
+ # ------------------------------
34
  app = Flask(__name__)
35
  CORS(app)
36
 
37
+ # ------------------------------
38
+ # Tabelle di spiegazione POS / NER
39
+ # ------------------------------
40
  SPIEGAZIONI_POS_IT = {
41
  "ADJ": "Aggettivo", "ADP": "Preposizione", "ADV": "Avverbio", "AUX": "Ausiliare",
42
  "CONJ": "Congiunzione", "CCONJ": "Congiunzione Coordinante", "SCONJ": "Congiunzione Subordinante",
 
49
  "PER": "Persona: Nomi di persone reali o fittizie.",
50
  "LOC": "Luogo: Nomi di luoghi geografici come paesi, città, stati.",
51
  "ORG": "Organizzazione: Nomi di aziende, istituzioni, governi.",
52
+ "MISC": "Miscellanea: Entità che non rientrano nelle altre categorie (eventi, nazionalità, prodotti)."
53
  }
54
 
55
+ # ------------------------------
56
+ # Traduzioni Morfologia (UD) con mappatura consapevole chiave/valore
57
+ # ------------------------------
58
+ KEY_MAP = {
59
  "Gender": "Genere", "Number": "Numero", "Mood": "Modo", "Tense": "Tempo",
60
  "Person": "Persona", "VerbForm": "Forma del Verbo", "PronType": "Tipo di Pronome",
61
+ "Clitic": "Clitico", "Definite": "Definitezza", "Degree": "Grado",
62
  "Case": "Caso", "Poss": "Possessivo", "Reflex": "Riflessivo",
63
+ "Aspect": "Aspetto", "Voice": "Voce",
64
+ }
65
+
66
+ # Valori generici (non ambigui)
67
+ VALUE_MAP = {
68
+ "Masc": "Maschile", "Fem": "Femminile",
69
+ "Sing": "Singolare", "Plur": "Plurale",
70
+ "Cnd": "Condizionale", "Sub": "Congiuntivo", "Ind": "Indicativo", "Imp": "Imperfetto",
71
+ "Inf": "Infinito", "Part": "Participio", "Ger": "Gerundio", "Fin": "Finita",
72
  "Pres": "Presente", "Past": "Passato", "Fut": "Futuro", "Pqp": "Trapassato",
73
+ "1": "", "2": "", "3": "",
74
  "Prs": "Personale", "Rel": "Relativo", "Int": "Interrogativo", "Dem": "Dimostrativo",
75
+ "Art": "Articolativo", "Yes": "Sì", "No": "No",
76
+ "Def": "Determinato", "Indef": "Indefinito",
77
  "Abs": "Assoluto", "Cmp": "Comparativo", "Sup": "Superlativo",
78
  "Nom": "Nominativo", "Acc": "Accusativo", "Gen": "Genitivo", "Dat": "Dativo",
79
  "Perf": "Perfetto", "Prog": "Progressivo",
80
  "Act": "Attiva", "Pass": "Passiva",
81
  }
82
 
83
+ # Coppie chiave/valore per risolvere ambiguità (es. Imp = Imperativo vs Imperfetto; Ind = Indicativo vs Indeterminato)
84
+ PAIR_VALUE_MAP = {
85
+ ("Mood", "Imp"): "Imperativo",
86
+ ("Tense", "Imp"): "Imperfetto",
87
+ ("Mood", "Ind"): "Indicativo",
88
+ ("Definite", "Ind"): "Indeterminato",
89
+ }
90
+
91
+
92
+ def spiega_in_italiano(tag, tipo='pos'):
93
+ """Spiega un tag POS o NER in italiano."""
94
+ if tipo == 'pos':
95
+ pos_base = tag.split("__")[0] if "__" in tag else tag
96
+ return SPIEGAZIONI_POS_IT.get(pos_base, tag)
97
+ if tipo == 'ent':
98
+ return SPIEGAZIONI_ENT_IT.get(tag, tag)
99
+ return tag
100
+
101
+
102
+ def traduci_morfologia(morph_str: str) -> str:
103
+ """Traduce la stringa morfologica UD in italiano con gestione delle ambiguità."""
104
+ if not morph_str or morph_str == "___":
105
+ return "Non disponibile"
106
+
107
+ parti = morph_str.split('|')
108
+ parti_tradotte = []
109
+ for parte in parti:
110
+ if '=' in parte:
111
+ chiave, valore = parte.split('=', 1)
112
+ chiave = chiave.strip()
113
+ valore = valore.strip()
114
+ chiave_trad = KEY_MAP.get(chiave, chiave)
115
+ # priorità a mappa di coppia chiave/valore, altrimenti valore generico
116
+ valore_trad = PAIR_VALUE_MAP.get((chiave, valore), VALUE_MAP.get(valore, valore))
117
+ parti_tradotte.append(f"{chiave_trad}: {valore_trad}")
118
+ else:
119
+ # valore singolo
120
+ parti_tradotte.append(VALUE_MAP.get(parte.strip(), parte.strip()))
121
+
122
+ # non usare set() per non perdere ordine; rimuovi duplicati preservando l'ordine
123
+ visti = set()
124
+ puliti = []
125
+ for p in parti_tradotte:
126
+ if p not in visti:
127
+ puliti.append(p)
128
+ visti.add(p)
129
+ return ", ".join(puliti) if puliti else "Non disponibile"
130
+
131
+ # ------------------------------
132
+ # Mappature Dependency → Etichette italiane
133
+ # ------------------------------
134
  MAPPA_DEP = {
135
  "nsubj": {"label": "Soggetto", "description": "Indica chi o cosa compie l'azione o si trova in un certo stato."},
136
  "nsubj:pass": {"label": "Soggetto (Passivo)", "description": "Soggetto in una costruzione passiva."},
137
  "ROOT": {"label": "Predicato Verbale", "description": "Esprime l'azione o lo stato del soggetto."},
138
  "obj": {"label": "Complemento Oggetto", "description": "Indica l'oggetto diretto dell'azione del verbo."},
139
  "iobj": {"label": "Complemento di Termine", "description": "Indica a chi o a cosa è destinata l'azione."},
140
+ "obl": {"label": "Complemento Indiretto", "description": "Informazioni aggiuntive (luogo, tempo, modo, causa, ecc.)."},
141
+ "obl:agent": {"label": "Complemento d'Agente", "description": "Chi compie l'azione in una frase passiva."},
142
+ "nmod": {"label": "Complemento di Specificazione", "description": "Specificazione legata a un nome."},
143
  "amod": {"label": "Attributo", "description": "Aggettivo che qualifica un nome."},
144
+ "advmod": {"label": "Complemento Avverbiale", "description": "Modifica verbo/aggettivo/avverbio."},
145
  "appos": {"label": "Apposizione", "description": "Nome che ne chiarisce un altro."},
146
+ "acl:relcl": {"label": "Proposizione Subordinata Relativa", "description": "Frase introdotta da pronome relativo."},
147
+ "advcl": {"label": "Proposizione Subordinata Avverbiale", "description": "Frase che modifica il verbo della principale."},
148
+ "ccomp": {"label": "Proposizione Subordinata Oggettiva", "description": "Frase che funge da complemento oggetto."},
149
+ "csubj": {"label": "Proposizione Subordinata Soggettiva", "description": "Frase che funge da soggetto."},
150
+ "xcomp": {"label": "Complemento Predicativo", "description": "Completa il significato del verbo."},
151
  "acl": {"label": "Modificatore Relativo", "description": "Clausola che modifica un nome."},
152
  "compound": {"label": "Composto", "description": "Parte di un composto nominale."},
153
+ "flat": {"label": "Nome Piatto", "description": "Parte di un nome proprio/espressione fissa."},
154
  "conj": {"label": "Congiunzione Coordinata", "description": "Elemento coordinato con un altro."},
155
+ "cc": {"label": "Congiunzione Coordinante", "description": "Congiunzione che collega elementi coordinati."}
 
156
  }
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ # ------------------------------
160
+ # Utilità sintagmi / complementi
161
+ # ------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  def ottieni_tipo_complemento_con_dettagli(token):
164
  preposizione = ""
165
+ # cerca preposizione nel figlio "case"
166
  for figlio in token.children:
167
  if figlio.dep_ == "case":
168
  preposizione = figlio.text.lower()
 
174
  break
175
 
176
  mappa_preposizioni = {
177
+ # di specificazione
178
+ "di": "Complemento di Specificazione", "del": "Complemento di Specificazione", "dello": "Complemento di Specificazione",
179
+ "della": "Complemento di Specificazione", "dei": "Complemento di Specificazione", "degli": "Complemento di Specificazione",
180
+ "delle": "Complemento di Specificazione",
181
+ # a termine
182
+ "a": "Complemento di Termine", "al": "Complemento di Termine", "allo": "Complemento di Termine",
183
+ "alla": "Complemento di Termine", "ai": "Complemento di Termine", "agli": "Complemento di Termine", "alle": "Complemento di Termine",
184
+ # da moto da luogo
185
+ "da": "Complemento di Moto da Luogo", "dal": "Complemento di Moto da Luogo", "dallo": "Complemento di Moto da Luogo",
186
+ "dalla": "Complemento di Moto da Luogo", "dai": "Complemento di Moto da Luogo", "dagli": "Complemento di Moto da Luogo",
187
+ "dalle": "Complemento di Moto da Luogo",
188
+ # in → stato in luogo
189
+ "in": "Complemento di Stato in Luogo", "nel": "Complemento di Stato in Luogo", "nello": "Complemento di Stato in Luogo",
190
+ "nella": "Complemento di Stato in Luogo", "nei": "Complemento di Stato in Luogo", "negli": "Complemento di Stato in Luogo", "nelle": "Complemento di Stato in Luogo",
191
+ # con → compagnia/mezzo
192
+ "con": "Complemento di Compagnia o Mezzo", "col": "Complemento di Compagnia o Mezzo", "coi": "Complemento di Compagnia o Mezzo",
193
+ # su → argomento/luogo
194
+ "su": "Complemento di Argomento o Luogo", "sul": "Complemento di Argomento o Luogo", "sullo": "Complemento di Argomento o Luogo",
195
+ "sulla": "Complemento di Argomento o Luogo", "sui": "Complemento di Argomento o Luogo", "sugli": "Complemento di Argomento o Luogo", "sulle": "Complemento di Argomento o Luogo",
196
+ # per → fine/causa
197
+ "per": "Complemento di Fine o Causa",
198
+ # tra/fra → luogo/tempo
199
+ "tra": "Complemento di Luogo o Tempo (Partitivo)", "fra": "Complemento di Luogo o Tempo (Partitivo)",
200
  }
201
 
202
  label = mappa_preposizioni.get(preposizione, "Complemento Indiretto")
203
+ description = (
204
+ "Fornisce un'informazione generica non classificata in modo più specifico." if label == "Complemento Indiretto"
205
+ else "Risponde alla domanda appropriata per il tipo di complemento."
206
+ )
207
 
208
+ # agente nelle passive (da + ausiliare passivo nelle vicinanze)
209
+ if preposizione.startswith("da") and any(figlio.dep_ == 'aux:pass' for figlio in token.head.children):
210
  label = "Complemento d'Agente"
211
  description = "Indica da chi è compiuta l'azione in una frase passiva."
212
 
213
  return {"label": label, "description": description}
214
 
215
+
216
  def ottieni_testo_completo(token):
217
+ """Raccoglie ricorsivamente il sintagma testuale del token (senza punteggiatura)."""
 
 
218
  def raccogli_figli(t):
219
+ figli = list(t.children)
220
+ for f in list(figli):
221
+ if f.dep_ in ('det', 'amod', 'case', 'advmod', 'nmod', 'appos', 'acl', 'compound', 'flat'): # includi modificatori utili
 
222
  figli.extend(raccogli_figli(f))
223
  return figli
224
 
225
  token_sintagma = [token] + raccogli_figli(token)
226
+ # ordina per indice e rimuovi duplicati mantenendo l'ordine
227
+ visti = set()
228
+ ordinati = []
229
+ for t in sorted(token_sintagma, key=lambda x: x.i):
230
+ if t not in visti and not t.is_punct:
231
+ ordinati.append(t)
232
+ visti.add(t)
233
+ return " ".join(t.text for t in ordinati).strip()
234
+
235
 
236
  def costruisci_sintagmi_con_dettagli(lista_token):
 
 
 
 
237
  mappa_sintagmi = {}
238
 
239
+ # Non scartare "advmod" (servono gli avverbi). Scarta elementi funzionali che non portano sintagmi autonomi.
240
+ SKIP = {'det', 'case', 'punct', 'aux', 'cop', 'mark', 'cc', 'aux:pass'}
241
+
242
  for token in lista_token:
243
+ if token.dep_ in SKIP:
244
  continue
245
  mappa_sintagmi[token.i] = {
246
  "text": ottieni_testo_completo(token),
247
  "token_details": {
 
248
  "lemma": token.lemma_,
249
+ "pos": f"{token.pos_}: {spiega_in_italiano(token.pos_, 'pos')}",
250
+ "tag": f"{token.tag_}: {spiega_in_italiano(token.tag_, 'pos')}",
251
+ "morph": traduci_morfologia(str(token.morph))
 
 
252
  },
253
  "label_info": {},
254
  "token": token
 
260
  for indice, sintagma in sorted(mappa_sintagmi.items()):
261
  if indice in indici_elaborati:
262
  continue
 
263
  token = sintagma['token']
264
  dep = token.dep_
265
  info_etichetta = MAPPA_DEP.get(dep, {"label": dep, "description": "Relazione non mappata."})
266
 
 
267
  if dep == "ROOT":
268
+ # Predicato verbale o nominale
269
+ copule = [c for c in token.children if c.dep_ == 'cop']
270
+ if copule:
271
+ cop = copule[0]
272
+ # aggiungi copula
273
  risultato_analisi.append({
274
  "text": cop.text,
275
+ "label_info": {"label": "Copula", "description": "Verbo 'essere' che collega il soggetto alla parte nominale."},
276
  "token_details": {
277
  "lemma": cop.lemma_,
278
+ "pos": f"{cop.pos_}: {spiega_in_italiano(cop.pos_, 'pos')}",
279
+ "tag": f"{cop.tag_}: {spiega_in_italiano(cop.tag_, 'pos')}",
280
+ "morph": traduci_morfologia(str(cop.morph))
 
 
281
  }
282
  })
283
+ # parte nominale del predicato
284
  risultato_analisi.append({
285
+ "text": ottieni_testo_completo(token),
286
+ "label_info": {"label": "Parte Nominale del Predicato", "description": "Aggettivo o nome che descrive il soggetto."},
287
  "token_details": sintagma["token_details"]
288
  })
289
  indici_elaborati.add(indice)
290
  continue
291
+ # altrimenti predicato verbale standard → già etichettato come ROOT
292
+ elif dep in ('obl', 'obl:agent'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  info_etichetta = ottieni_tipo_complemento_con_dettagli(token)
294
+ elif dep == 'nsubj:pass':
 
295
  info_etichetta = MAPPA_DEP.get('nsubj:pass', MAPPA_DEP['nsubj'])
296
 
297
+ # aggiungi il sintagma
298
  sintagma_da_aggiungere = {
299
  "text": sintagma['text'],
300
+ "label_info": info_etichetta,
301
+ "token_details": sintagma["token_details"]
302
  }
 
 
 
 
 
 
 
303
  risultato_analisi.append(sintagma_da_aggiungere)
304
  indici_elaborati.add(indice)
305
 
306
+ # Rimuovi duplicati per testo preservando l'ordine
307
  risultato_unico = []
308
  testi_visti = set()
309
  for item in risultato_analisi:
 
312
  testi_visti.add(item['text'])
313
  return risultato_unico
314
 
315
+
316
  def analizza_proposizione_con_dettagli(token_proposizione):
317
+ token_nella_proposizione = [t for t in token_proposizione if t.dep_ != 'mark' and not t.is_punct and not t.is_space]
318
  return costruisci_sintagmi_con_dettagli(token_nella_proposizione)
319
 
320
+ # ------------------------------
321
+ # Routes
322
+ # ------------------------------
323
  @app.route("/")
324
  def home():
325
+ return jsonify({
326
+ "messaggio": "API analisi logica in esecuzione",
327
+ "modello_spacy": IT_MODEL,
328
+ "endpoint": "/api/analyze"
329
+ })
330
+
331
 
332
  @app.route('/api/analyze', methods=['POST'])
333
  def analizza_frase():
334
  try:
335
+ dati = request.get_json(silent=True) or {}
336
+ frase = (dati.get('sentence') or "").strip()
 
 
 
337
  if not frase:
338
+ return jsonify({"errore": "Frase non fornita o vuota."}), 400
339
 
340
  doc = nlp(frase)
341
 
342
+ # individua subordinate/coordinate rilevanti
343
  proposizioni_subordinate = []
344
  indici_subordinate = set()
345
+ SUBORD_DEPS = {"acl:relcl", "advcl", "ccomp", "csubj", "xcomp", "acl", "parataxis"}
346
 
347
  for token in doc:
348
+ if token.dep_ in SUBORD_DEPS:
349
+ subtree = list(token.subtree)
350
+ for t in subtree:
351
  indici_subordinate.add(t.i)
352
 
353
+ info_tipo = MAPPA_DEP.get(token.dep_, {"label": "Proposizione Subordinata", "description": "Frase che dipende da un'altra."})
354
+ mark_token = next((f for f in token.children if f.dep_ == 'mark'), None)
355
+ intro = mark_token.text if mark_token else ""
356
 
357
  proposizioni_subordinate.append({
358
+ "type_info": info_tipo,
359
+ "text": " ".join(t.text for t in subtree if not t.is_punct and not t.is_space).strip(),
360
  "intro": intro,
361
+ "analysis": analizza_proposizione_con_dettagli(subtree)
362
  })
363
 
364
+ # principale = token non inclusi nelle subordinate
365
+ token_principale = [t for t in doc if t.i not in indici_subordinate]
366
+
367
+ # entità nominate (senza duplicati)
368
+ entita_nominate = []
369
+ visti = set()
370
+ for ent in doc.ents:
371
+ if ent.text not in visti:
372
+ entita_nominate.append({
373
+ "text": ent.text,
374
+ "label": ent.label_,
375
+ "explanation": spiega_in_italiano(ent.label_, 'ent')
376
+ })
377
+ visti.add(ent.text)
 
378
 
379
  analisi_finale = {
380
  "full_sentence": frase,
381
+ "model": IT_MODEL,
382
  "main_clause": {
383
+ "text": " ".join(t.text for t in token_principale if not t.is_punct and not t.is_space).strip(),
384
+ "analysis": analizza_proposizione_con_dettagli(token_principale)
385
  },
386
  "subordinate_clauses": proposizioni_subordinate,
387
+ "named_entities": entita_nominate
388
  }
389
 
390
  return jsonify(analisi_finale)
 
393
  traceback.print_exc()
394
  return jsonify({"errore": "Si è verificato un errore interno.", "dettagli": str(e)}), 500
395
 
396
+
397
  if __name__ == '__main__':
398
+ port = int(os.environ.get("PORT", 8080))
399
+ app.run(host="0.0.0.0", port=port, debug=False, threaded=True)