AIdeaText commited on
Commit
e3208ab
·
verified ·
1 Parent(s): 3ee3961

Update modules/text_analysis/semantic_analysis.py

Browse files
modules/text_analysis/semantic_analysis.py CHANGED
@@ -1,484 +1,484 @@
1
- # modules/text_analysis/semantic_analysis.py
2
-
3
- # 1. Importaciones estándar del sistema
4
- import logging
5
- import io
6
- import base64
7
- from collections import Counter, defaultdict
8
-
9
- # 2. Importaciones de terceros
10
- import streamlit as st
11
- import spacy
12
- import networkx as nx
13
- import matplotlib.pyplot as plt
14
- from sklearn.feature_extraction.text import TfidfVectorizer
15
- from sklearn.metrics.pairwise import cosine_similarity
16
-
17
- # Solo configurar si no hay handlers ya configurados
18
- logger = logging.getLogger(__name__)
19
-
20
- # 4. Importaciones locales
21
- from .stopwords import (
22
- process_text,
23
- clean_text,
24
- get_custom_stopwords,
25
- get_stopwords_for_spacy
26
- )
27
-
28
-
29
- # Define colors for grammatical categories
30
- POS_COLORS = {
31
- 'ADJ': '#FFA07A', 'ADP': '#98FB98', 'ADV': '#87CEFA', 'AUX': '#DDA0DD',
32
- 'CCONJ': '#F0E68C', 'DET': '#FFB6C1', 'INTJ': '#FF6347', 'NOUN': '#90EE90',
33
- 'NUM': '#FAFAD2', 'PART': '#D3D3D3', 'PRON': '#FFA500', 'PROPN': '#20B2AA',
34
- 'SCONJ': '#DEB887', 'SYM': '#7B68EE', 'VERB': '#FF69B4', 'X': '#A9A9A9',
35
- }
36
-
37
- POS_TRANSLATIONS = {
38
- 'es': {
39
- 'ADJ': 'Adjetivo', 'ADP': 'Preposición', 'ADV': 'Adverbio', 'AUX': 'Auxiliar',
40
- 'CCONJ': 'Conjunción Coordinante', 'DET': 'Determinante', 'INTJ': 'Interjección',
41
- 'NOUN': 'Sustantivo', 'NUM': 'Número', 'PART': 'Partícula', 'PRON': 'Pronombre',
42
- 'PROPN': 'Nombre Propio', 'SCONJ': 'Conjunción Subordinante', 'SYM': 'Símbolo',
43
- 'VERB': 'Verbo', 'X': 'Otro',
44
- },
45
- 'en': {
46
- 'ADJ': 'Adjective', 'ADP': 'Preposition', 'ADV': 'Adverb', 'AUX': 'Auxiliary',
47
- 'CCONJ': 'Coordinating Conjunction', 'DET': 'Determiner', 'INTJ': 'Interjection',
48
- 'NOUN': 'Noun', 'NUM': 'Number', 'PART': 'Particle', 'PRON': 'Pronoun',
49
- 'PROPN': 'Proper Noun', 'SCONJ': 'Subordinating Conjunction', 'SYM': 'Symbol',
50
- 'VERB': 'Verb', 'X': 'Other',
51
- },
52
- 'fr': {
53
- 'ADJ': 'Adjectif', 'ADP': 'Préposition', 'ADV': 'Adverbe', 'AUX': 'Auxiliaire',
54
- 'CCONJ': 'Conjonction de Coordination', 'DET': 'Déterminant', 'INTJ': 'Interjection',
55
- 'NOUN': 'Nom', 'NUM': 'Nombre', 'PART': 'Particule', 'PRON': 'Pronom',
56
- 'PROPN': 'Nom Propre', 'SCONJ': 'Conjonction de Subordination', 'SYM': 'Symbole',
57
- 'VERB': 'Verbe', 'X': 'Autre',
58
- }
59
- }
60
-
61
- ENTITY_LABELS = {
62
- 'es': {
63
- "Personas": "lightblue",
64
- "Lugares": "lightcoral",
65
- "Inventos": "lightgreen",
66
- "Fechas": "lightyellow",
67
- "Conceptos": "lightpink"
68
- },
69
- 'en': {
70
- "People": "lightblue",
71
- "Places": "lightcoral",
72
- "Inventions": "lightgreen",
73
- "Dates": "lightyellow",
74
- "Concepts": "lightpink"
75
- },
76
- 'fr': {
77
- "Personnes": "lightblue",
78
- "Lieux": "lightcoral",
79
- "Inventions": "lightgreen",
80
- "Dates": "lightyellow",
81
- "Concepts": "lightpink"
82
- }
83
- }
84
-
85
- def fig_to_bytes(fig):
86
- """Convierte una figura de matplotlib a bytes."""
87
- try:
88
- buf = io.BytesIO()
89
- fig.savefig(buf, format='png', dpi=300, bbox_inches='tight')
90
- buf.seek(0)
91
- return buf.getvalue()
92
- except Exception as e:
93
- logger.error(f"Error en fig_to_bytes: {str(e)}")
94
- return None
95
-
96
- ###########################################################
97
- def perform_semantic_analysis(text, nlp, lang_code):
98
- """
99
- Realiza el análisis semántico completo del texto.
100
- """
101
- if not text or not nlp or not lang_code:
102
- logger.error("Parámetros inválidos para el análisis semántico")
103
- return {
104
- 'success': False,
105
- 'error': 'Parámetros inválidos'
106
- }
107
-
108
- try:
109
- logger.info(f"Starting semantic analysis for language: {lang_code}")
110
-
111
- # Procesar texto y remover stopwords
112
- doc = nlp(text)
113
- if not doc:
114
- logger.error("Error al procesar el texto con spaCy")
115
- return {
116
- 'success': False,
117
- 'error': 'Error al procesar el texto'
118
- }
119
-
120
- # Identificar conceptos clave
121
- logger.info("Identificando conceptos clave...")
122
- stopwords = get_custom_stopwords(lang_code)
123
- key_concepts = identify_key_concepts(doc, stopwords=stopwords)
124
-
125
- if not key_concepts:
126
- logger.warning("No se identificaron conceptos clave")
127
- return {
128
- 'success': False,
129
- 'error': 'No se pudieron identificar conceptos clave'
130
- }
131
-
132
- # Crear grafo de conceptos
133
- logger.info(f"Creando grafo de conceptos con {len(key_concepts)} conceptos...")
134
- concept_graph = create_concept_graph(doc, key_concepts)
135
-
136
- if not concept_graph.nodes():
137
- logger.warning("Se creó un grafo vacío")
138
- return {
139
- 'success': False,
140
- 'error': 'No se pudo crear el grafo de conceptos'
141
- }
142
-
143
- # Visualizar grafo
144
- logger.info("Visualizando grafo...")
145
- plt.clf() # Limpiar figura actual
146
- concept_graph_fig = visualize_concept_graph(concept_graph, lang_code)
147
-
148
- # Convertir a bytes
149
- logger.info("Convirtiendo grafo a bytes...")
150
- graph_bytes = fig_to_bytes(concept_graph_fig)
151
-
152
- if not graph_bytes:
153
- logger.error("Error al convertir grafo a bytes")
154
- return {
155
- 'success': False,
156
- 'error': 'Error al generar visualización'
157
- }
158
-
159
- # Limpiar recursos
160
- plt.close(concept_graph_fig)
161
- plt.close('all')
162
-
163
- result = {
164
- 'success': True,
165
- 'key_concepts': key_concepts,
166
- 'concept_graph': graph_bytes
167
- }
168
-
169
- logger.info("Análisis semántico completado exitosamente")
170
- return result
171
-
172
- except Exception as e:
173
- logger.error(f"Error in perform_semantic_analysis: {str(e)}")
174
- plt.close('all') # Asegurarse de limpiar recursos
175
- return {
176
- 'success': False,
177
- 'error': str(e)
178
- }
179
- finally:
180
- plt.close('all') # Asegurar limpieza incluso si hay error
181
-
182
- ############################################################
183
-
184
- def identify_key_concepts(doc, stopwords, min_freq=2, min_length=3):
185
- """
186
- Identifica conceptos clave en el texto, excluyendo entidades nombradas.
187
- Args:
188
- doc: Documento procesado por spaCy
189
- stopwords: Lista de stopwords
190
- min_freq: Frecuencia mínima para considerar un concepto
191
- min_length: Longitud mínima del concepto
192
- Returns:
193
- List[Tuple[str, int]]: Lista de tuplas (concepto, frecuencia)
194
- """
195
- try:
196
- word_freq = Counter()
197
-
198
- # Crear conjunto de tokens que son parte de entidades
199
- entity_tokens = set()
200
- for ent in doc.ents:
201
- entity_tokens.update(token.i for token in ent)
202
-
203
- # Procesar tokens
204
- for token in doc:
205
- # Verificar si el token no es parte de una entidad nombrada
206
- if (token.i not in entity_tokens and # No es parte de una entidad
207
- token.lemma_.lower() not in stopwords and # No es stopword
208
- len(token.lemma_) >= min_length and # Longitud mínima
209
- token.is_alpha and # Es alfabético
210
- not token.is_punct and # No es puntuación
211
- not token.like_num and # No es número
212
- not token.is_space and # No es espacio
213
- not token.is_stop and # No es stopword de spaCy
214
- not token.pos_ == 'PROPN' and # No es nombre propio
215
- not token.pos_ == 'SYM' and # No es símbolo
216
- not token.pos_ == 'NUM' and # No es número
217
- not token.pos_ == 'X'): # No es otro
218
-
219
- # Convertir a minúsculas y añadir al contador
220
- word_freq[token.lemma_.lower()] += 1
221
-
222
- # Filtrar conceptos por frecuencia mínima y ordenar por frecuencia
223
- concepts = [(word, freq) for word, freq in word_freq.items()
224
- if freq >= min_freq]
225
- concepts.sort(key=lambda x: x[1], reverse=True)
226
-
227
- logger.info(f"Identified {len(concepts)} key concepts after excluding entities")
228
- return concepts[:10]
229
-
230
- except Exception as e:
231
- logger.error(f"Error en identify_key_concepts: {str(e)}")
232
- return []
233
-
234
- ########################################################################
235
-
236
- def create_concept_graph(doc, key_concepts):
237
- """
238
- Crea un grafo de relaciones entre conceptos, ignorando entidades.
239
- Args:
240
- doc: Documento procesado por spaCy
241
- key_concepts: Lista de tuplas (concepto, frecuencia)
242
- Returns:
243
- nx.Graph: Grafo de conceptos
244
- """
245
- try:
246
- G = nx.Graph()
247
-
248
- # Crear un conjunto de conceptos clave para búsqueda rápida
249
- concept_words = {concept[0].lower() for concept in key_concepts}
250
-
251
- # Crear conjunto de tokens que son parte de entidades
252
- entity_tokens = set()
253
- for ent in doc.ents:
254
- entity_tokens.update(token.i for token in ent)
255
-
256
- # Añadir nodos al grafo
257
- for concept, freq in key_concepts:
258
- G.add_node(concept.lower(), weight=freq)
259
-
260
- # Analizar cada oración
261
- for sent in doc.sents:
262
- # Obtener conceptos en la oración actual, excluyendo entidades
263
- current_concepts = []
264
- for token in sent:
265
- if (token.i not in entity_tokens and
266
- token.lemma_.lower() in concept_words):
267
- current_concepts.append(token.lemma_.lower())
268
-
269
- # Crear conexiones entre conceptos en la misma oración
270
- for i, concept1 in enumerate(current_concepts):
271
- for concept2 in current_concepts[i+1:]:
272
- if concept1 != concept2:
273
- if G.has_edge(concept1, concept2):
274
- G[concept1][concept2]['weight'] += 1
275
- else:
276
- G.add_edge(concept1, concept2, weight=1)
277
-
278
- return G
279
-
280
- except Exception as e:
281
- logger.error(f"Error en create_concept_graph: {str(e)}")
282
- return nx.Graph()
283
-
284
- ###############################################################################
285
-
286
- def visualize_concept_graph(G, lang_code):
287
- """
288
- Visualiza el grafo de conceptos con layout consistente.
289
- Args:
290
- G: networkx.Graph - Grafo de conceptos
291
- lang_code: str - Código del idioma
292
- Returns:
293
- matplotlib.figure.Figure - Figura del grafo
294
- """
295
- try:
296
- # Crear nueva figura con mayor tamaño y definir los ejes explícitamente
297
- fig, ax = plt.subplots(figsize=(15, 10))
298
-
299
- if not G.nodes():
300
- logger.warning("Grafo vacío, retornando figura vacía")
301
- return fig
302
-
303
- # Convertir grafo no dirigido a dirigido para mostrar flechas
304
- DG = nx.DiGraph(G)
305
-
306
- # Calcular centralidad de los nodos para el color
307
- centrality = nx.degree_centrality(G)
308
-
309
- # Establecer semilla para reproducibilidad
310
- seed = 42
311
-
312
- # Calcular layout con parámetros fijos
313
- pos = nx.spring_layout(
314
- DG,
315
- k=2, # Distancia ideal entre nodos
316
- iterations=50, # Número de iteraciones
317
- seed=seed # Semilla fija para reproducibilidad
318
- )
319
-
320
- # Calcular factor de escala basado en número de nodos
321
- num_nodes = len(DG.nodes())
322
- scale_factor = 1000 if num_nodes < 10 else 500 if num_nodes < 20 else 200
323
-
324
- # Obtener pesos ajustados
325
- node_weights = [DG.nodes[node].get('weight', 1) * scale_factor for node in DG.nodes()]
326
- edge_weights = [DG[u][v].get('weight', 1) for u, v in DG.edges()]
327
-
328
- # Crear mapa de colores basado en centralidad
329
- node_colors = [plt.cm.viridis(centrality[node]) for node in DG.nodes()]
330
-
331
- # Dibujar nodos
332
- nodes = nx.draw_networkx_nodes(
333
- DG,
334
- pos,
335
- node_size=node_weights,
336
- node_color=node_colors,
337
- alpha=0.7,
338
- ax=ax
339
- )
340
-
341
- # Dibujar aristas con flechas
342
- edges = nx.draw_networkx_edges(
343
- DG,
344
- pos,
345
- width=edge_weights,
346
- alpha=0.6,
347
- edge_color='gray',
348
- arrows=True,
349
- arrowsize=20,
350
- arrowstyle='->',
351
- connectionstyle='arc3,rad=0.2',
352
- ax=ax
353
- )
354
-
355
- # Ajustar tamaño de fuente según número de nodos
356
- font_size = 12 if num_nodes < 10 else 10 if num_nodes < 20 else 8
357
-
358
- # Dibujar etiquetas con fondo blanco para mejor legibilidad
359
- labels = nx.draw_networkx_labels(
360
- DG,
361
- pos,
362
- font_size=font_size,
363
- font_weight='bold',
364
- bbox=dict(
365
- facecolor='white',
366
- edgecolor='none',
367
- alpha=0.7
368
- ),
369
- ax=ax
370
- )
371
-
372
- # Añadir leyenda de centralidad
373
- sm = plt.cm.ScalarMappable(
374
- cmap=plt.cm.viridis,
375
- norm=plt.Normalize(vmin=0, vmax=1)
376
- )
377
- sm.set_array([])
378
- plt.colorbar(sm, ax=ax, label='Centralidad del concepto')
379
-
380
- plt.title("Red de conceptos relacionados", pad=20, fontsize=14)
381
- ax.set_axis_off()
382
-
383
- # Ajustar el layout para que la barra de color no se superponga
384
- plt.tight_layout()
385
-
386
- return fig
387
-
388
- except Exception as e:
389
- logger.error(f"Error en visualize_concept_graph: {str(e)}")
390
- return plt.figure() # Retornar figura vacía en caso de error
391
-
392
- ########################################################################
393
- def create_entity_graph(entities):
394
- G = nx.Graph()
395
- for entity_type, entity_list in entities.items():
396
- for entity in entity_list:
397
- G.add_node(entity, type=entity_type)
398
- for i, entity1 in enumerate(entity_list):
399
- for entity2 in entity_list[i+1:]:
400
- G.add_edge(entity1, entity2)
401
- return G
402
-
403
-
404
- #############################################################
405
- def visualize_entity_graph(G, lang_code):
406
- fig, ax = plt.subplots(figsize=(12, 8))
407
- pos = nx.spring_layout(G)
408
- for entity_type, color in ENTITY_LABELS[lang_code].items():
409
- node_list = [node for node, data in G.nodes(data=True) if data['type'] == entity_type]
410
- nx.draw_networkx_nodes(G, pos, nodelist=node_list, node_color=color, node_size=500, alpha=0.8, ax=ax)
411
- nx.draw_networkx_edges(G, pos, width=1, alpha=0.5, ax=ax)
412
- nx.draw_networkx_labels(G, pos, font_size=8, font_weight="bold", ax=ax)
413
- ax.set_title(f"Relaciones entre Entidades ({lang_code})", fontsize=16)
414
- ax.axis('off')
415
- plt.tight_layout()
416
- return fig
417
-
418
-
419
- #################################################################################
420
- def create_topic_graph(topics, doc):
421
- G = nx.Graph()
422
- for topic in topics:
423
- G.add_node(topic, weight=doc.text.count(topic))
424
- for i, topic1 in enumerate(topics):
425
- for topic2 in topics[i+1:]:
426
- weight = sum(1 for sent in doc.sents if topic1 in sent.text and topic2 in sent.text)
427
- if weight > 0:
428
- G.add_edge(topic1, topic2, weight=weight)
429
- return G
430
-
431
- def visualize_topic_graph(G, lang_code):
432
- fig, ax = plt.subplots(figsize=(12, 8))
433
- pos = nx.spring_layout(G)
434
- node_sizes = [G.nodes[node]['weight'] * 100 for node in G.nodes()]
435
- nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color='lightgreen', alpha=0.8, ax=ax)
436
- nx.draw_networkx_labels(G, pos, font_size=10, font_weight="bold", ax=ax)
437
- edge_weights = [G[u][v]['weight'] for u, v in G.edges()]
438
- nx.draw_networkx_edges(G, pos, width=edge_weights, alpha=0.5, ax=ax)
439
- ax.set_title(f"Relaciones entre Temas ({lang_code})", fontsize=16)
440
- ax.axis('off')
441
- plt.tight_layout()
442
- return fig
443
-
444
- ###########################################################################################
445
- def generate_summary(doc, lang_code):
446
- sentences = list(doc.sents)
447
- summary = sentences[:3] # Toma las primeras 3 oraciones como resumen
448
- return " ".join([sent.text for sent in summary])
449
-
450
- def extract_entities(doc, lang_code):
451
- entities = defaultdict(list)
452
- for ent in doc.ents:
453
- if ent.label_ in ENTITY_LABELS[lang_code]:
454
- entities[ent.label_].append(ent.text)
455
- return dict(entities)
456
-
457
- def analyze_sentiment(doc, lang_code):
458
- positive_words = sum(1 for token in doc if token.sentiment > 0)
459
- negative_words = sum(1 for token in doc if token.sentiment < 0)
460
- total_words = len(doc)
461
- if positive_words > negative_words:
462
- return "Positivo"
463
- elif negative_words > positive_words:
464
- return "Negativo"
465
- else:
466
- return "Neutral"
467
-
468
- def extract_topics(doc, lang_code):
469
- vectorizer = TfidfVectorizer(stop_words='english', max_features=5)
470
- tfidf_matrix = vectorizer.fit_transform([doc.text])
471
- feature_names = vectorizer.get_feature_names_out()
472
- return list(feature_names)
473
-
474
- # Asegúrate de que todas las funciones necesarias estén exportadas
475
- __all__ = [
476
- 'perform_semantic_analysis',
477
- 'identify_key_concepts',
478
- 'create_concept_graph',
479
- 'visualize_concept_graph',
480
- 'fig_to_bytes', # Faltaba esta coma
481
- 'ENTITY_LABELS',
482
- 'POS_COLORS',
483
- 'POS_TRANSLATIONS'
484
  ]
 
