AIdeaText commited on
Commit
dcdbcfb
·
1 Parent(s): 4a2b4af

update live analysis

Browse files
modules/database/semantic_mongo_live_db.py CHANGED
@@ -5,6 +5,7 @@ import io
5
  import base64
6
  from datetime import datetime, timezone
7
  from pymongo.errors import PyMongoError
 
8
 
9
  # Importaciones de terceros
10
  import matplotlib.pyplot as plt
@@ -27,63 +28,76 @@ COLLECTION_NAME = 'student_semantic_live_analysis'
27
 
28
  def store_student_semantic_live_result(username, text, analysis_result, lang_code='en'):
29
  """
30
- Versión corregida con:
31
- - Verificación correcta de colección
32
- - Manejo de proyección alternativo
33
- - Mejor manejo de errores
34
  """
35
  try:
36
- # 1. Obtener colección con verificación correcta
 
 
 
 
37
  collection = get_collection(COLLECTION_NAME)
38
- if collection is None: # Cambiado de 'if not collection'
39
  logger.error(f"No se pudo obtener la colección {COLLECTION_NAME}")
40
  return False
41
 
42
- # 2. Validación de parámetros
43
- if not all([username, text, analysis_result]):
44
- logger.error("Parámetros incompletos para guardar análisis")
45
- return False
46
 
47
- # 3. Preparar documento (CORREGIDO)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  analysis_document = {
49
  'username': username,
50
- 'timestamp': datetime.now(timezone.utc), # Mejor práctica
51
- 'text': text[:50000],
52
  'analysis_type': 'semantic_live',
53
  'language': lang_code,
54
- # Extraer datos correctamente del análisis
55
  'key_concepts': analysis_result.get('key_concepts', []),
56
  'concept_centrality': analysis_result.get('concept_centrality', {}),
57
- 'concept_graph': None # Inicializar como None
58
  }
59
-
60
- # 4. Manejo del gráfico (CORREGIDO)
61
- if 'concept_graph' in analysis_result and analysis_result['concept_graph']:
62
- try:
63
- # Si ya es bytes, usar directamente
64
- if isinstance(analysis_result['concept_graph'], bytes):
65
- analysis_document['concept_graph'] = analysis_result['concept_graph']
66
- else:
67
- # Convertir a bytes si es necesario
68
- analysis_document['concept_graph'] = base64.b64decode(
69
- analysis_result['concept_graph'])
70
- except Exception as e:
71
- logger.error(f"Error procesando gráfico: {str(e)}")
72
 
73
- # 5. Insertar documento
74
  try:
75
  result = collection.insert_one(analysis_document)
76
  if result.inserted_id:
77
- logger.info(f"Análisis guardado. ID: {result.inserted_id}")
78
  return True
79
- logger.error("Inserción fallida - Sin ID devuelto")
80
  return False
81
  except PyMongoError as e:
82
- logger.error(f"Error de MongoDB: {str(e)}")
83
  return False
84
 
85
  except Exception as e:
86
- logger.error(f"Error inesperado: {str(e)}", exc_info=True)
87
  return False
88
 
89
  ##########################################
 
5
  import base64
6
  from datetime import datetime, timezone
7
  from pymongo.errors import PyMongoError
8
+ from PIL import Image
9
 
10
  # Importaciones de terceros
11
  import matplotlib.pyplot as plt
 
28
 
29
  def store_student_semantic_live_result(username, text, analysis_result, lang_code='en'):
30
  """
31
+ Versión optimizada:
32
+ - Elimina redundancias en el procesamiento de bytes.
33
+ - Soluciona el error 413 (RequestEntityTooLarge) mediante compresión JPEG.
34
+ - Manejo de fechas nativo para MongoDB.
35
  """
36
  try:
37
+ # 1. Validación inicial y obtención de colección
38
+ if not all([username, text, analysis_result]):
39
+ logger.error("Parámetros incompletos para guardar análisis")
40
+ return False
41
+
42
  collection = get_collection(COLLECTION_NAME)
43
+ if collection is None:
44
  logger.error(f"No se pudo obtener la colección {COLLECTION_NAME}")
45
  return False
46
 
47
+ # 2. Procesamiento y Optimización del Gráfico (Sin redundancias)
48
+ graph_data = analysis_result.get('concept_graph')
49
+ final_graph_bytes = None
 
