devusman commited on
Commit
6058f1c
·
verified ·
1 Parent(s): e8fa023

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -118
app.py CHANGED
@@ -1,50 +1,25 @@
 
 
 
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, ("La libreria spaCy non è installata. Installa spaCy: pip install spacy")
22
-
23
- candidates = ["it_core_news_lg", "it_core_news_md", "it_core_news_sm"]
24
- last_err = None
25
- for name in candidates:
26
- try:
27
- nlp = spacy.load(name)
28
- return nlp, name, None
29
- except Exception as e:
30
- last_err = e
31
- suggestion = (
32
- "Impossibile caricare un modello italiano spaCy. "
33
- "Installa almeno uno tra: it_core_news_lg / it_core_news_md / it_core_news_sm.\n"
34
- f"Esempio: python -m spacy download it_core_news_lg\nDettagli ultimo errore: {last_err}"
35
- )
36
- return None, None, suggestion
37
-
38
- nlp, IT_MODEL, MODEL_LOAD_ERROR = load_it_model()
39
-
40
- # ------------------------------
41
- # Flask App
42
- # ------------------------------
43
- app = Flask(__name__)
44
- CORS(app)
45
 
46
  # ------------------------------
47
- # Tabelle di spiegazione POS / NER
48
  # ------------------------------
