Rhulli commited on
Commit
658fb5e
·
verified ·
1 Parent(s): 61cdbf6

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.txt +61 -0
  2. app.py +235 -0
  3. requirements.txt +11 -0
README.txt ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Reconocimiento y Normalización de Expresiones Temporales (TIMEX3)-
3
+
4
+ **Demo online** para detectar y normalizar expresiones temporales en textos, usando modelos de lenguaje (LLMs) personalizados.
5
+
6
+ ## 🚀 ¿Qué hace este Space?
7
+
8
+ * **Reconoce expresiones temporales** en texto libre o archivos (.txt y .pdf).
9
+ * **Normaliza las expresiones** al estándar [TIMEX3](https://timeml.org/site/publications/timeMLdocs/timex3-technical-spec.pdf).
10
+ * Permite **descargar los resultados** como archivo CSV.
11
+ * Procesamiento rápido y seguro (limite: 8.000 caracteres por entrada).
12
+
13
+ ## 🧑‍💻 ¿Cómo usarlo?
14
+
15
+ 1. **Introduce un texto** en la pestaña “Texto” o **sube un archivo** `.txt` o `.pdf` en la pestaña “Archivo”.
16
+ 2. Selecciona la fecha de refenrencia del texto
17
+ 3. Haz clic en **“Procesar”**.
18
+ 4. Visualiza el resultado en una tabla con:
19
+
20
+ * Columna 1: Expresión temporal detectada
21
+ * Columna 2: Normalización TIMEX3
22
+ 5. Descarga el resultado como CSV si lo deseas.
23
+
24
+ ## 🛠️ Modelos usados
25
+
26
+ * **NER temporal**: [`Rhulli/Roberta-ner-temporal-expresions-secondtrain`](https://huggingface.co/Rhulli/Roberta-ner-temporal-expresions-secondtrain)
27
+ * **Normalizador TIMEX3**: [`Rhulli/gemma-2b-it-TIMEX3`](https://huggingface.co/Rhulli/gemma-2b-it-TIMEX3)
28
+
29
+ ## 📄 Ejemplo de uso
30
+
31
+ Texto de entrada:
32
+
33
+ > "El concierto es el 14 de julio de 2025. La garantía dura dos años."
34
+
35
+ | Expresión temporal | Normalización TIMEX3 |
36
+ | ------------------- | --------------------------------------------------------------------- |
37
+ | 14 de julio de 2025 | \[TIMEX3 type='DATE' value='2025-07-14']14 de julio de 2025\[/TIMEX3] |
38
+ | dos años | \[TIMEX3 type='DURATION' value='P2Y']dos años\[/TIMEX3] |
39
+
40
+ ## 📦 Requisitos técnicos
41
+
42
+ * [Gradio](https://gradio.app/)
43
+ * [Transformers](https://huggingface.co/docs/transformers/index)
44
+ * [PyPDF2](https://pypdf2.readthedocs.io/en/latest/)
45
+ * [Torch](https://pytorch.org/)
46
+
47
+ (Ver `requirements.txt` para detalles)
48
+
49
+ ## ⚠️ Notas
50
+
51
+ * No subas información sensible. Los archivos y textos enviados pueden ser procesados temporalmente en el servidor.
52
+ * Límite de texto: 8.000 caracteres por entrada.
53
+ * Idioma recomendado, Inglés.
54
+
55
+ ## 👨‍🎓 Autor
56
+
57
+ Desarrollado por Raúl Moreno Mejías como parte de un proyecto de investigación en reconocimiento y normalización de expresiones temporales con LLMs.
58
+
59
+ ---
60
+
61
+
app.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import unicodedata
3
+ import io
4
+ import torch
5
+ import gradio as gr
6
+ import pdfplumber
7
+ import pandas as pd
8
+ from transformers import (
9
+ AutoTokenizer,
10
+ AutoModelForTokenClassification,
11
+ AutoModelForCausalLM,
12
+ BitsAndBytesConfig,
13
+ )
14
+ from peft import PeftModel
15
+
16
+ # --- Funciones de normalización y limpieza ---
17
+ _SPACE_VARIANTS = r"[\u202f\u00a0\u2009\u200a\u2060]"
18
+
19
+ def _normalise_apostrophes(text: str) -> str:
20
+ return text.replace("´", "'").replace("’", "'")
21
+
22
+ def _normalise_spaces(text: str, collapse: bool = True) -> str:
23
+ text = re.sub(_SPACE_VARIANTS, " ", text)
24
+ text = unicodedata.normalize("NFKC", text)
25
+ if collapse:
26
+ text = re.sub(r"[ ]{2,}", " ", text)
27
+ return text.strip()
28
+
29
+ def _clean_timex(ent: str) -> str:
30
+ ent = ent.replace("</s>", "").strip()
31
+ return re.sub(r"[\.]+$", "", ent)
32
+
33
+ # --- Identificadores de los modelos ---
34
+ NER_ID = "Rhulli/Roberta-ner-temporal-expresions-secondtrain"
35
+ ID2LABEL = {0: "O", 1: "B-TIMEX", 2: "I-TIMEX"}
36
+ BASE_ID = "google/gemma-2b-it"
37
+ ADAPTER_ID = "Rhulli/gemma-2b-it-TIMEX3"
38
+
39
+ # --- Configuración de cuantización para el modelo de normalización ---
40
+ quant_config = BitsAndBytesConfig(
41
+ load_in_4bit=True,
42
+ bnb_4bit_quant_type="nf4",
43
+ bnb_4bit_compute_dtype=torch.float16,
44
+ )
45
+
46
+ def load_models():
47
+ # Carga del modelo NER
48
+ ner_tok = AutoTokenizer.from_pretrained(NER_ID)
49
+ ner_mod = AutoModelForTokenClassification.from_pretrained(NER_ID)
50
+ ner_mod.eval()
51
+ if torch.cuda.is_available():
52
+ ner_mod.to("cuda")
53
+
54
+ # Carga del modelo de normalización (LoRA + 4bit)
55
+ base_mod = AutoModelForCausalLM.from_pretrained(
56
+ BASE_ID,
57
+ quantization_config=quant_config,
58
+ device_map="auto"
59
+ )
60
+ norm_tok = AutoTokenizer.from_pretrained(ADAPTER_ID, use_fast=True)
61
+ norm_mod = PeftModel.from_pretrained(
62
+ base_mod,
63
+ ADAPTER_ID,
64
+ device_map="auto"
65
+ )
66
+ norm_mod.eval()
67
+
68
+ return ner_tok, ner_mod, norm_tok, norm_mod
69
+
70
+ # Carga inicial de los modelos
71
+ ner_tok, ner_mod, norm_tok, norm_mod = load_models()
72
+ eos_id = norm_tok.convert_tokens_to_ids("<end_of_turn>")
73
+
74
+ # --- Lectura de archivos ---
75
+ def read_file(file_obj) -> str:
76
+ """
77
+ Lee contenido de un archivo (.txt o .pdf) usando su ruta file_obj.name.
78
+ """
79
+ path = file_obj.name
80
+ if path.lower().endswith('.pdf'):
81
+ full = ''
82
+ with pdfplumber.open(path) as pdf:
83
+ for page in pdf.pages:
84
+ txt = page.extract_text()
85
+ if txt:
86
+ full += txt + '\n'
87
+ return full
88
+ else:
89
+ with open(path, 'rb') as f:
90
+ data = f.read()
91
+ try:
92
+ return data.decode('utf-8')
93
+ except:
94
+ return data.decode('latin-1', errors='ignore')
95
+
96
+ # --- Procesamiento de texto ---
97
+ def extract_timex(text: str):
98
+ text_norm = _normalise_spaces(_normalise_apostrophes(text))
99
+ inputs = ner_tok(text_norm, return_tensors="pt", truncation=True)
100
+ if torch.cuda.is_available():
101
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
102
+ with torch.no_grad():
103
+ logits = ner_mod(**inputs).logits
104
+
105
+ preds = torch.argmax(logits, dim=2)[0].cpu().numpy()
106
+ tokens = ner_tok.convert_ids_to_tokens(inputs["input_ids"][0])
107
+
108
+ entities = []
109
+ current = []
110
+ for tok, lab in zip(tokens, preds):
111
+ tag = ID2LABEL.get(lab, "O")
112
+ if tag == "B-TIMEX":
113
+ if current:
114
+ entities.append(ner_tok.convert_tokens_to_string(current).strip())
115
+ current = [tok]
116
+ elif tag == "I-TIMEX" and current:
117
+ current.append(tok)
118
+ else:
119
+ if current:
120
+ entities.append(ner_tok.convert_tokens_to_string(current).strip())
121
+ current = []
122
+ if current:
123
+ entities.append(ner_tok.convert_tokens_to_string(current).strip())
124
+
125
+ return [_clean_timex(e) for e in entities]
126
+
127
+ def normalize_timex(expr: str, dct: str) -> str:
128
+ prompt = (
129
+ f"<start_of_turn>user\n"
130
+ f"Tu tarea es normalizar la expresión temporal al formato TIMEX3, utilizando la fecha de anclaje (DCT) cuando sea necesaria.\n"
131
+ f"Fecha de Anclaje (DCT): {dct}\n"
132
+ f"Expresión Original: {expr}<end_of_turn>\n"
133
+ f"<start_of_turn>model\n"
134
+ )
135
+ inputs = norm_tok(prompt, return_tensors="pt").to(norm_mod.device)
136
+ outputs = norm_mod.generate(**inputs, max_new_tokens=64, eos_token_id=eos_id)
137
+
138
+ full_decoded = norm_tok.decode(
139
+ outputs[0, inputs.input_ids.shape[1]:],
140
+ skip_special_tokens=False
141
+ )
142
+ raw_tag = full_decoded.split("<end_of_turn>")[0].strip()
143
+ # Reemplazar corchetes por angulares
144
+ return raw_tag.replace("[", "<").replace("]", ">")
145
+
146
+ # --- Pipeline principal ---
147
+ def run_pipeline(files, raw_text, dct):
148
+ rows = []
149
+ file_list = files if isinstance(files, list) else ([files] if files else [])
150
+
151
+ # Procesar texto libre
152
+ if raw_text:
153
+ for line in raw_text.splitlines():
154
+ if line.strip():
155
+ for expr in extract_timex(line):
156
+ rows.append({
157
+ 'Expresión': expr,
158
+ 'Normalización': normalize_timex(expr, dct)
159
+ })
160
+
161
+ # Procesar archivos
162
+ for f in file_list:
163
+ content = read_file(f)
164
+ for line in content.splitlines():
165
+ if line.strip():
166
+ for expr in extract_timex(line):
167
+ rows.append({
168
+ 'Expresión': expr,
169
+ 'Normalización': normalize_timex(expr, dct)
170
+ })
171
+
172
+ df = pd.DataFrame(rows)
173
+ if df.empty:
174
+ df = pd.DataFrame([], columns=['Expresión', 'Normalización'])
175
+
176
+ # Devolver dos valores: la tabla y un string vacío para logs
177
+ return df, ""
178
+
179
+ # --- Interfaz Gradio ---
180
+ with gr.Blocks() as demo:
181
+ gr.Markdown(
182
+ """
183
+ ## TIMEX Extractor & Normalizer
184
+
185
+ Esta aplicación permite extraer expresiones temporales de textos o archivos (.txt, .pdf)
186
+ y normalizarlas a formato TIMEX3.
187
+
188
+ **Cómo usar:**
189
+ - Sube uno o varios archivos en la columna izquierda.
190
+ - Ajusta la *Fecha de Anclaje (DCT)* justo debajo de los archivos.
191
+ - Escribe o pega tu texto en la columna derecha.
192
+ - Pulsa **Procesar** para ver los resultados en la tabla debajo.
193
+
194
+ **Columnas de salida:**
195
+ - *Expresión*: la frase temporal extraída.
196
+ - *Normalización*: la etiqueta TIMEX3 generada (con `< >`).
197
+ """
198
+ )
199
+
200
+ with gr.Row():
201
+ with gr.Column(scale=1):
202
+ files = gr.File(
203
+ file_types=['.txt', '.pdf'],
204
+ file_count='multiple',
205
+ label='Archivos (.txt, .pdf)'
206
+ )
207
+ dct_input = gr.Textbox(
208
+ value="2025-06-11",
209
+ label="Fecha de Anclaje (YYYY-MM-DD)"
210
+ )
211
+ run_btn = gr.Button("Procesar")
212
+ with gr.Column(scale=2):
213
+ raw_text = gr.Textbox(
214
+ lines=15,
215
+ placeholder='Pega o escribe aquí tu texto... (opcional si subes archivos)',
216
+ label='Texto libre'
217
+ )
218
+
219
+ output_table = gr.Dataframe(
220
+ headers=['Expresión', 'Normalización'],
221
+ label="Resultados"
222
+ )
223
+ output_logs = gr.Textbox(
224
+ label="Logs",
225
+ lines=5,
226
+ interactive=False
227
+ )
228
+
229
+ run_btn.click(
230
+ fn=run_pipeline,
231
+ inputs=[files, raw_text, dct_input],
232
+ outputs=[output_table, output_logs]
233
+ )
234
+
235
+ demo.launch(debug=True, server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ transformers
2
+ accelerate
3
+ peft
4
+ bitsandbytes
5
+ datasets
6
+ sentencepiece
7
+ scikit-learn
8
+ torch
9
+ pdfplumber
10
+ pandas
11
+ gradio