alezsd commited on
Commit
f1bbe92
Β·
1 Parent(s): 543dd1d

Initial deploy

Browse files
Files changed (4) hide show
  1. Dockerfile +32 -0
  2. README.md +60 -5
  3. api_ner.py +184 -0
  4. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Imagen base ───────────────────────────────────────────────────────────────
2
+ FROM python:3.10-slim
3
+
4
+ # HF Spaces expone el puerto 7860
5
+ ENV PORT=7860
6
+ ENV NER_MODEL=en_core_web_sm
7
+
8
+ # Directorio de trabajo
9
+ WORKDIR /app
10
+
11
+ # ── Dependencias del sistema ───────────────────────────────────────────────────
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential \
14
+ curl \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # ── Dependencias Python ────────────────────────────────────────────────────────
18
+ COPY requirements.txt .
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # ── Modelo spaCy ──────────────────────────────────────────────────────────────
22
+ # Cambia "en_core_web_sm" por tu modelo si usas uno propio.
23
+ # Si tienes un modelo local, cΓ³pialo con: COPY models/ ./models/
24
+ RUN python -m spacy download en_core_web_sm
25
+
26
+ # ── CΓ³digo de la app ──────────────────────────────────────────────────────────
27
+ COPY api_ner.py .
28
+
29
+ # ── Puerto y arranque ─────────────────────────────────────────────────────────
30
+ EXPOSE 7860
31
+
32
+ CMD ["uvicorn", "api_ner:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,65 @@
1
  ---
2
- title: Lancaster Ner Api
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: green
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Lancaster NER API
3
+ emoji: 🏷️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # Lancaster NER API
12
+
13
+ API REST construida con **FastAPI** y **spaCy** para detecciΓ³n de entidades nombradas (NER).
14
+
15
+ ## Endpoints
16
+
17
+ ### `GET /`
18
+ Health check β€” confirma que el modelo estΓ‘ cargado.
19
+
20
+ ### `POST /annotate`
21
+ Recibe un archivo `.txt` y devuelve las anotaciones NER.
22
+
23
+ **Ejemplo con curl:**
24
+ ```bash
25
+ curl -X POST "https://<tu-usuario>-lancaster-ner-api.hf.space/annotate" \
26
+ -F "file=@documento.txt"
27
+ ```
28
+
29
+ **Respuesta:**
30
+ ```json
31
+ {
32
+ "filename": "documento.txt",
33
+ "num_entities": 2,
34
+ "entities": [
35
+ {
36
+ "text": "London",
37
+ "label": "GPE",
38
+ "label_description": "Countries, cities, states",
39
+ "start": 5,
40
+ "end": 11
41
+ }
42
+ ],
43
+ "tokens": [...],
44
+ "html_visualization": "<div>...</div>"
45
+ }
46
+ ```
47
+
48
+ ### `POST /annotate/text`
49
+ Alternativa para enviar texto directamente como JSON.
50
+
51
+ ```bash
52
+ curl -X POST "https://<tu-usuario>-lancaster-ner-api.hf.space/annotate/text" \
53
+ -H "Content-Type: application/json" \
54
+ -d '{"text": "John lives in New York."}'
55
+ ```
56
+
57
+ ## DocumentaciΓ³n interactiva
58
+
59
+ Disponible en `/docs` (Swagger UI) y `/redoc`.
60
+
61
+ ## Modelo
62
+
63
+ Por defecto usa `en_core_web_sm` de spaCy. Para usar un modelo propio,
64
+ sube los archivos del modelo a la carpeta `models/` y ajusta la variable
65
+ de entorno `NER_MODEL` en la configuraciΓ³n del Space.
api_ner.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+ """
4
+ FastAPI endpoint para el proyecto lanc-ner
5
+ Recibe un archivo .txt y devuelve las anotaciones NER en formato JSON.
6
+
7
+ Uso:
8
+ uvicorn api_ner:app --host 0.0.0.0 --port 8000 --reload
9
+
10
+ Endpoint:
11
+ POST /annotate
12
+ - Body: multipart/form-data con campo "file" (archivo .txt)
13
+ - Respuesta: JSON con anotaciones NER
14
+ """
15
+
16
+ from fastapi import FastAPI, File, UploadFile, HTTPException
17
+ from fastapi.responses import JSONResponse
18
+ import spacy
19
+ from spacy import displacy
20
+ import os
21
+ import logging
22
+ from typing import Optional
23
+
24
+ # ── Logging ──────────────────────────────────────────────────────────────────
25
+ logging.basicConfig(level=logging.INFO)
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # ── App ───────────────────────────────────────────────────────────────────────
29
+ app = FastAPI(
30
+ title="Lancaster NER API",
31
+ description="Recibe un documento .txt y devuelve sus anotaciones NER.",
32
+ version="1.0.0",
33
+ )
34
+
35
+ # ── Carga del modelo spaCy ────────────────────────────────────────────────────
36
+ # Ajusta el nombre del modelo al que uses en tu proyecto (lanc-ner).
37
+ # Si tienes un modelo entrenado propio en ./models/, cΓ‘mbialo aquΓ­:
38
+ # NLP_MODEL_PATH = "./models/mi_modelo"
39
+ NLP_MODEL_PATH = os.getenv("NER_MODEL", "en_core_web_sm")
40
+
41
+ try:
42
+ nlp = spacy.load(NLP_MODEL_PATH)
43
+ logger.info(f"Modelo cargado: {NLP_MODEL_PATH}")
44
+ except OSError:
45
+ logger.error(
46
+ f"No se encontrΓ³ el modelo '{NLP_MODEL_PATH}'. "
47
+ "Ejecuta: python -m spacy download en_core_web_sm"
48
+ )
49
+ nlp = None
50
+
51
+
52
+ # ── FunciΓ³n NER (misma lΓ³gica que views/NER.py del repo) ─────────────────────
53
+ def run_ner(text: str) -> dict:
54
+ """
55
+ Aplica el pipeline NER de spaCy sobre el texto y devuelve
56
+ un diccionario con:
57
+ - entities : lista de entidades detectadas
58
+ - tokens : lista de todos los tokens con sus etiquetas IOB
59
+ - html : visualizaciΓ³n HTML (displaCy) β€” opcional
60
+ """
61
+ if nlp is None:
62
+ raise RuntimeError("El modelo NLP no estΓ‘ disponible.")
63
+
64
+ doc = nlp(text)
65
+
66
+ # Entidades detectadas
67
+ entities = [
68
+ {
69
+ "text": ent.text,
70
+ "label": ent.label_,
71
+ "label_description": spacy.explain(ent.label_) or "",
72
+ "start": ent.start_char,
73
+ "end": ent.end_char,
74
+ }
75
+ for ent in doc.ents
76
+ ]
77
+
78
+ # Tokens con etiquetas IOB
79
+ tokens = [
80
+ {
81
+ "token": token.text,
82
+ "pos": token.pos_,
83
+ "tag": token.tag_,
84
+ "dep": token.dep_,
85
+ "ent_iob": token.ent_iob_,
86
+ "ent_type": token.ent_type_,
87
+ }
88
+ for token in doc
89
+ ]
90
+
91
+ # VisualizaciΓ³n HTML de displaCy (ΓΊtil para depuraciΓ³n/front)
92
+ html = displacy.render(doc, style="ent", page=False, minify=True)
93
+
94
+ return {
95
+ "num_entities": len(entities),
96
+ "entities": entities,
97
+ "tokens": tokens,
98
+ "html_visualization": html,
99
+ }
100
+
101
+
102
+ # ── Endpoints ─────────────────────────────────────────────────────────────────
103
+
104
+ @app.get("/", tags=["Health"])
105
+ def health_check():
106
+ """Comprueba que la API estΓ‘ en marcha."""
107
+ model_status = "loaded" if nlp is not None else "not loaded"
108
+ return {"status": "ok", "model": NLP_MODEL_PATH, "model_status": model_status}
109
+
110
+
111
+ @app.post("/annotate", tags=["NER"])
112
+ async def annotate(file: UploadFile = File(..., description="Archivo .txt a anotar")):
113
+ """
114
+ Recibe un archivo de texto plano (.txt) y devuelve las anotaciones
115
+ NER producidas por el modelo spaCy configurado en el proyecto.
116
+
117
+ **Respuesta JSON:**
118
+ ```json
119
+ {
120
+ "filename": "documento.txt",
121
+ "num_entities": 3,
122
+ "entities": [
123
+ {"text": "London", "label": "GPE", "label_description": "Countries, cities, states",
124
+ "start": 5, "end": 11}
125
+ ],
126
+ "tokens": [...],
127
+ "html_visualization": "<div>...</div>"
128
+ }
129
+ ```
130
+ """
131
+ # ValidaciΓ³n del tipo de archivo
132
+ if not file.filename.lower().endswith(".txt"):
133
+ raise HTTPException(
134
+ status_code=415,
135
+ detail="Solo se aceptan archivos .txt",
136
+ )
137
+
138
+ # Lectura del contenido
139
+ try:
140
+ content_bytes = await file.read()
141
+ text = content_bytes.decode("utf-8")
142
+ except UnicodeDecodeError:
143
+ raise HTTPException(
144
+ status_code=422,
145
+ detail="No se pudo decodificar el archivo como UTF-8.",
146
+ )
147
+
148
+ if not text.strip():
149
+ raise HTTPException(
150
+ status_code=422,
151
+ detail="El archivo estΓ‘ vacΓ­o.",
152
+ )
153
+
154
+ # NER
155
+ try:
156
+ result = run_ner(text)
157
+ except RuntimeError as e:
158
+ raise HTTPException(status_code=503, detail=str(e))
159
+
160
+ return JSONResponse(
161
+ content={"filename": file.filename, **result}
162
+ )
163
+
164
+
165
+ @app.post("/annotate/text", tags=["NER"])
166
+ async def annotate_text(payload: dict):
167
+ """
168
+ Alternativa: envΓ­a el texto directamente como JSON.
169
+
170
+ **Body:**
171
+ ```json
172
+ { "text": "John lives in New York." }
173
+ ```
174
+ """
175
+ text = payload.get("text", "").strip()
176
+ if not text:
177
+ raise HTTPException(status_code=422, detail="El campo 'text' estΓ‘ vacΓ­o.")
178
+
179
+ try:
180
+ result = run_ner(text)
181
+ except RuntimeError as e:
182
+ raise HTTPException(status_code=503, detail=str(e))
183
+
184
+ return JSONResponse(content=result)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn[standard]==0.29.0
3
+ python-multipart==0.0.9
4
+ spacy==3.7.4