49
  SPIEGAZIONI_POS_IT = {
50
  "ADJ": "Aggettivo", "ADP": "Preposizione", "ADV": "Avverbio", "AUX": "Ausiliare",
@@ -55,12 +30,10 @@ SPIEGAZIONI_POS_IT = {
55
  }
56
 
57
  SPIEGAZIONI_ENT_IT = {
58
- "PER": "Persona", "LOC": "Luogo", "ORG": "Organizzazione", "MISC": "Miscellanea"
 
59
  }
60
 
61
- # ------------------------------
62
- # Traduzioni Morfologia (UD)
63
- # ------------------------------
64
  KEY_MAP = {
65
  "Gender": "Genere", "Number": "Numero", "Mood": "Modo", "Tense": "Tempo",
66
  "Person": "Persona", "VerbForm": "Forma del Verbo", "PronType": "Tipo di Pronome",
@@ -82,7 +55,7 @@ PAIR_VALUE_MAP = {
82
  }
83
 
84
  # ------------------------------
85
- # Mappature Dependency Etichette italiane
86
  # ------------------------------
87
  MAPPA_DEP = {
88
  "nsubj": {"label": "Soggetto", "description": "Indica chi o cosa compie l'azione o si trova in un certo stato."},
@@ -107,156 +80,206 @@ MAPPA_DEP = {
107
  }
108
 
109
  # ------------------------------
110
- # Utilità di Analisi
111
  # ------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
 
 
 
 
 
113
  def spiega_in_italiano(tag, tipo='pos'):
114
- if tipo == 'pos': return SPIEGAZIONI_POS_IT.get(tag, tag)
115
- if tipo == 'ent': return f"{SPIEGAZIONI_ENT_IT.get(tag, tag)}: {SPIEGAZIONI_ENT_IT.get(tag, {}).get('description', '')}"
 
 
116
  return tag
117
 
118
  def traduci_morfologia(morph_str: str) -> str:
119
- if not morph_str or morph_str == "___": return "Non disponibile"
 
120
  parti = morph_str.split('|')
121
- parti_tradotte = set()
122
  for parte in parti:
123
- if '=' not in parte: continue
 
124
  chiave, valore = parte.split('=', 1)
125
  chiave_trad = KEY_MAP.get(chiave, chiave)
126
  valore_trad = PAIR_VALUE_MAP.get((chiave, valore), VALUE_MAP.get(valore, valore))
127
- parti_tradotte.add(f"{chiave_trad}: {valore_trad}")
128
- return ", ".join(sorted(list(parti_tradotte))) or "Non disponibile"
129
 
130
  def ottieni_tipo_complemento_con_dettagli(token):
131
- case_token = next((child for child in token.children if child.dep_ == 'case'), None)
132
- if not case_token: return MAPPA_DEP.get("obl")
133
- preposizione = case_token.text.lower()
134
-
 
 
 
 
 
 
 
 
135
  mappa = {
136
  "di": ("Complemento di Specificazione", "Risponde alla domanda: di chi? / di che cosa?"),
137
  "a": ("Complemento di Termine", "Risponde alla domanda: a chi? / a che cosa?"),
138
- "da": ("Complemento di Moto da Luogo", "Risponde alla domanda: da dove?"),
139
  "in": ("Complemento di Stato in Luogo", "Risponde alla domanda: dove?"),
140
  "con": ("Complemento di Compagnia o Mezzo", "Risponde alla domanda: con chi? / con che cosa?"),
141
  "su": ("Complemento di Argomento o Luogo", "Risponde alla domanda: su chi? / su che cosa? / dove?"),
142
  "per": ("Complemento di Fine o Causa", "Risponde alla domanda: per quale fine? / per quale causa?"),
143
  "tra": ("Complemento Partitivo / Luogo", "Risponde alla domanda: tra chi? / tra cosa?"),
144
  "fra": ("Complemento Partitivo / Luogo", "Risponde alla domanda: fra chi? / fra cosa?"),
 
 
145
  }
146
-
147
  for base, (label, desc) in mappa.items():
148
- if preposizione.startswith(base):
149
- if base == "da" and any(c.dep_ == "aux:pass" for c in token.head.children):
 
150
  return {"label": "Complemento d'Agente", "description": "Indica da chi è compiuta l'azione in una frase passiva."}
151
  return {"label": label, "description": desc}
152
-
153
- return MAPPA_DEP.get("obl")
154
 
155
  def get_full_phrase_for_token(token):
156
  """
157
- FIXED: Costruisce un sintagma in modo preciso, raccogliendo solo i modificatori
158
- strettamente collegati e gli elementi coordinati.
159
  """
160
- phrase_tokens = []
161
-
162
- # Funzione interna per raccogliere i token di un singolo elemento e i suoi figli diretti
163
- def collect_children(t):
164
- # Raccoglie i modificatori diretti (articoli, aggettivi, preposizioni)
165
- children = [t]
 
 
166
  for child in t.children:
167
- if child.dep_ in ('det', 'amod', 'case', 'compound', 'advmod', 'appos'):
168
- children.extend(collect_children(child)) # Raccoglie anche i figli dei figli (es. avverbi di aggettivi)
169
- return children
170
 
171
- # Raccoglie i token per il token principale
172
- phrase_tokens.extend(collect_children(token))
173
-
174
- # Gestisce la coordinazione (es. "libri e quaderni")
175
  for child in token.children:
176
  if child.dep_ == 'conj':
177
- # Aggiunge la congiunzione (es. "e", "o")
 
178
  cc = next((c for c in child.children if c.dep_ == 'cc'), None)
179
  if cc:
180
- phrase_tokens.append(cc)
181
- # Aggiunge l'intero sintagma coordinato
182
- phrase_tokens.extend(get_full_phrase_for_token(child))
183
 
184
- # Ordina i token in base alla loro posizione originale e rimuove duplicati
185
- unique_tokens = sorted(list(set(phrase_tokens)), key=lambda t: t.i)
186
- text = " ".join(t.text for t in unique_tokens)
187
- indices = {t.i for t in unique_tokens}
188
- return text, indices
189
 
190
  def costruisci_sintagmi_con_dettagli(tokens_proposizione):
191
  """
192
- FIXED: L'algoritmo ora processa ogni componente logico separatamente e con precisione.
193
  """
194
- risultato_analisi = []
 
 
195
  indici_elaborati = set()
196
-
197
- # Definisce le dipendenze che non sono "teste" di un sintagma ma parti di esso
198
- DEPS_DA_SALTARE = {'det', 'amod', 'case', 'aux', 'aux:pass', 'cop', 'mark', 'cc', 'advmod', 'compound', 'appos'}
199
 
200
  for token in tokens_proposizione:
201
- if token.i in indici_elaborati or token.dep_ in DEPS_DA_SALTARE:
 
 
 
202
  continue
203
-
204
  testo_sintagma, indici_usati = get_full_phrase_for_token(token)
205
-
206
  dep = token.dep_
207
  if dep in ('obl', 'obl:agent', 'nmod'):
208
  info_etichetta = ottieni_tipo_complemento_con_dettagli(token)
209
  else:
210
  info_etichetta = MAPPA_DEP.get(dep, {"label": dep.capitalize(), "description": "Relazione non mappata."})
211
 
212
- # Caso speciale per predicato nominale
213
- if dep == "ROOT" and any(c.dep_ == 'cop' for c in token.children):
214
- info_etichetta = {"label": "Parte Nominale del Predicato", "description": "Aggettivo o nome che descrive il soggetto."}
 
 
 
215
 
216
- risultato_analisi.append({
217
  "text": testo_sintagma,
218
  "label_info": info_etichetta,
219
- "token_details": {
220
- "lemma": token.lemma_,
221
- "pos": f"{token.pos_}: {spiega_in_italiano(token.pos_)}",
222
- "tag": f"{token.tag_}: {spiega_in_italiano(token.tag_)}",
223
- "morph": traduci_morfologia(str(token.morph))
224
- },
225
  "token_index": token.i
226
  })
 
227
  indici_elaborati.update(indici_usati)
228
 
229
- # Aggiungi componenti saltati (es. copula, congiunzioni) che sono importanti
230
  for token in tokens_proposizione:
231
  if token.i not in indici_elaborati and token.dep_ in ('cop', 'cc'):
232
- risultato_analisi.append({
233
  "text": token.text,
234
- "label_info": MAPPA_DEP.get(token.dep_),
235
- "token_details": { "lemma": token.lemma_, "pos": f"{token.pos_}: {spiega_in_italiano(token.pos_)}", "morph": traduci_morfologia(str(token.morph)) },
 
 
 
 
 
236
  "token_index": token.i
237
  })
 
238
 
239
- # Ordina i risultati finali in base all'indice del token principale
240
- risultato_analisi.sort(key=lambda x: x['token_index'])
241
- return risultato_analisi
242
 
243
  def analizza_proposizione_con_dettagli(tokens):
244
  tokens_validi = [t for t in tokens if not t.is_punct and not t.is_space]
245
  return costruisci_sintagmi_con_dettagli(tokens_validi)
246
 
247
  # ------------------------------
248
- # Routes
249
  # ------------------------------
 
 
 
250
  @app.route("/")
251
  def home():
252
  status = "ok" if nlp else "model_missing"
253
  return jsonify({
254
- "messaggio": "API analisi logica in esecuzione", "modello_spacy": IT_MODEL or "Nessuno",
255
- "model_status": status, "model_error": MODEL_LOAD_ERROR, "endpoint": "/api/analyze"
 
 
256
  })
257
 
258
  @app.route('/api/analyze', methods=['POST'])
259
  def analizza_frase():
 
260
  if not nlp:
261
  return jsonify({"errore": "Modello spaCy non caricato.", "dettagli": MODEL_LOAD_ERROR}), 503
262
 
@@ -265,12 +288,15 @@ def analizza_frase():
265
  frase = (dati.get('sentence') or "").strip()
266
  if not frase:
267
  return jsonify({"errore": "Frase non fornita o vuota."}), 400
 
 
268
 
269
  doc = nlp(frase)
270
-
271
- proposizioni_subordinate, indici_subordinate = [], set()
272
- SUBORD_DEPS = {"acl:relcl", "advcl", "ccomp", "csubj", "xcomp", "acl", "parataxis"}
273
 
 
 
 
 
274
  for token in doc:
275
  if token.dep_ in SUBORD_DEPS and token.i not in indici_subordinate:
276
  subtree = list(token.subtree)
@@ -283,22 +309,26 @@ def analizza_frase():
283
  "analysis": analizza_proposizione_con_dettagli(subtree)
284
  })
285
 
286
- token_principale = [t for t in doc if t.i not in indici_subordinate]
287
-
 
 
288
  entita_nominate = []
289
  visti = set()
290
  for ent in doc.ents:
291
  if ent.text not in visti:
292
  visti.add(ent.text)
293
  entita_nominate.append({
294
- "text": ent.text, "label": ent.label_,
295
- "explanation": f"{SPIEGAZIONI_ENT_IT.get(ent.label_, ent.label_)}"
 
296
  })
297
 
298
  analisi_finale = {
299
- "full_sentence": frase, "model": IT_MODEL,
 
300
  "main_clause": {
301
- "text": " ".join(t.text for t in token_principale if not t.is_punct).strip(),
302
  "analysis": analizza_proposizione_con_dettagli(token_principale)
303
  },
304
  "subordinate_clauses": proposizioni_subordinate,
@@ -308,9 +338,11 @@ def analizza_frase():
308
  return jsonify(analisi_finale)
309
 
310
  except Exception as e:
 
311
  traceback.print_exc()
312
  return jsonify({"errore": "Si è verificato un errore interno.", "dettagli": str(e)}), 500
313
 
314
  if __name__ == '__main__':
315
  port = int(os.environ.get("PORT", 8080))
316
- app.run(host="0.0.0.0", port=port, debug=False, threaded=True)
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
  import os
5
  import traceback
6
  from flask import Flask, request, jsonify
7
  from flask_cors import CORS
8
 
9
+ # Try to import spaCy lazily and handle missing models gracefully
10
  try:
11
  import spacy
12
  except Exception:
13
  spacy = None
14
 
15
  # ------------------------------
16
+ # Config
17
  # ------------------------------
18
+ MAX_SENTENCE_LENGTH = 2000 # characters, to avoid huge inputs
19
+ SUBORD_DEPS = {"acl:relcl", "advcl", "ccomp", "csubj", "xcomp", "acl", "parataxis"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  # ------------------------------
22
+ # Utility dictionaries (Italian)
23
  # ------------------------------
24
  SPIEGAZIONI_POS_IT = {
25
  "ADJ": "Aggettivo", "ADP": "Preposizione", "ADV": "Avverbio", "AUX": "Ausiliare",
 
30
  }
31
 
32
  SPIEGAZIONI_ENT_IT = {
33
+ "PER": "Persona", "LOC": "Luogo", "ORG": "Organizzazione", "MISC": "Miscellanea",
34
+ # spaCy uses many possible entity labels depending on model/lang — fallback to label itself later
35
  }
36
 
 
 
 
37
  KEY_MAP = {
38
  "Gender": "Genere", "Number": "Numero", "Mood": "Modo", "Tense": "Tempo",
39
  "Person": "Persona", "VerbForm": "Forma del Verbo", "PronType": "Tipo di Pronome",
 
55
  }
56
 
57
  # ------------------------------
58
+ # Map dependency labels to Italian labels and explanations
59
  # ------------------------------
60
  MAPPA_DEP = {
61
  "nsubj": {"label": "Soggetto", "description": "Indica chi o cosa compie l'azione o si trova in un certo stato."},
 
80
  }
81
 
82
  # ------------------------------
83
+ # Model load helper (non-blocking)
84
  # ------------------------------
85
+ def load_it_model():
86
+ """
87
+ Try to load an Italian spaCy model in order of quality.
88
+ Returns (nlp, model_name, error_message) where nlp may be None.
89
+ """
90
+ if spacy is None:
91
+ return None, None, "La libreria spaCy non è installata. Esegui: pip install spacy"
92
+
93
+ candidates = ["it_core_news_lg", "it_core_news_md", "it_core_news_sm"]
94
+ last_err = None
95
+ for name in candidates:
96
+ try:
97
+ nlp = spacy.load(name)
98
+ return nlp, name, None
99
+ except Exception as e:
100
+ last_err = e
101
+ suggestion = (
102
+ "Impossibile caricare un modello italiano spaCy. "
103
+ "Installa almeno uno tra: it_core_news_lg / it_core_news_md / it_core_news_sm.\n"
104
+ "Esempio: python -m spacy download it_core_news_lg\n"
105
+ f"Dettagli ultimo errore: {last_err}"
106
+ )
107
+ return None, None, suggestion
108
 
109
+ nlp, IT_MODEL, MODEL_LOAD_ERROR = load_it_model()
110
+
111
+ # ------------------------------
112
+ # Small helper converters
113
+ # ------------------------------
114
  def spiega_in_italiano(tag, tipo='pos'):
115
+ if tipo == 'pos':
116
+ return SPIEGAZIONI_POS_IT.get(tag, tag)
117
+ if tipo == 'ent':
118
+ return SPIEGAZIONI_ENT_IT.get(tag, tag)
119
  return tag
120
 
121
  def traduci_morfologia(morph_str: str) -> str:
122
+ if not morph_str or morph_str == "___":
123
+ return "Non disponibile"
124
  parti = morph_str.split('|')
125
+ parti_tradotte = []
126
  for parte in parti:
127
+ if '=' not in parte:
128
+ continue
129
  chiave, valore = parte.split('=', 1)
130
  chiave_trad = KEY_MAP.get(chiave, chiave)
131
  valore_trad = PAIR_VALUE_MAP.get((chiave, valore), VALUE_MAP.get(valore, valore))
132
+ parti_tradotte.append(f"{chiave_trad}: {valore_trad}")
133
+ return ", ".join(parti_tradotte) or "Non disponibile"
134
 
135
  def ottieni_tipo_complemento_con_dettagli(token):
136
+ """
137
+ Given a token that is an 'obl' or similar, inspect 'case' (preposition) children to
138
+ return a more precise complement label (e.g. stato in luogo, di termine, ecc.)
139
+ """
140
+ # find child with dep_ == 'case' (a preposition)
141
+ case_token = next((c for c in token.children if c.dep_ == 'case'), None)
142
+ if not case_token:
143
+ # fallback
144
+ return MAPPA_DEP.get("obl", {"label": "Complemento", "description": "Complemento non specificato."})
145
+
146
+ prepo = case_token.text.lower()
147
+ # mapping by start of preposition
148
  mappa = {
149
  "di": ("Complemento di Specificazione", "Risponde alla domanda: di chi? / di che cosa?"),
150
  "a": ("Complemento di Termine", "Risponde alla domanda: a chi? / a che cosa?"),
151
+ "da": ("Complemento di Moto da Luogo / Origine", "Risponde alla domanda: da dove?"),
152
  "in": ("Complemento di Stato in Luogo", "Risponde alla domanda: dove?"),
153
  "con": ("Complemento di Compagnia o Mezzo", "Risponde alla domanda: con chi? / con che cosa?"),
154
  "su": ("Complemento di Argomento o Luogo", "Risponde alla domanda: su chi? / su che cosa? / dove?"),
155
  "per": ("Complemento di Fine o Causa", "Risponde alla domanda: per quale fine? / per quale causa?"),
156
  "tra": ("Complemento Partitivo / Luogo", "Risponde alla domanda: tra chi? / tra cosa?"),
157
  "fra": ("Complemento Partitivo / Luogo", "Risponde alla domanda: fra chi? / fra cosa?"),
158
+ "sopra": ("Complemento di Luogo", "Risponde alla domanda: dove?"),
159
+ "sotto": ("Complemento di Luogo", "Risponde alla domanda: dove?"),
160
  }
 
161
  for base, (label, desc) in mappa.items():
162
+ if prepo.startswith(base):
163
+ # special-case: 'da' + passive aux => agente
164
+ if base == "da" and any(c.dep_.endswith('agent') or c.dep_ == 'aux:pass' for c in token.head.children):
165
  return {"label": "Complemento d'Agente", "description": "Indica da chi è compiuta l'azione in una frase passiva."}
166
  return {"label": label, "description": desc}
167
+ return MAPPA_DEP.get("obl", {"label": "Complemento", "description": "Complemento non specificato."})
 
168
 
169
  def get_full_phrase_for_token(token):
170
  """
171
+ Build a compact phrase for a head token by collecting determiners, amod, case, compounds, and simple modifiers.
172
+ Returns (text, set(indices)).
173
  """
174
+ # recursive collection but with small scope to avoid over-collecting
175
+ collected = set()
176
+
177
+ def collect(t):
178
+ if t.i in collected:
179
+ return
180
+ collected.add(t.i)
181
+ # Collect children that usually belong inside the noun phrase / token phrase
182
  for child in t.children:
183
+ if child.dep_ in ('det', 'amod', 'case', 'compound', 'nummod', 'appos', 'fixed', 'flat', 'advmod'):
184
+ collect(child)
185
+ collect(token)
186
 
187
+ # also include simple coordinated tokens (conj)
 
 
 
188
  for child in token.children:
189
  if child.dep_ == 'conj':
190
+ collect(child)
191
+ # include the coordinating conjunction token if present (cc)
192
  cc = next((c for c in child.children if c.dep_ == 'cc'), None)
193
  if cc:
194
+ collected.add(cc.i)
 
 
195
 
196
+ # sort by token index
197
+ tokens = sorted(collected)
198
+ text = " ".join(token.doc[i].text for i in tokens)
199
+ return text, set(tokens)
 
200
 
201
  def costruisci_sintagmi_con_dettagli(tokens_proposizione):
202
  """
203
+ Build structured analysis for each "major" token in a clause.
204
  """
205
+ risultato = []
206
+ # tokens_proposizione assumed to be a list of spaCy tokens (no punctuation/space)
207
+ DEPS_DA_SALTARE = {'det', 'amod', 'case', 'aux', 'aux:pass', 'cop', 'mark', 'cc', 'compound', 'appos', 'punct'}
208
  indici_elaborati = set()
 
 
 
209
 
210
  for token in tokens_proposizione:
211
+ if token.i in indici_elaborati:
212
+ continue
213
+ # skip tokens that are primarily modifiers (we will include them as part of head tokens)
214
+ if token.dep_ in DEPS_DA_SALTARE and token.head.i != token.i:
215
  continue
216
+
217
  testo_sintagma, indici_usati = get_full_phrase_for_token(token)
218
+
219
  dep = token.dep_
220
  if dep in ('obl', 'obl:agent', 'nmod'):
221
  info_etichetta = ottieni_tipo_complemento_con_dettagli(token)
222
  else:
223
  info_etichetta = MAPPA_DEP.get(dep, {"label": dep.capitalize(), "description": "Relazione non mappata."})
224
 
225
+ token_details = {
226
+ "lemma": getattr(token, "lemma_", token.text),
227
+ "pos": f"{getattr(token, 'pos_', token.pos_)}: {spiega_in_italiano(getattr(token, 'pos_', token.pos_), 'pos')}",
228
+ "tag": getattr(token, "tag_", ""),
229
+ "morph": traduci_morfologia(str(getattr(token, "morph", "")))
230
+ }
231
 
232
+ risultato.append({
233
  "text": testo_sintagma,
234
  "label_info": info_etichetta,
235
+ "token_details": token_details,
 
 
 
 
 
236
  "token_index": token.i
237
  })
238
+
239
  indici_elaborati.update(indici_usati)
240
 
241
+ # include leftover important tokens like copula or coordinating conjunctions if not already included
242
  for token in tokens_proposizione:
243
  if token.i not in indici_elaborati and token.dep_ in ('cop', 'cc'):
244
+ risultato.append({
245
  "text": token.text,
246
+ "label_info": MAPPA_DEP.get(token.dep_, {"label": token.dep_, "description": ""}),
247
+ "token_details": {
248
+ "lemma": getattr(token, "lemma_", token.text),
249
+ "pos": f"{getattr(token, 'pos_', token.pos_)}: {spiega_in_italiano(getattr(token, 'pos_', token.pos_), 'pos')}",
250
+ "tag": getattr(token, "tag_", ""),
251
+ "morph": traduci_morfologia(str(getattr(token, "morph", "")))
252
+ },
253
  "token_index": token.i
254
  })
255
+ indici_elaborati.add(token.i)
256
 
257
+ risultato.sort(key=lambda x: x['token_index'])
258
+ return risultato
 
259
 
260
  def analizza_proposizione_con_dettagli(tokens):
261
  tokens_validi = [t for t in tokens if not t.is_punct and not t.is_space]
262
  return costruisci_sintagmi_con_dettagli(tokens_validi)
263
 
264
  # ------------------------------
265
+ # Flask app
266
  # ------------------------------
267
+ app = Flask(__name__)
268
+ CORS(app)
269
+
270
  @app.route("/")
271
  def home():
272
  status = "ok" if nlp else "model_missing"
273
  return jsonify({
274
+ "messaggio": "API analisi logica in esecuzione",
275
+ "modello_spacy": IT_MODEL or "Nessuno",
276
+ "model_status": status,
277
+ "model_error": MODEL_LOAD_ERROR
278
  })
279
 
280
  @app.route('/api/analyze', methods=['POST'])
281
  def analizza_frase():
282
+ # Basic checks
283
  if not nlp:
284
  return jsonify({"errore": "Modello spaCy non caricato.", "dettagli": MODEL_LOAD_ERROR}), 503
285
 
 
288
  frase = (dati.get('sentence') or "").strip()
289
  if not frase:
290
  return jsonify({"errore": "Frase non fornita o vuota."}), 400
291
+ if len(frase) > MAX_SENTENCE_LENGTH:
292
+ return jsonify({"errore": "Frase troppo lunga.", "max_length": MAX_SENTENCE_LENGTH}), 400
293
 
294
  doc = nlp(frase)
 
 
 
295
 
296
+ proposizioni_subordinate = []
297
+ indici_subordinate = set()
298
+
299
+ # detect subordinate clauses via tokens that have dependency in SUBORD_DEPS
300
  for token in doc:
301
  if token.dep_ in SUBORD_DEPS and token.i not in indici_subordinate:
302
  subtree = list(token.subtree)
 
309
  "analysis": analizza_proposizione_con_dettagli(subtree)
310
  })
311
 
312
+ # main clause tokens are tokens not part of subordinate clause subtrees
313
+ token_principale = [t for t in doc if t.i not in indici_subordinate and not t.is_punct and not t.is_space]
314
+
315
+ # named entities (unique)
316
  entita_nominate = []
317
  visti = set()
318
  for ent in doc.ents:
319
  if ent.text not in visti:
320
  visti.add(ent.text)
321
  entita_nominate.append({
322
+ "text": ent.text,
323
+ "label": ent.label_,
324
+ "explanation": spiega_in_italiano(ent.label_, 'ent')
325
  })
326
 
327
  analisi_finale = {
328
+ "full_sentence": frase,
329
+ "model": IT_MODEL,
330
  "main_clause": {
331
+ "text": " ".join(t.text for t in token_principale).strip(),
332
  "analysis": analizza_proposizione_con_dettagli(token_principale)
333
  },
334
  "subordinate_clauses": proposizioni_subordinate,
 
338
  return jsonify(analisi_finale)
339
 
340
  except Exception as e:
341
+ # print to server log for debugging but return safe message
342
  traceback.print_exc()
343
  return jsonify({"errore": "Si è verificato un errore interno.", "dettagli": str(e)}), 500
344
 
345
  if __name__ == '__main__':
346
  port = int(os.environ.get("PORT", 8080))
347
+ # Note: debug=False for production; set to True only during development
348
+ app.run(host="0.0.0.0", port=port, debug=False, threaded=True)