50
 
51
+ if graph_data:
52
+ try:
53
+ # Convertir a bytes si viene en base64 (string)
54
+ if isinstance(graph_data, str):
55
+ final_graph_bytes = base64.b64decode(graph_data)
56
+ else:
57
+ final_graph_bytes = graph_data
58
+
59
+ # Optimización de tamaño para evitar error 413 en Azure/Mongo
60
+ # Solo comprimimos si detectamos que existe la imagen
61
+ img = Image.open(io.BytesIO(final_graph_bytes))
62
+ if img.mode != 'RGB':
63
+ img = img.convert('RGB')
64
+
65
+ output = io.BytesIO()
66
+ # JPEG al 75% mantiene legibilidad y reduce el peso drásticamente
67
+ img.save(output, format="JPEG", quality=75, optimize=True)
68
+ final_graph_bytes = output.getvalue()
69
+
70
+ logger.info(f"Grafo optimizado para {username} ({len(final_graph_bytes)} bytes)")
71
+ except Exception as e:
72
+ logger.warning(f"Error optimizando imagen, se usará formato original: {e}")
73
+ # Si falla la optimización, mantenemos lo que teníamos (si eran bytes)
74
+ final_graph_bytes = final_graph_bytes if isinstance(final_graph_bytes, bytes) else None
75
+
76
+ # 3. Preparación del documento (Directo y limpio)
77
  analysis_document = {
78
  'username': username,
79
+ 'timestamp': datetime.now(timezone.utc),
80
+ 'text': text[:50000], # Límite de seguridad
81
  'analysis_type': 'semantic_live',
82
  'language': lang_code,
 
83
  'key_concepts': analysis_result.get('key_concepts', []),
84
  'concept_centrality': analysis_result.get('concept_centrality', {}),
85
+ 'concept_graph': final_graph_bytes # Insertamos los bytes ya procesados
86
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ # 4. Inserción en base de datos
89
  try:
90
  result = collection.insert_one(analysis_document)
91
  if result.inserted_id:
92
+ logger.info(f"Análisis guardado exitosamente. ID: {result.inserted_id}")
93
  return True
 
94
  return False
95
  except PyMongoError as e:
96
+ logger.error(f"Error de inserción en MongoDB: {str(e)}")
97
  return False
98
 
99
  except Exception as e:
100
+ logger.error(f"Error inesperado en store_student_semantic_live_result: {str(e)}", exc_info=True)
101
  return False
102
 
103
  ##########################################
modules/text_analysis/stopwords.py CHANGED
@@ -1,188 +1,81 @@
1
  # modules/text_analysis/stopwords.py
2
  import spacy
3
  from typing import Set, List
 
 
4
 
5
- def get_custom_stopwords(lang_code: str) -> Set[str]:
6
- """
7
- Retorna un conjunto de stopwords personalizadas según el idioma.
8
- """
9
- # Stopwords base en español
10
- # Símbolos, números y caracteres especiales
11
 
12
- SYMBOLS_AND_NUMBERS = {
13
- # Números
14
  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
15
-
16
- # Signos de puntuación básicos
17
- '.', ',', ';', ':', '!', '¡', '?', '¿', '"', "'",
18
-
19
- # Símbolos matemáticos
20
- '+', '-', '*', '/', '=', '<', '>', '%',
21
-
22
- # Paréntesis y otros delimitadores
23
- '(', ')', '[', ']', '{', '}',
24
-
25
- # Otros símbolos comunes
26
- '@', '#', '$', '€', '£', '¥', '&', '_', '|', '\\', '/',
27
-
28
- # Caracteres especiales
29
- '•', '·', '…', '—', '–', '°', '´', '`', '^', '¨',
30
-
31
- # Símbolos de ordenamiento
32
- '§', '†', '‡', '¶',
33
-
34
- # Símbolos de copyright y marcas registradas
35
- '©', '®', '™',
36
-
37
- # Fracciones comunes
38
- '½', '¼', '¾', '⅓', '⅔',
39
-
40
- # Otros caracteres especiales
41
- '±', '×', '÷', '∞', '≠', '≤', '≥', '≈', '∑', '∏', '√',
42
-
43
- # Espacios y caracteres de control
44
- ' ', '\t', '\n', '\r', '\f', '\v'
45
  }
46
- spanish_stopwords = {
47
- 'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas', 'y', 'o', 'pero', 'si',
48
- 'de', 'del', 'al', 'a', 'ante', 'bajo', 'cabe', 'con', 'contra', 'de', 'desde',
49
- 'en', 'entre', 'hacia', 'hasta', 'para', 'por', 'según', 'sin', 'sobre', 'tras',
50
- 'que', 'más', 'este', 'esta', 'estos', 'estas', 'ese', 'esa', 'esos', 'esas',
51
- 'muy', 'mucho', 'muchos', 'muchas', 'ser', 'estar', 'tener', 'hacer', 'como',
52
- 'cuando', 'donde', 'quien', 'cual', 'mientras', 'sino', 'pues', 'porque',
53
- 'cada', 'cual', 'cuales', 'cuanta', 'cuantas', 'cuanto', 'cuantos', 'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve', 'diez',
54
- 'once', 'doce', 'trece', 'catorce', 'quince', 'dieciséis', 'diecisiete', 'dieciocho', 'diecinueve', 'veinte',
55
- 'treinta', 'cuarenta', 'cincuenta', 'sesenta', 'setenta', 'ochenta', 'noventa', 'cien', 'mil', 'millón',
56
- 'primero', 'segundo', 'tercero', 'cuarto', 'quinto', 'sexto', 'séptimo', 'octavo', 'noveno', 'décimo'
57
- }
58
-
59
- # Stopwords base en inglés
60
- english_stopwords = {
61
- 'the', 'be', 'to', 'of', 'and', 'a', 'in', 'that', 'have', 'i', 'it', 'for',
62
- 'not', 'on', 'with', 'he', 'as', 'you', 'do', 'at', 'this', 'but', 'his',
63
- 'by', 'from', 'they', 'we', 'say', 'her', 'she', 'or', 'an', 'will', 'my',
64
- 'one', 'all', 'would', 'there', 'their', 'what', 'so', 'up', 'out', 'if',
65
- 'about', 'who', 'get', 'which', 'go', 'me', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
66
- 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty',
67
- 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety', 'hundred', 'thousand', 'million',
68
- 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth'
69
- }
70
 
71
- french_stopwords = {
72
- 'le', 'la', 'les', 'un', 'une', 'des', 'du', 'de', 'et', 'ou', 'mais', 'si',
73
- 'à', 'dans', 'sur', 'pour', 'en', 'vers', 'par', 'avec', 'sans', 'sous', 'sur',
74
- 'que', 'qui', 'quoi', 'dont', 'où', 'quand', 'comment', 'pourquoi',
75
- 'ce', 'cet', 'cette', 'ces', 'mon', 'ton', 'son', 'ma', 'ta', 'sa',
76
- 'mes', 'tes', 'ses', 'notre', 'votre', 'leur', 'nos', 'vos', 'leurs',
77
- 'je', 'tu', 'il', 'elle', 'nous', 'vous', 'ils', 'elles',
78
- 'me', 'te', 'se', 'lui', 'leur', 'y', 'en', 'plus', 'moins',
79
- 'très', 'trop', 'peu', 'beaucoup', 'assez', 'tout', 'toute', 'tous', 'toutes',
80
- 'autre', 'autres', 'même', 'mêmes', 'tel', 'telle', 'tels', 'telles',
81
- 'quel', 'quelle', 'quels', 'quelles', 'quelque', 'quelques',
82
- 'aucun', 'aucune', 'aucuns', 'aucunes', 'plusieurs', 'chaque',
83
- 'être', 'avoir', 'faire', 'dire', 'aller', 'venir', 'voir', 'savoir',
84
- 'pouvoir', 'vouloir', 'falloir', 'devoir', 'croire', 'sembler',
85
- 'alors', 'ainsi', 'car', 'donc', 'or', 'ni', 'ne', 'pas', 'plus',
86
- 'jamais', 'toujours', 'parfois', 'souvent', 'maintenant', 'après',
87
- 'avant', 'pendant', 'depuis', 'déjà', 'encore', 'ici', '',
88
- 'oui', 'non', 'peut-être', 'bien', 'mal', 'aussi', 'surtout',
89
- 'c\'est', 'j\'ai', 'n\'est', 'd\'un', 'd\'une', 'qu\'il', 'qu\'elle',
90
- 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf', 'dix',
91
- 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize', 'dix-sept', 'dix-huit', 'dix-neuf', 'vingt',
92
- 'trente', 'quarante', 'cinquante', 'soixante', 'soixante-dix', 'quatre-vingts', 'quatre-vingt-dix', 'cent', 'mille', 'million',
93
- 'premier', 'deuxième', 'troisième', 'quatrième', 'cinquième', 'sixième', 'septième', 'huitième', 'neuvième', 'dixième'
94
- }
95
-
96
- stopwords_dict = {
97
- 'es': spanish_stopwords,
98
- 'en': english_stopwords,
99
- 'fr': french_stopwords
100
  }
101
-
102
- # Obtener stopwords del idioma especificado o devolver conjunto vacío si no existe
103
- return stopwords_dict.get(lang_code, set())
 
 
 
 
 
 
 
 
 
104
 
105
- def process_text(text: str, lang_code: str, nlp) -> List[str]:
106
  """
107
- Procesa un texto completo, removiendo stopwords, símbolos y números.
108
-
109
- Args:
110
- text (str): Texto a procesar
111
- lang_code (str): Código del idioma ('es', 'en', 'fr')
112
- nlp: Modelo de spaCy cargado
113
-
114
- Returns:
115
- List[str]: Lista de tokens procesados
116
  """
117
  try:
118
- # Obtener stopwords personalizadas
119
- custom_stopwords = get_custom_stopwords(lang_code)
120
-
121
- # Procesar el texto con spaCy
122
  doc = nlp(text)
123
-
124
- # Filtrar y procesar tokens
125
  processed_tokens = []
 
126
  for token in doc:
127
- # Convertir a minúsculas y obtener el lema
128
  lemma = token.lemma_.lower()
 
 
129
 
130
- # Aplicar filtros
131
- if (len(lemma) >= 2 and # Longitud mínima
132
- lemma not in custom_stopwords and # No es stopword
133
- not token.is_punct and # No es puntuación
134
- not token.is_space and # No es espacio
135
- lemma not in SYMBOLS_AND_NUMBERS and # No es símbolo o número
136
- not any(char in string.punctuation for char in lemma) and # No contiene puntuación
137
- not any(char.isdigit() for char in lemma)): # No contiene números
138
-
139
  processed_tokens.append(lemma)
140
-
141
  return processed_tokens
142
-
143
  except Exception as e:
144
  logger.error(f"Error en process_text: {str(e)}")
145
  return []
146
 
147
  def clean_text(text: str) -> str:
148
- """
149
- Limpia un texto removiendo caracteres especiales y normalizando espacios.
150
-
151
- Args:
152
- text (str): Texto a limpiar
153
-
154
- Returns:
155
- str: Texto limpio
156
- """
157
- # Remover caracteres especiales y números
158
  cleaned = ''.join(char for char in text if char not in SYMBOLS_AND_NUMBERS)
159
-
160
- # Normalizar espacios
161
- cleaned = ' '.join(cleaned.split())
162
-
163
- return cleaned.strip()
164
-
165
- def get_stopwords_for_spacy(lang_code: str, nlp) -> Set[str]:
166
- """
167
- Combina stopwords personalizadas con las de spaCy.
168
-
169
- Args:
170
- lang_code (str): Código del idioma
171
- nlp: Modelo de spaCy
172
-
173
- Returns:
174
- Set[str]: Conjunto combinado de stopwords
175
- """
176
- custom_stops = get_custom_stopwords(lang_code)
177
- spacy_stops = nlp.Defaults.stop_words if hasattr(nlp.Defaults, 'stop_words') else set()
178
-
179
- return custom_stops.union(spacy_stops)
180
-
181
- # Asegúrate de exportar todas las funciones necesarias
182
- __all__ = [
183
- 'get_custom_stopwords',
184
- 'process_text',
185
- 'clean_text',
186
- 'get_stopwords_for_spacy',
187
- 'SYMBOLS_AND_NUMBERS'
188
- ]
 
1
  # modules/text_analysis/stopwords.py
2
  import spacy
3
  from typing import Set, List
4
+ import unicodedata
5
+ import logging
6
 
7
+ logger = logging.getLogger(__name__)
 
 
 
 
 
8
 
9
+ # Símbolos universales (Preservado del original)
10
+ SYMBOLS_AND_NUMBERS = {
11
  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
12
+ '.', ',', ';', ':', '!', '¡', '?', '¿', '"', "'",
13
+ '+', '-', '*', '/', '=', '<', '>', '%', '(', ')',
14
+ '[', ']', '{', '}', '@', '#', '$', '', '&', '_',
15
+ '|', '\\', '•', '·', '…', '©', '®', '™'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ # --- FUNCIÓN ADICIONAL INTERNA (Documentación: Manejo de acentos) ---
19
+ def _normalize_caseless(text: str) -> str:
20
+ """Función interna para comparar palabras ignorando tildes y mayúsculas."""
21
+ text = text.lower()
22
+ return ''.join(
23
+ c for c in unicodedata.normalize('NFD', text)
24
+ if unicodedata.category(c) != 'Mn'
25
+ )
26
+
27
+ def get_custom_stopwords(lang_code: str) -> Set[str]:
28
+ """
29
+ NOMBRE PRESERVADO. Ahora incluye diccionarios para ES, EN, FR, PT.
30
+ """
31
+ stops_by_lang = {
32
+ 'es': {'el', 'la', 'un', 'una', 'y', 'e', 'o', 'u', 'pero', 'con', 'por', 'para'},
33
+ 'en': {'the', 'a', 'an', 'and', 'but', 'if', 'or', 'with', 'for', 'at', 'by'},
34
+ 'fr': {'le', 'la', 'les', 'un', 'une', 'des', 'et', 'ou', 'mais', 'dans'},
35
+ 'pt': {'o', 'a', 'os', 'as', 'um', 'uma', 'e', 'mas', 'ou', 'com', 'por'}
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
+ return stops_by_lang.get(lang_code, stops_by_lang['en'])
38
+
39
+ def get_stopwords_for_spacy(lang_code: str, nlp) -> Set[str]:
40
+ """
41
+ NOMBRE PRESERVADO. Ahora normaliza las stopwords de spaCy para que
42
+ filtren palabras con tilde correctamente.
43
+ """
44
+ spacy_stops = nlp.Defaults.stop_words if hasattr(nlp.Defaults, 'stop_words') else set()
45
+ custom_stops = get_custom_stopwords(lang_code)
46
+ combined = spacy_stops.union(custom_stops)
47
+ # Normalizamos todas para una comparación ciega a tildes
48
+ return {_normalize_caseless(word) for word in combined}
49
 
50
+ def process_text(text: str, nlp, lang_code: str) -> List[str]:
51
  """
52
+ NOMBRE PRESERVADO. Lógica mejorada: usa normalización para que 'Análisis'
53
+ sea filtrado si 'analisis' está en la lista de stopwords.
 
 
 
 
 
 
 
54
  """
55
  try:
56
+ if not text: return []
 
 
 
57
  doc = nlp(text)
58
+ stopwords = get_stopwords_for_spacy(lang_code, nlp)
 
59
  processed_tokens = []
60
+
61
  for token in doc:
 
62
  lemma = token.lemma_.lower()
63
+ # Normalizamos el lema para la comparación
64
+ norm_lemma = _normalize_caseless(lemma)
65
 
66
+ if (norm_lemma not in stopwords and
67
+ not token.is_punct and
68
+ not token.is_space and
69
+ lemma not in SYMBOLS_AND_NUMBERS and
70
+ len(norm_lemma) > 1):
 
 
 
 
71
  processed_tokens.append(lemma)
 
72
  return processed_tokens
 
73
  except Exception as e:
74
  logger.error(f"Error en process_text: {str(e)}")
75
  return []
76
 
77
  def clean_text(text: str) -> str:
78
+ """NOMBRE PRESERVADO. Limpia espacios y símbolos manteniendo caracteres latinos."""
79
+ if not text: return ""
 
 
 
 
 
 
 
 
80
  cleaned = ''.join(char for char in text if char not in SYMBOLS_AND_NUMBERS)
81
+ return ' '.join(cleaned.split()).strip()