AIdeaText commited on
Commit
2eccb8b
·
verified ·
1 Parent(s): e1bc382

Update modules/text_analysis/semantic_analysis.py

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