devusman commited on
Commit
05d3a8d
·
verified ·
1 Parent(s): 7abf422

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +204 -250
app.py CHANGED
@@ -1,32 +1,46 @@
1
  import os
 
2
  from flask import Flask, request, jsonify
3
  from flask_cors import CORS
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
@@ -53,7 +67,7 @@ SPIEGAZIONI_ENT_IT = {
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",
@@ -63,274 +77,222 @@ KEY_MAP = {
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": "1ª", "2": "2ª", "3": "3ª",
74
- "Prs": "Personale", "Rel": "Relativo", "Int": "Interrogativo", "Dem": "Dimostrativo",
75
- "Art": "Articolativo", "Yes": "", "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()
169
- break
170
- if not preposizione and token.dep_ == 'obl':
171
- for figlio in token.head.children:
172
- if figlio.dep_ == "case" and figlio.head == token:
173
- 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
255
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  risultato_analisi = []
258
  indici_elaborati = set()
 
 
 
259
 
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:
310
- if item['text'] not in testi_visti:
311
- risultato_unico.append(item)
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()
@@ -338,8 +300,7 @@ def analizza_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"}
@@ -347,24 +308,18 @@ def analizza_frase():
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:
@@ -380,7 +335,7 @@ def analizza_frase():
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,
@@ -393,7 +348,6 @@ def analizza_frase():
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)
 
1
  import os
2
+ import traceback
3
  from flask import Flask, request, jsonify
4
  from flask_cors import CORS
5
+
6
+ # Try to import spacy lazily and handle missing models gracefully
7
+ try:
8
+ import spacy
9
+ except Exception:
10
+ spacy = None
11
 
12
  # ------------------------------
13
+ # Caricamento modello spaCy (con fallback non-bloccante)
14
  # ------------------------------
 
15
  def load_it_model():
 
 
16
  """
17
+ Prova a caricare un modello italiano in ordine di qualità.
18
+ Se nessun modello è installato, restituisce (None, None) e una istruzione per l'utente.
19
+ """
20
+ if spacy is None:
21
+ return None, None, (
22
+ "La libreria spaCy non è installata. "
23
+ "Installa spaCy: pip install spacy"
24
+ )
25
+
26
  candidates = ["it_core_news_lg", "it_core_news_md", "it_core_news_sm"]
27
  last_err = None
28
  for name in candidates:
29
  try:
30
+ nlp = spacy.load(name)
31
+ return nlp, name, None
32
  except Exception as e:
33
  last_err = e
34
+ # nessun modello trovato -> non fallare l'import, ma restituire messaggio utile
35
+ suggestion = (
36
  "Impossibile caricare un modello italiano spaCy. "
37
  "Installa almeno uno tra: it_core_news_lg / it_core_news_md / it_core_news_sm.\n"
38
  "Esempio: python -m spacy download it_core_news_lg\n"
39
  f"Dettagli ultimo errore: {last_err}"
40
  )
41
+ return None, None, suggestion
42
 
43
+ nlp, IT_MODEL, MODEL_LOAD_ERROR = load_it_model()
44
 
45
  # ------------------------------
46
  # Flask App
 
67
  }
68
 
69
  # ------------------------------
70
+ # Traduzioni Morfologia (UD)
71
  # ------------------------------
72
  KEY_MAP = {
73
  "Gender": "Genere", "Number": "Numero", "Mood": "Modo", "Tense": "Tempo",
 
77
  "Aspect": "Aspetto", "Voice": "Voce",
78
  }