1
+ # modules/text_analysis/semantic_analysis.py
2
+
3
+ # 1. Importaciones estándar del sistema
4
+ import logging
5
+ import io
6
+ import base64
7
+ from collections import Counter, defaultdict
8
+
9
+ # 2. Importaciones de terceros
10
+ import streamlit as st
11
+ import spacy
12
+ import networkx as nx
13
+ import matplotlib.pyplot as plt
14
+ from sklearn.feature_extraction.text import TfidfVectorizer
15
+ from sklearn.metrics.pairwise import cosine_similarity
16
+
17
+ # Solo configurar si no hay handlers ya configurados
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # 4. Importaciones locales
21
+ from .stopwords import (
22
+ process_text,
23
+ clean_text,
24
+ get_custom_stopwords,
25
+ get_stopwords_for_spacy
26
+ )
27
+
28
+
29
+ # Define colors for grammatical categories
30
+ POS_COLORS = {
31
+ 'ADJ': '#FFA07A', 'ADP': '#98FB98', 'ADV': '#87CEFA', 'AUX': '#DDA0DD',
32
+ 'CCONJ': '#F0E68C', 'DET': '#FFB6C1', 'INTJ': '#FF6347', 'NOUN': '#90EE90',
33
+ 'NUM': '#FAFAD2', 'PART': '#D3D3D3', 'PRON': '#FFA500', 'PROPN': '#20B2AA',
34
+ 'SCONJ': '#DEB887', 'SYM': '#7B68EE', 'VERB': '#FF69B4', 'X': '#A9A9A9',
35
+ }
36
+
37
+ POS_TRANSLATIONS = {
38
+ 'es': {
39
+ 'ADJ': 'Adjetivo', 'ADP': 'Preposición', 'ADV': 'Adverbio', 'AUX': 'Auxiliar',
40
+ 'CCONJ': 'Conjunción Coordinante', 'DET': 'Determinante', 'INTJ': 'Interjección',
41
+ 'NOUN': 'Sustantivo', 'NUM': 'Número', 'PART': 'Partícula', 'PRON': 'Pronombre',
42
+ 'PROPN': 'Nombre Propio', 'SCONJ': 'Conjunción Subordinante', 'SYM': 'Símbolo',
43
+ 'VERB': 'Verbo', 'X': 'Otro',
44
+ },
45
+ 'en': {
46
+ 'ADJ': 'Adjective', 'ADP': 'Preposition', 'ADV': 'Adverb', 'AUX': 'Auxiliary',
47
+ 'CCONJ': 'Coordinating Conjunction', 'DET': 'Determiner', 'INTJ': 'Interjection',
48
+ 'NOUN': 'Noun', 'NUM': 'Number', 'PART': 'Particle', 'PRON': 'Pronoun',
49
+ 'PROPN': 'Proper Noun', 'SCONJ': 'Subordinating Conjunction', 'SYM': 'Symbol',
50
+ 'VERB': 'Verb', 'X': 'Other',
51
+ },
52
+ 'uk': {
53
+ 'ADJ': 'Прикметник', 'ADP': 'Прийменник', 'ADV': 'Прислівник', 'AUX': 'Допоміжне дієслово',
54
+ 'CCONJ': 'Сурядний сполучник', 'DET': 'Означник', 'INTJ': 'Вигук',
55
+ 'NOUN': 'Іменник', 'NUM': 'Число', 'PART': 'Частка', 'PRON': 'Займенник',
56
+ 'PROPN': 'Власна назва', 'SCONJ': 'Підрядний сполучник', 'SYM': 'Символ',
57
+ 'VERB': 'Дієслово', 'X': 'Інше',
58
+ }
59
+ }
60
+
61
+ ENTITY_LABELS = {
62
+ 'es': {
63
+ "Personas": "lightblue",
64
+ "Lugares": "lightcoral",
65
+ "Inventos": "lightgreen",
66
+ "Fechas": "lightyellow",
67
+ "Conceptos": "lightpink"
68
+ },
69
+ 'en': {
70
+ "People": "lightblue",
71
+ "Places": "lightcoral",
72
+ "Inventions": "lightgreen",
73
+ "Dates": "lightyellow",
74
+ "Concepts": "lightpink"
75
+ },
76
+ 'uk': {
77
+ "Люди": "lightblue",
78
+ "Місця": "lightcoral",
79
+ "Винаходи": "lightgreen",
80
+ "Дати": "lightyellow",
81
+ "Концепції": "lightpink"
82
+ }
83
+ }
84
+
85
+ def fig_to_bytes(fig):
86
+ """Convierte una figura de matplotlib a bytes."""
87
+ try:
88
+ buf = io.BytesIO()
89
+ fig.savefig(buf, format='png', dpi=300, bbox_inches='tight')
90
+ buf.seek(0)
91
+ return buf.getvalue()
92
+ except Exception as e:
93
+ logger.error(f"Error en fig_to_bytes: {str(e)}")
94
+ return None
95
+
96
+ ###########################################################
97
+ def perform_semantic_analysis(text, nlp, lang_code):
98
+ """
99
+ Realiza el análisis semántico completo del texto.
100
+ """
101
+ if not text or not nlp or not lang_code:
102
+ logger.error("Parámetros inválidos para el análisis semántico")
103
+ return {
104
+ 'success': False,
105
+ 'error': 'Parámetros inválidos'
106
+ }
107
+
108
+ try:
109
+ logger.info(f"Starting semantic analysis for language: {lang_code}")
110
+
111
+ # Procesar texto y remover stopwords
112
+ doc = nlp(text)
113
+ if not doc:
114
+ logger.error("Error al procesar el texto con spaCy")
115
+ return {
116
+ 'success': False,
117
+ 'error': 'Error al procesar el texto'
118
+ }
119
+
120
+ # Identificar conceptos clave
121
+ logger.info("Identificando conceptos clave...")
122
+ stopwords = get_custom_stopwords(lang_code)
123
+ key_concepts = identify_key_concepts(doc, stopwords=stopwords)
124
+
125
+ if not key_concepts:
126
+ logger.warning("No se identificaron conceptos clave")
127
+ return {
128
+ 'success': False,
129
+ 'error': 'No se pudieron identificar conceptos clave'
130
+ }
131
+
132
+ # Crear grafo de conceptos
133
+ logger.info(f"Creando grafo de conceptos con {len(key_concepts)} conceptos...")
134
+ concept_graph = create_concept_graph(doc, key_concepts)
135
+
136
+ if not concept_graph.nodes():
137
+ logger.warning("Se creó un grafo vacío")
138
+ return {
139
+ 'success': False,
140
+ 'error': 'No se pudo crear el grafo de conceptos'
141
+ }
142
+
143
+ # Visualizar grafo
144
+ logger.info("Visualizando grafo...")
145
+ plt.clf() # Limpiar figura actual
146
+ concept_graph_fig = visualize_concept_graph(concept_graph, lang_code)
147
+
148
+ # Convertir a bytes
149
+ logger.info("Convirtiendo grafo a bytes...")
150
+ graph_bytes = fig_to_bytes(concept_graph_fig)
151
+
152
+ if not graph_bytes:
153
+ logger.error("Error al convertir grafo a bytes")
154
+ return {
155
+ 'success': False,
156
+ 'error': 'Error al generar visualización'
157
+ }
158
+
159
+ # Limpiar recursos
160
+ plt.close(concept_graph_fig)
161
+ plt.close('all')
162
+
163
+ result = {
164
+ 'success': True,
165
+ 'key_concepts': key_concepts,
166
+ 'concept_graph': graph_bytes
167
+ }
168
+
169
+ logger.info("Análisis semántico completado exitosamente")
170
+ return result
171
+
172
+ except Exception as e:
173
+ logger.error(f"Error in perform_semantic_analysis: {str(e)}")
174
+ plt.close('all') # Asegurarse de limpiar recursos
175
+ return {
176
+ 'success': False,
177
+ 'error': str(e)
178
+ }
179
+ finally:
180
+ plt.close('all') # Asegurar limpieza incluso si hay error
181
+
182
+ ############################################################
183
+
184
+ def identify_key_concepts(doc, stopwords, min_freq=2, min_length=3):
185
+ """
186
+ Identifica conceptos clave en el texto, excluyendo entidades nombradas.
187
+ Args:
188
+ doc: Documento procesado por spaCy
189
+ stopwords: Lista de stopwords
190
+ min_freq: Frecuencia mínima para considerar un concepto
191
+ min_length: Longitud mínima del concepto
192
+ Returns:
193
+ List[Tuple[str, int]]: Lista de tuplas (concepto, frecuencia)
194
+ """
195
+ try:
196
+ word_freq = Counter()
197
+
198
+ # Crear conjunto de tokens que son parte de entidades
199
+ entity_tokens = set()
200
+ for ent in doc.ents:
201
+ entity_tokens.update(token.i for token in ent)
202
+
203
+ # Procesar tokens
204
+ for token in doc:
205
+ # Verificar si el token no es parte de una entidad nombrada
206
+ if (token.i not in entity_tokens and # No es parte de una entidad
207
+ token.lemma_.lower() not in stopwords and # No es stopword
208
+ len(token.lemma_) >= min_length and # Longitud mínima
209
+ token.is_alpha and # Es alfabético
210
+ not token.is_punct and # No es puntuación
211
+ not token.like_num and # No es número
212
+ not token.is_space and # No es espacio
213
+ not token.is_stop and # No es stopword de spaCy
214
+ not token.pos_ == 'PROPN' and # No es nombre propio
215
+ not token.pos_ == 'SYM' and # No es símbolo
216
+ not token.pos_ == 'NUM' and # No es número
217
+ not token.pos_ == 'X'): # No es otro
218
+
219
+ # Convertir a minúsculas y añadir al contador
220
+ word_freq[token.lemma_.lower()] += 1
221
+
222
+ # Filtrar conceptos por frecuencia mínima y ordenar por frecuencia
223
+ concepts = [(word, freq) for word, freq in word_freq.items()
224
+ if freq >= min_freq]
225
+ concepts.sort(key=lambda x: x[1], reverse=True)
226
+
227
+ logger.info(f"Identified {len(concepts)} key concepts after excluding entities")
228
+ return concepts[:10]
229
+
230
+ except Exception as e:
231
+ logger.error(f"Error en identify_key_concepts: {str(e)}")
232
+ return []
233
+
234
+ ########################################################################
235
+
236
+ def create_concept_graph(doc, key_concepts):
237
+ """
238
+ Crea un grafo de relaciones entre conceptos, ignorando entidades.
239
+ Args:
240
+ doc: Documento procesado por spaCy
241
+ key_concepts: Lista de tuplas (concepto, frecuencia)
242
+ Returns:
243
+ nx.Graph: Grafo de conceptos
244
+ """
245
+ try:
246
+ G = nx.Graph()
247
+
248
+ # Crear un conjunto de conceptos clave para búsqueda rápida
249
+ concept_words = {concept[0].lower() for concept in key_concepts}
250
+
251
+ # Crear conjunto de tokens que son parte de entidades
252
+ entity_tokens = set()
253
+ for ent in doc.ents:
254
+ entity_tokens.update(token.i for token in ent)
255
+
256
+ # Añadir nodos al grafo
257
+ for concept, freq in key_concepts:
258
+ G.add_node(concept.lower(), weight=freq)
259
+
260
+ # Analizar cada oración
261
+ for sent in doc.sents:
262
+ # Obtener conceptos en la oración actual, excluyendo entidades
263
+ current_concepts = []
264
+ for token in sent:
265
+ if (token.i not in entity_tokens and
266
+ token.lemma_.lower() in concept_words):
267
+ current_concepts.append(token.lemma_.lower())
268
+
269
+ # Crear conexiones entre conceptos en la misma oración
270
+ for i, concept1 in enumerate(current_concepts):
271
+ for concept2 in current_concepts[i+1:]:
272
+ if concept1 != concept2:
273
+ if G.has_edge(concept1, concept2):
274
+ G[concept1][concept2]['weight'] += 1
275
+ else:
276
+ G.add_edge(concept1, concept2, weight=1)
277
+
278
+ return G
279
+
280
+ except Exception as e:
281
+ logger.error(f"Error en create_concept_graph: {str(e)}")
282
+ return nx.Graph()
283
+
284
+ ###############################################################################
285
+
286
+ def visualize_concept_graph(G, lang_code):
287
+ """
288
+ Visualiza el grafo de conceptos con layout consistente.
289
+ Args:
290
+ G: networkx.Graph - Grafo de conceptos
291
+ lang_code: str - Código del idioma
292
+ Returns:
293
+ matplotlib.figure.Figure - Figura del grafo
294
+ """
295
+ try:
296
+ # Crear nueva figura con mayor tamaño y definir los ejes explícitamente
297
+ fig, ax = plt.subplots(figsize=(15, 10))
298
+
299
+ if not G.nodes():
300
+ logger.warning("Grafo vacío, retornando figura vacía")
301
+ return fig
302
+
303
+ # Convertir grafo no dirigido a dirigido para mostrar flechas
304
+ DG = nx.DiGraph(G)
305
+
306
+ # Calcular centralidad de los nodos para el color
307
+ centrality = nx.degree_centrality(G)
308
+
309
+ # Establecer semilla para reproducibilidad
310
+ seed = 42
311
+
312
+ # Calcular layout con parámetros fijos
313
+ pos = nx.spring_layout(
314
+ DG,
315
+ k=2, # Distancia ideal entre nodos
316
+ iterations=50, # Número de iteraciones
317
+ seed=seed # Semilla fija para reproducibilidad
318
+ )
319
+
320
+ # Calcular factor de escala basado en número de nodos
321
+ num_nodes = len(DG.nodes())
322
+ scale_factor = 1000 if num_nodes < 10 else 500 if num_nodes < 20 else 200
323
+
324
+ # Obtener pesos ajustados
325
+ node_weights = [DG.nodes[node].get('weight', 1) * scale_factor for node in DG.nodes()]
326
+ edge_weights = [DG[u][v].get('weight', 1) for u, v in DG.edges()]
327
+
328
+ # Crear mapa de colores basado en centralidad
329
+ node_colors = [plt.cm.viridis(centrality[node]) for node in DG.nodes()]
330
+
331
+ # Dibujar nodos
332
+ nodes = nx.draw_networkx_nodes(
333
+ DG,
334
+ pos,
335
+ node_size=node_weights,
336
+ node_color=node_colors,
337
+ alpha=0.7,
338
+ ax=ax
339
+ )
340
+
341
+ # Dibujar aristas con flechas
342
+ edges = nx.draw_networkx_edges(
343
+ DG,
344
+ pos,
345
+ width=edge_weights,
346
+ alpha=0.6,
347
+ edge_color='gray',
348
+ arrows=True,
349
+ arrowsize=20,
350
+ arrowstyle='->',
351
+ connectionstyle='arc3,rad=0.2',
352
+ ax=ax
353
+ )
354
+
355
+ # Ajustar tamaño de fuente según número de nodos
356
+ font_size = 12 if num_nodes < 10 else 10 if num_nodes < 20 else 8
357
+
358
+ # Dibujar etiquetas con fondo blanco para mejor legibilidad
359
+ labels = nx.draw_networkx_labels(
360
+ DG,
361
+ pos,
362
+ font_size=font_size,
363
+ font_weight='bold',
364
+ bbox=dict(
365
+ facecolor='white',
366
+ edgecolor='none',
367
+ alpha=0.7
368
+ ),
369
+ ax=ax
370
+ )
371
+
372
+ # Añadir leyenda de centralidad
373
+ sm = plt.cm.ScalarMappable(
374
+ cmap=plt.cm.viridis,
375
+ norm=plt.Normalize(vmin=0, vmax=1)
376
+ )
377
+ sm.set_array([])
378
+ plt.colorbar(sm, ax=ax, label='Centralidad del concepto')
379
+
380
+ plt.title("Red de conceptos relacionados", pad=20, fontsize=14)
381
+ ax.set_axis_off()
382
+
383
+ # Ajustar el layout para que la barra de color no se superponga
384
+ plt.tight_layout()
385
+
386
+ return fig
387
+
388
+ except Exception as e:
389
+ logger.error(f"Error en visualize_concept_graph: {str(e)}")
390
+ return plt.figure() # Retornar figura vacía en caso de error
391
+
392
+ ########################################################################
393
+ def create_entity_graph(entities):
394
+ G = nx.Graph()
395
+ for entity_type, entity_list in entities.items():
396
+ for entity in entity_list:
397
+ G.add_node(entity, type=entity_type)
398
+ for i, entity1 in enumerate(entity_list):
399
+ for entity2 in entity_list[i+1:]:
400
+ G.add_edge(entity1, entity2)
401
+ return G
402
+
403
+
404
+ #############################################################
405
+ def visualize_entity_graph(G, lang_code):
406
+ fig, ax = plt.subplots(figsize=(12, 8))
407
+ pos = nx.spring_layout(G)
408
+ for entity_type, color in ENTITY_LABELS[lang_code].items():
409
+ node_list = [node for node, data in G.nodes(data=True) if data['type'] == entity_type]
410
+ nx.draw_networkx_nodes(G, pos, nodelist=node_list, node_color=color, node_size=500, alpha=0.8, ax=ax)
411
+ nx.draw_networkx_edges(G, pos, width=1, alpha=0.5, ax=ax)
412
+ nx.draw_networkx_labels(G, pos, font_size=8, font_weight="bold", ax=ax)
413
+ ax.set_title(f"Relaciones entre Entidades ({lang_code})", fontsize=16)
414
+ ax.axis('off')
415
+ plt.tight_layout()
416
+ return fig
417
+
418
+
419
+ #################################################################################
420
+ def create_topic_graph(topics, doc):
421
+ G = nx.Graph()
422
+ for topic in topics:
423
+ G.add_node(topic, weight=doc.text.count(topic))
424
+ for i, topic1 in enumerate(topics):
425
+ for topic2 in topics[i+1:]:
426
+ weight = sum(1 for sent in doc.sents if topic1 in sent.text and topic2 in sent.text)
427
+ if weight > 0:
428
+ G.add_edge(topic1, topic2, weight=weight)
429
+ return G
430
+
431
+ def visualize_topic_graph(G, lang_code):
432
+ fig, ax = plt.subplots(figsize=(12, 8))
433
+ pos = nx.spring_layout(G)
434
+ node_sizes = [G.nodes[node]['weight'] * 100 for node in G.nodes()]
435
+ nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color='lightgreen', alpha=0.8, ax=ax)
436
+ nx.draw_networkx_labels(G, pos, font_size=10, font_weight="bold", ax=ax)
437
+ edge_weights = [G[u][v]['weight'] for u, v in G.edges()]
438
+ nx.draw_networkx_edges(G, pos, width=edge_weights, alpha=0.5, ax=ax)
439
+ ax.set_title(f"Relaciones entre Temas ({lang_code})", fontsize=16)
440
+ ax.axis('off')
441
+ plt.tight_layout()
442
+ return fig
443
+
444
+ ###########################################################################################
445
+ def generate_summary(doc, lang_code):
446
+ sentences = list(doc.sents)
447
+ summary = sentences[:3] # Toma las primeras 3 oraciones como resumen
448
+ return " ".join([sent.text for sent in summary])
449
+
450
+ def extract_entities(doc, lang_code):
451
+ entities = defaultdict(list)
452
+ for ent in doc.ents:
453
+ if ent.label_ in ENTITY_LABELS[lang_code]:
454
+ entities[ent.label_].append(ent.text)
455
+ return dict(entities)
456
+
457
+ def analyze_sentiment(doc, lang_code):
458
+ positive_words = sum(1 for token in doc if token.sentiment > 0)
459
+ negative_words = sum(1 for token in doc if token.sentiment < 0)
460
+ total_words = len(doc)
461
+ if positive_words > negative_words:
462
+ return "Positivo"
463
+ elif negative_words > positive_words:
464
+ return "Negativo"
465
+ else:
466
+ return "Neutral"
467
+
468
+ def extract_topics(doc, lang_code):
469
+ vectorizer = TfidfVectorizer(stop_words='english', max_features=5)
470
+ tfidf_matrix = vectorizer.fit_transform([doc.text])
471
+ feature_names = vectorizer.get_feature_names_out()
472
+ return list(feature_names)
473
+
474
+ # Asegúrate de que todas las funciones necesarias estén exportadas
475
+ __all__ = [
476
+ 'perform_semantic_analysis',
477
+ 'identify_key_concepts',
478
+ 'create_concept_graph',
479
+ 'visualize_concept_graph',
480
+ 'fig_to_bytes', # Faltaba esta coma
481
+ 'ENTITY_LABELS',
482
+ 'POS_COLORS',
483
+ 'POS_TRANSLATIONS'
484
  ]