79
 
 
80
  VALUE_MAP = {
81
+ "Masc": "Maschile", "Fem": "Femminile", "Sing": "Singolare", "Plur": "Plurale",
 
82
  "Cnd": "Condizionale", "Sub": "Congiuntivo", "Ind": "Indicativo", "Imp": "Imperfetto",
83
  "Inf": "Infinito", "Part": "Participio", "Ger": "Gerundio", "Fin": "Finita",
84
  "Pres": "Presente", "Past": "Passato", "Fut": "Futuro", "Pqp": "Trapassato",
85
+ "1": "1ª", "2": "2ª", "3": "3ª", "Prs": "Personale", "Rel": "Relativo", "Int": "Interrogativo",
86
+ "Dem": "Dimostrativo", "Art": "Articolativo", "Yes": "", "No": "No", "Def": "Determinato",
87
+ "Indef": "Indefinito", "Abs": "Assoluto", "Cmp": "Comparativo", "Sup": "Superlativo",
 
 
88
  "Nom": "Nominativo", "Acc": "Accusativo", "Gen": "Genitivo", "Dat": "Dativo",
89
+ "Perf": "Perfetto", "Prog": "Progressivo", "Act": "Attiva", "Pass": "Passiva",
 
90
  }
91
 
 
92
  PAIR_VALUE_MAP = {
93
+ ("Mood", "Imp"): "Imperativo", ("Tense", "Imp"): "Imperfetto",
94
+ ("Mood", "Ind"): "Indicativo", ("Definite", "Ind"): "Indeterminato",
 
 
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  # ------------------------------
98
  # Mappature Dependency → Etichette italiane
99
  # ------------------------------
100
  MAPPA_DEP = {
101
  "nsubj": {"label": "Soggetto", "description": "Indica chi o cosa compie l'azione o si trova in un certo stato."},
102
+ "nsubj:pass": {"label": "Soggetto (Passivo)", "description": "Soggetto di una frase in forma passiva."},
103
+ "ROOT": {"label": "Predicato Verbale", "description": "Esprime l'azione, l'esistenza o lo stato del soggetto."},
104
+ "obj": {"label": "Complemento Oggetto", "description": "Indica l'oggetto diretto dell'azione. Risponde alla domanda: chi? / che cosa?"},
105
+ "iobj": {"label": "Complemento di Termine", "description": "Indica a chi o a cosa è destinata l'azione. Risponde alla domanda: a chi? / a che cosa?"},
106
+ "obl": {"label": "Complemento Indiretto", "description": "Fornisce informazioni aggiuntive (luogo, tempo, modo, causa, ecc.)."},
107
+ "obl:agent": {"label": "Complemento d'Agente", "description": "Indica chi compie l'azione in una frase passiva. Risponde alla domanda: da chi?"},
108
+ "nmod": {"label": "Complemento di Specificazione", "description": "Specifica o definisce un altro nome. Risponde alla domanda: di chi? / di che cosa?"},
109
+ "amod": {"label": "Attributo", "description": "Aggettivo che qualifica o descrive un nome a cui si riferisce."},
110
+ "advmod": {"label": "Complemento Avverbiale", "description": "Modifica o precisa il significato di un verbo, aggettivo o altro avverbio."},
111
+ "appos": {"label": "Apposizione", "description": "Sostantivo che si affianca a un altro per meglio identificarlo."},
112
+ "acl:relcl": {"label": "Proposizione Subordinata Relativa", "description": "Frase introdotta da un pronome relativo che espande un nome."},
113
+ "advcl": {"label": "Proposizione Subordinata Avverbiale", "description": "Frase che funziona come un complemento avverbiale per la principale."},
114
+ "ccomp": {"label": "Proposizione Subordinata Oggettiva", "description": "Frase che funge da complemento oggetto del verbo della principale."},
115
+ "csubj": {"label": "Proposizione Subordinata Soggettiva", "description": "Frase che funge da soggetto del verbo della principale."},
116
+ "xcomp": {"label": "Complemento Predicativo", "description": "Completa il significato del verbo riferendosi al soggetto o all'oggetto."},
117
+ "conj": {"label": "Elemento Coordinato", "description": "Elemento collegato a un altro con la stessa funzione logica."},
118
+ "cc": {"label": "Congiunzione Coordinante", "description": "Congiunzione (es. e, ma, o) che collega elementi con la stessa funzione."},
119
+ "cop": {"label": "Copula", "description": "Verbo 'essere' che collega il soggetto a un nome o aggettivo (parte nominale)."},
 
 
120
  }
121
 
 
122
  # ------------------------------
123
+ # Utilità di Analisi
124
  # ------------------------------
125
 
126
+ def spiega_in_italiano(tag, tipo='pos'):
127
+ if tipo == 'pos':
128
+ return SPIEGAZIONI_POS_IT.get(tag, tag)
129
+ return SPIEGAZIONI_ENT_IT.get(tag, tag)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ def traduci_morfologia(morph_str: str) -> str:
132
+ if not morph_str or morph_str == "___": return "Non disponibile"
133
+ parti_tradotte = []
134
+ for parte in morph_str.split('|'):
135
+ if '=' not in parte: continue
136
+ chiave, valore = parte.split('=', 1)
137
+ chiave_trad = KEY_MAP.get(chiave, chiave)
138
+ valore_trad = PAIR_VALUE_MAP.get((chiave, valore), VALUE_MAP.get(valore, valore))
139
+ parti_tradotte.append(f"{chiave_trad}: {valore_trad}")
140
+ return ", ".join(sorted(list(set(parti_tradotte)))) or "Non disponibile"
141
 
 
 
142
 
143
+ def ottieni_tipo_complemento_con_dettagli(token):
144
+ preposizione = next((t.text.lower() for t in token.children if t.dep_ == "case"), None)
145
+ if not preposizione: return MAPPA_DEP.get("obl")
146
+
147
+ mappa = {
148
+ "di": ("Complemento di Specificazione", "Risponde alla domanda: di chi? / di che cosa?"),
149
+ "a": ("Complemento di Termine", "Risponde alla domanda: a chi? / a che cosa?"),
150
+ "da": ("Complemento di Moto da Luogo", "Risponde alla domanda: da dove?"),
151
+ "in": ("Complemento di Stato in Luogo", "Risponde alla domanda: dove?"),
152
+ "con": ("Complemento di Compagnia o Mezzo", "Risponde alla domanda: con chi? / con che cosa?"),
153
+ "su": ("Complemento di Argomento o Luogo", "Risponde alla domanda: su chi? / su che cosa? / dove?"),
154
+ "per": ("Complemento di Fine o Causa", "Risponde alla domanda: per quale fine? / per quale causa?"),
155
+ "tra": ("Complemento Partitivo / Luogo", "Risponde alla domanda: tra chi? / tra cosa?"),
156
+ "fra": ("Complemento Partitivo / Luogo", "Risponde alla domanda: fra chi? / fra cosa?"),
157
+ }
158
+
159
+ # Gestione preposizioni articolate
160
+ for base, (label, desc) in mappa.items():
161
+ if preposizione.startswith(base):
162
+ label_final = label
163
+ desc_final = desc
164
+ # Check per complemento d'agente
165
+ if base == "da" and any(c.dep_ == "aux:pass" for c in token.head.children):
166
+ label_final = "Complemento d'Agente"
167
+ desc_final = "Indica da chi è compiuta l'azione in una frase passiva."
168
+ return {"label": label_final, "description": desc_final}
169
+
170
+ return MAPPA_DEP.get("obl")
171
+
172
+
173
+ def costruisci_sintagmi_con_dettagli(tokens_proposizione):
174
+ """
175
+ Costruisce una lista di componenti logici (sintagmi) da una lista di token spaCy.
176
+ Questa versione è più precisa e robusta, evitando di raggruppare erroneamente i componenti.
177
+ """
178
+
179
+ def get_phrase_and_indices(token):
180
+ """Costruisce il testo di un sintagma e restituisce gli indici dei token usati."""
181
+ # Raccoglie ricorsivamente i token coordinati (es. "libri e quaderni")
182
+ conjuncts = [c for c in token.children if c.dep_ == 'conj']
183
+ tokens_nel_sintagma = [token]
184
+ for conj in conjuncts:
185
+ tokens_nel_sintagma.append(conj)
186
+ # Aggiunge anche le congiunzioni (es. "e", "o")
187
+ cc = next((c for c in conj.children if c.dep_ == 'cc'), None)
188
+ if cc: tokens_nel_sintagma.append(cc)
189
+
190
+ # Per ogni token principale, raccoglie i suoi modificatori diretti (articoli, aggettivi, etc.)
191
+ all_phrase_tokens = []
192
+ for t in tokens_nel_sintagma:
193
+ subtree = list(t.subtree)
194
+ # Filtra per tenere solo i modificatori strettamente legati
195
+ modificatori = [n for n in subtree if n.head == t and n.dep_ in ('det', 'amod', 'advmod', 'case', 'compound', 'appos', 'nmod')]
196
+ all_phrase_tokens.extend([t] + modificatori)
197
+
198
+ # Assicura che la congiunzione sia inclusa se presente
199
+ all_phrase_tokens.extend(c for c in token.children if c.dep_ == 'cc')
200
+
201
+ # Ordina i token e crea la stringa finale
202
+ tokens_ordinati = sorted(list(set(all_phrase_tokens)), key=lambda x: x.i)
203
+ testo_sintagma = " ".join(t.text for t in tokens_ordinati)
204
+ indici_usati = {t.i for t in tokens_ordinati}
205
+
206
+ return testo_sintagma, indici_usati
207
 
208
  risultato_analisi = []
209
  indici_elaborati = set()
210
+
211
+ # Token da non processare come "teste" di un sintagma (verranno inclusi dai loro "genitori")
212
+ SKIP_DEPS = {'det', 'case', 'punct', 'aux', 'cop', 'mark', 'cc', 'aux:pass', 'amod', 'advmod'}
213
 
214
+ for token in tokens_proposizione:
215
+ if token.i in indici_elaborati or token.dep_ in SKIP_DEPS:
216
  continue
217
+
218
+ # Gestione speciale per la copula in predicati nominali
219
+ if token.dep_ == "ROOT" and any(c.dep_ == 'cop' for c in token.children):
220
+ cop_token = next(c for c in token.children if c.dep_ == 'cop')
221
+
222
+ # 1. Aggiungi il soggetto
223
+ soggetto = next((s for s in token.head.children if s.dep_.startswith('nsubj')), None)
224
+ if soggetto and soggetto.i not in indici_elaborati:
225
+ s_text, s_indices = get_phrase_and_indices(soggetto)
 
 
 
 
 
 
 
 
 
 
 
 
226
  risultato_analisi.append({
227
+ "text": s_text, "label_info": MAPPA_DEP['nsubj'],
228
+ "token_details": {"lemma": soggetto.lemma_, "pos": spiega_in_italiano(soggetto.pos_), "morph": traduci_morfologia(str(soggetto.morph))}
 
229
  })
230
+ indici_elaborati.update(s_indices)
231
+
232
+ # 2. Aggiungi la copula
233
+ risultato_analisi.append({
234
+ "text": cop_token.text, "label_info": MAPPA_DEP['cop'],
235
+ "token_details": {"lemma": cop_token.lemma_, "pos": spiega_in_italiano(cop_token.pos_), "morph": traduci_morfologia(str(cop_token.morph))}
236
+ })
237
+ indici_elaborati.add(cop_token.i)
238
+
239
+ # 3. Aggiungi la parte nominale
240
+ pn_text, pn_indices = get_phrase_and_indices(token)
241
+ risultato_analisi.append({
242
+ "text": pn_text, "label_info": {"label": "Parte Nominale del Predicato", "description": "Aggettivo o nome che descrive il soggetto."},
243
+ "token_details": {"lemma": token.lemma_, "pos": spiega_in_italiano(token.pos_), "morph": traduci_morfologia(str(token.morph))}
244
+ })
245
+ indici_elaborati.update(pn_indices)
246
+ continue
247
+
248
+ # Logica standard per tutti gli altri componenti
249
+ testo_sintagma, indici_usati = get_phrase_and_indices(token)
250
+
251
+ dep = token.dep_
252
+ if dep in ('obl', 'obl:agent'):
253
  info_etichetta = ottieni_tipo_complemento_con_dettagli(token)
254
+ else:
255
+ info_etichetta = MAPPA_DEP.get(dep, {"label": dep.capitalize(), "description": "Relazione non mappata."})
256
 
257
+ risultato_analisi.append({
258
+ "text": testo_sintagma,
 
259
  "label_info": info_etichetta,
260
+ "token_details": {
261
+ "lemma": token.lemma_,
262
+ "pos": f"{token.pos_}: {spiega_in_italiano(token.pos_)}",
263
+ "tag": f"{token.tag_}: {spiega_in_italiano(token.tag_)}",
264
+ "morph": traduci_morfologia(str(token.morph))
265
+ }
266
+ })
267
+ indici_elaborati.update(indici_usati)
 
 
 
 
 
268
 
269
+ # Ordina i risultati in base alla loro apparizione nella frase
270
+ risultato_analisi.sort(key=lambda x: x['text'].split()[0] in [t.text for t in tokens_proposizione] and [t.text for t in tokens_proposizione].index(x['text'].split()[0]))
271
+ return risultato_analisi
272
 
273
  def analizza_proposizione_con_dettagli(token_proposizione):
274
+ token_validi = [t for t in token_proposizione if not t.is_punct and not t.is_space]
275
+ return costruisci_sintagmi_con_dettagli(token_validi)
276
 
277
  # ------------------------------
278
  # Routes
279
  # ------------------------------
280
  @app.route("/")
281
  def home():
282
+ status = "ok" if nlp is not None else "model_missing"
283
  return jsonify({
284
  "messaggio": "API analisi logica in esecuzione",
285
+ "modello_spacy": IT_MODEL if IT_MODEL else "Nessuno",
286
+ "model_status": status,
287
+ "model_error": MODEL_LOAD_ERROR,
288
  "endpoint": "/api/analyze"
289
  })
290
 
 
291
  @app.route('/api/analyze', methods=['POST'])
292
  def analizza_frase():
293
+ if nlp is None:
294
+ return jsonify({"errore": "Modello spaCy non caricato.", "dettagli": MODEL_LOAD_ERROR}), 503
295
+
296
  try:
297
  dati = request.get_json(silent=True) or {}
298
  frase = (dati.get('sentence') or "").strip()
 
300
  return jsonify({"errore": "Frase non fornita o vuota."}), 400
301
 
302
  doc = nlp(frase)
303
+
 
304
  proposizioni_subordinate = []
305
  indici_subordinate = set()
306
  SUBORD_DEPS = {"acl:relcl", "advcl", "ccomp", "csubj", "xcomp", "acl", "parataxis"}
 
308
  for token in doc:
309
  if token.dep_ in SUBORD_DEPS:
310
  subtree = list(token.subtree)
311
+ subtree_indices = {t.i for t in subtree}
312
+ if not indici_subordinate.intersection(subtree_indices):
313
+ indici_subordinate.update(subtree_indices)
314
+ info_tipo = MAPPA_DEP.get(token.dep_, {"label": "Proposizione Subordinata", "description": "Frase che dipende da un'altra."})
315
+ proposizioni_subordinate.append({
316
+ "type_info": info_tipo,
317
+ "text": " ".join(t.text for t in subtree),
318
+ "analysis": analizza_proposizione_con_dettagli(subtree)
319
+ })
 
 
 
 
320
 
 
321
  token_principale = [t for t in doc if t.i not in indici_subordinate]
322
+
 
323
  entita_nominate = []
324
  visti = set()
325
  for ent in doc.ents:
 
335
  "full_sentence": frase,
336
  "model": IT_MODEL,
337
  "main_clause": {
338
+ "text": " ".join(t.text for t in token_principale),
339
  "analysis": analizza_proposizione_con_dettagli(token_principale)
340
  },
341
  "subordinate_clauses": proposizioni_subordinate,
 
348
  traceback.print_exc()
349
  return jsonify({"errore": "Si è verificato un errore interno.", "dettagli": str(e)}), 500
350
 
 
351
  if __name__ == '__main__':
352
  port = int(os.environ.get("PORT", 8080))
353
+ app.run(host="0.0.0.0", port=port, debug=False, threaded=True)