ttzzs commited on
Commit
69b5a3f
·
verified ·
1 Parent(s): 36a7e49

Initial deployment: Chronos-2 Excel Forecasting API

Browse files
Files changed (7) hide show
  1. .gitignore +6 -0
  2. Dockerfile +42 -0
  3. INSTRUCTIONS.md +35 -0
  4. README.md +189 -4
  5. app/__init__.py +0 -0
  6. app/main.py +395 -0
  7. requirements.txt +7 -0
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .venv/
5
+ *.log
6
+ .cache/
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile optimizado para HuggingFace Spaces
2
+ FROM python:3.11-slim
3
+
4
+ # Variables de entorno
5
+ ENV PYTHONUNBUFFERED=1 \
6
+ PYTHONDONTWRITEBYTECODE=1 \
7
+ PORT=7860
8
+
9
+ # Directorio de trabajo
10
+ WORKDIR /app
11
+
12
+ # Instalar dependencias del sistema
13
+ RUN apt-get update && \
14
+ apt-get install -y --no-install-recommends \
15
+ build-essential \
16
+ curl \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # Copiar requirements
20
+ COPY requirements.txt .
21
+
22
+ # Instalar dependencias Python
23
+ RUN pip install --no-cache-dir --upgrade pip && \
24
+ pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copiar código de la aplicación
27
+ COPY app/ ./app/
28
+
29
+ # Crear usuario no-root
30
+ RUN useradd -m -u 1000 user && \
31
+ chown -R user:user /app
32
+ USER user
33
+
34
+ # Exponer puerto (HF Spaces usa 7860 por defecto)
35
+ EXPOSE 7860
36
+
37
+ # Health check
38
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
39
+ CMD curl -f http://localhost:7860/health || exit 1
40
+
41
+ # Comando de inicio
42
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
INSTRUCTIONS.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Instrucciones de Despliegue
2
+
3
+ ## Archivos listos para subir a HuggingFace Spaces
4
+
5
+ Este directorio contiene todos los archivos necesarios para desplegar en HF Spaces.
6
+
7
+ ### Pasos:
8
+
9
+ 1. **Crear Space en HuggingFace**:
10
+ - Ve a: https://huggingface.co/new-space
11
+ - SDK: **Docker**
12
+ - Hardware: CPU basic (gratis)
13
+
14
+ 2. **Configurar HF_TOKEN**:
15
+ - Settings → Variables and secrets
16
+ - Nombre: `HF_TOKEN`
17
+ - Valor: tu token de HuggingFace
18
+ - Marcar como Secret
19
+
20
+ 3. **Subir archivos**:
21
+ ```bash
22
+ git clone https://huggingface.co/spaces/TU-USUARIO/TU-SPACE
23
+ cd TU-SPACE
24
+ cp -r ../hf_space_ready/* .
25
+ git add .
26
+ git commit -m "Initial deployment"
27
+ git push
28
+ ```
29
+
30
+ 4. **Verificar**:
31
+ ```bash
32
+ curl https://TU-USUARIO-TU-SPACE.hf.space/health
33
+ ```
34
+
35
+ ¡Listo! 🚀
README.md CHANGED
@@ -1,10 +1,195 @@
1
  ---
2
- title: Chronos2 Excel Forecasting Api
3
- emoji: 💻
4
  colorFrom: blue
5
- colorTo: pink
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: Chronos2 Excel Forecasting API
3
+ emoji: 📊
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # 📊 Chronos2 Excel Forecasting API
13
+
14
+ API de pronósticos con IA para Microsoft Excel usando [Amazon Chronos-2](https://huggingface.co/amazon/chronos-t5-large).
15
+
16
+ 🔗 **Úsalo directamente desde Excel** con nuestro Office Add-in
17
+
18
+ ## 🚀 Características
19
+
20
+ - ✅ **Pronósticos univariados**: Series temporales simples
21
+ - ✅ **Detección de anomalías**: Identifica valores atípicos automáticamente
22
+ - ✅ **Backtesting**: Valida la precisión de tus modelos
23
+ - ✅ **API REST con FastAPI**: Fácil integración
24
+ - ✅ **Documentación interactiva**: Swagger UI incluido
25
+
26
+ ## 📖 Documentación
27
+
28
+ Accede a la documentación interactiva:
29
+ - **Swagger UI**: `/docs`
30
+ - **ReDoc**: `/redoc`
31
+ - **Health Check**: `/health`
32
+
33
+ ## 🧪 Prueba Rápida
34
+
35
+ ### Pronóstico Simple
36
+
37
+ ```bash
38
+ curl -X POST https://YOUR-USERNAME-chronos2-excel-forecasting-api.hf.space/forecast_univariate \
39
+ -H "Content-Type: application/json" \
40
+ -d '{
41
+ "series": {"values": [100, 102, 105, 103, 108, 112, 115]},
42
+ "prediction_length": 3,
43
+ "freq": "D"
44
+ }'
45
+ ```
46
+
47
+ **Respuesta esperada:**
48
+ ```json
49
+ {
50
+ "timestamps": ["t+1", "t+2", "t+3"],
51
+ "median": [117.5, 119.2, 121.0],
52
+ "quantiles": {
53
+ "0.1": [112.3, 113.8, 115.5],
54
+ "0.5": [117.5, 119.2, 121.0],
55
+ "0.9": [122.7, 124.6, 126.5]
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Detección de Anomalías
61
+
62
+ ```bash
63
+ curl -X POST https://YOUR-USERNAME-chronos2-excel-forecasting-api.hf.space/detect_anomalies \
64
+ -H "Content-Type: application/json" \
65
+ -d '{
66
+ "context": {"values": [100, 102, 105, 103, 108]},
67
+ "recent_observed": [107, 200, 106],
68
+ "prediction_length": 3
69
+ }'
70
+ ```
71
+
72
+ ### Backtesting
73
+
74
+ ```bash
75
+ curl -X POST https://YOUR-USERNAME-chronos2-excel-forecasting-api.hf.space/backtest_simple \
76
+ -H "Content-Type: application/json" \
77
+ -d '{
78
+ "series": {"values": [100, 102, 105, 103, 108, 112, 115, 118, 120, 122, 125, 128]},
79
+ "prediction_length": 7,
80
+ "test_length": 4
81
+ }'
82
+ ```
83
+
84
+ ## 🔗 Endpoints Disponibles
85
+
86
+ | Endpoint | Método | Descripción |
87
+ |----------|--------|-------------|
88
+ | `/` | GET | Información de la API |
89
+ | `/health` | GET | Health check del servicio |
90
+ | `/docs` | GET | Documentación Swagger |
91
+ | `/forecast_univariate` | POST | Pronóstico de serie simple |
92
+ | `/detect_anomalies` | POST | Detectar valores atípicos |
93
+ | `/backtest_simple` | POST | Validar precisión del modelo |
94
+ | `/simple_forecast` | POST | Pronóstico rápido (testing) |
95
+
96
+ ## 💻 Uso con Excel
97
+
98
+ Este API funciona perfectamente con nuestro **Office Add-in para Excel**:
99
+
100
+ 1. Descarga el Add-in desde [GitHub](https://github.com/tu-usuario/chronos2-server)
101
+ 2. Configura la URL de este Space en el Add-in
102
+ 3. ¡Realiza pronósticos directamente desde tus hojas de cálculo!
103
+
104
+ ### Ejemplo en Excel
105
+
106
+ ```javascript
107
+ // En el Excel Add-in, configura:
108
+ const API_BASE_URL = 'https://YOUR-USERNAME-chronos2-excel-forecasting-api.hf.space';
109
+ ```
110
+
111
+ ## 🛠️ Tecnologías
112
+
113
+ - **Modelo**: [Amazon Chronos-2 T5-Large](https://huggingface.co/amazon/chronos-t5-large)
114
+ - **Framework**: [FastAPI](https://fastapi.tiangolo.com/)
115
+ - **Inference**: [Hugging Face Inference API](https://huggingface.co/docs/api-inference)
116
+ - **Deployment**: Hugging Face Spaces (Docker)
117
+
118
+ ## 📊 Casos de Uso
119
+
120
+ - 📈 **Ventas**: Predice demanda futura de productos
121
+ - 💰 **Finanzas**: Proyecta ingresos y gastos
122
+ - 📦 **Inventario**: Optimiza stock y reposición
123
+ - 🌡️ **Sensores**: Anticipa valores de sensores IoT
124
+ - 🏪 **Retail**: Planifica recursos y personal
125
+
126
+ ## ⚙️ Configuración
127
+
128
+ ### Variables de Entorno
129
+
130
+ Para desplegar tu propia instancia, configura:
131
+
132
+ - `HF_TOKEN`: Tu token de Hugging Face (requerido)
133
+ - `CHRONOS_MODEL_ID`: ID del modelo (default: `amazon/chronos-t5-large`)
134
+ - `PORT`: Puerto del servidor (default: `7860`)
135
+
136
+ ### Crear tu propio Space
137
+
138
+ 1. Fork este repositorio
139
+ 2. Crea un nuevo Space en Hugging Face
140
+ 3. Selecciona **Docker** como SDK
141
+ 4. Conecta tu repositorio
142
+ 5. Configura `HF_TOKEN` en los Secrets del Space
143
+ 6. ¡Listo!
144
+
145
+ ## 🔒 Seguridad
146
+
147
+ - ✅ CORS configurado para orígenes permitidos
148
+ - ✅ Validación de entrada con Pydantic
149
+ - ✅ Rate limiting en HuggingFace Inference API
150
+ - ✅ Timeouts configurados para evitar bloqueos
151
+
152
+ ## 📚 Recursos
153
+
154
+ - [Documentación de Chronos-2](https://huggingface.co/amazon/chronos-t5-large)
155
+ - [API de HuggingFace Inference](https://huggingface.co/docs/api-inference)
156
+ - [FastAPI Docs](https://fastapi.tiangolo.com/)
157
+ - [Tutorial de Office Add-ins](https://docs.microsoft.com/en-us/office/dev/add-ins/)
158
+
159
+ ## 🐛 Solución de Problemas
160
+
161
+ ### "Model is loading"
162
+
163
+ La primera request puede tardar 30-60 segundos mientras el modelo se carga. Reintenta después.
164
+
165
+ ### "HF_TOKEN not configured"
166
+
167
+ Asegúrate de configurar `HF_TOKEN` en los Secrets de tu Space.
168
+
169
+ ### Errores de timeout
170
+
171
+ El modelo puede estar frío. Espera unos segundos y reintenta.
172
+
173
+ ## 📝 Licencia
174
+
175
+ MIT License - Ver [LICENSE](LICENSE) para más detalles.
176
+
177
+ ## 🤝 Contribuir
178
+
179
+ ¿Quieres mejorar este proyecto?
180
+
181
+ 1. Fork el repositorio
182
+ 2. Crea una branch para tu feature (`git checkout -b feature/amazing`)
183
+ 3. Commit tus cambios (`git commit -m 'Add amazing feature'`)
184
+ 4. Push a la branch (`git push origin feature/amazing`)
185
+ 5. Abre un Pull Request
186
+
187
+ ## 📧 Contacto
188
+
189
+ ¿Preguntas o sugerencias? Abre un [issue en GitHub](https://github.com/tu-usuario/chronos2-server/issues).
190
+
191
+ ---
192
+
193
+ **Desarrollado con ❤️ usando [Chronos-2](https://huggingface.co/amazon/chronos-t5-large) y [FastAPI](https://fastapi.tiangolo.com/)**
194
+
195
+ 🌟 Si te gusta este proyecto, ¡dale una estrella en [GitHub](https://github.com/tu-usuario/chronos2-server)!
app/__init__.py ADDED
File without changes
app/main.py ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List, Dict, Optional
3
+ import json
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ from fastapi import FastAPI, HTTPException
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from pydantic import BaseModel, Field
10
+ from huggingface_hub import InferenceClient
11
+
12
+
13
+ # =========================
14
+ # Configuración
15
+ # =========================
16
+
17
+ HF_TOKEN = os.getenv("HF_TOKEN")
18
+ MODEL_ID = os.getenv("CHRONOS_MODEL_ID", "amazon/chronos-t5-large")
19
+
20
+ app = FastAPI(
21
+ title="Chronos-2 Forecasting API (HF Inference)",
22
+ description=(
23
+ "API de pronósticos usando Chronos-2 via Hugging Face Inference API. "
24
+ "Compatible con Excel Add-in."
25
+ ),
26
+ version="1.0.0",
27
+ )
28
+
29
+ # Configurar CORS
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"], # En producción, especificar dominios permitidos
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ # Cliente de HF Inference
39
+ if not HF_TOKEN:
40
+ print("⚠️ WARNING: HF_TOKEN no configurado. La API puede no funcionar correctamente.")
41
+ print(" Configura HF_TOKEN en las variables de entorno del Space.")
42
+ client = None
43
+ else:
44
+ client = InferenceClient(token=HF_TOKEN)
45
+
46
+
47
+ # =========================
48
+ # Modelos Pydantic
49
+ # =========================
50
+
51
+ class UnivariateSeries(BaseModel):
52
+ values: List[float]
53
+
54
+
55
+ class ForecastUnivariateRequest(BaseModel):
56
+ series: UnivariateSeries
57
+ prediction_length: int = Field(7, description="Número de pasos a predecir")
58
+ quantile_levels: Optional[List[float]] = Field(
59
+ default=[0.1, 0.5, 0.9],
60
+ description="Cuantiles para intervalos de confianza"
61
+ )
62
+ freq: str = Field("D", description="Frecuencia temporal (D, W, M, etc.)")
63
+
64
+
65
+ class ForecastUnivariateResponse(BaseModel):
66
+ timestamps: List[str]
67
+ median: List[float]
68
+ quantiles: Dict[str, List[float]]
69
+
70
+
71
+ class AnomalyDetectionRequest(BaseModel):
72
+ context: UnivariateSeries
73
+ recent_observed: List[float]
74
+ prediction_length: int = 7
75
+ quantile_low: float = 0.05
76
+ quantile_high: float = 0.95
77
+
78
+
79
+ class AnomalyPoint(BaseModel):
80
+ index: int
81
+ value: float
82
+ predicted_median: float
83
+ lower: float
84
+ upper: float
85
+ is_anomaly: bool
86
+
87
+
88
+ class AnomalyDetectionResponse(BaseModel):
89
+ anomalies: List[AnomalyPoint]
90
+
91
+
92
+ class BacktestRequest(BaseModel):
93
+ series: UnivariateSeries
94
+ prediction_length: int = 7
95
+ test_length: int = 28
96
+
97
+
98
+ class BacktestMetrics(BaseModel):
99
+ mae: float
100
+ mape: float
101
+ rmse: float
102
+
103
+
104
+ class BacktestResponse(BaseModel):
105
+ metrics: BacktestMetrics
106
+ forecast_median: List[float]
107
+ forecast_timestamps: List[str]
108
+ actuals: List[float]
109
+
110
+
111
+ # =========================
112
+ # Función auxiliar para llamar a HF Inference
113
+ # =========================
114
+
115
+ def call_chronos_inference(series: List[float], prediction_length: int) -> Dict:
116
+ """
117
+ Llama a la API de Hugging Face Inference para Chronos.
118
+ Retorna un diccionario con las predicciones.
119
+ """
120
+ if client is None:
121
+ raise HTTPException(
122
+ status_code=503,
123
+ detail="HF_TOKEN no configurado. Contacta al administrador del servicio."
124
+ )
125
+
126
+ try:
127
+ # Intentar usando el endpoint específico de time series
128
+ import requests
129
+
130
+ url = f"https://api-inference.huggingface.co/models/{MODEL_ID}"
131
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"}
132
+
133
+ payload = {
134
+ "inputs": series,
135
+ "parameters": {
136
+ "prediction_length": prediction_length,
137
+ "num_samples": 100 # Para obtener cuantiles
138
+ }
139
+ }
140
+
141
+ response = requests.post(url, headers=headers, json=payload, timeout=60)
142
+
143
+ if response.status_code == 503:
144
+ raise HTTPException(
145
+ status_code=503,
146
+ detail="El modelo está cargando. Por favor, intenta de nuevo en 30-60 segundos."
147
+ )
148
+ elif response.status_code != 200:
149
+ raise HTTPException(
150
+ status_code=response.status_code,
151
+ detail=f"Error de la API de HuggingFace: {response.text}"
152
+ )
153
+
154
+ result = response.json()
155
+ return result
156
+
157
+ except requests.exceptions.Timeout:
158
+ raise HTTPException(
159
+ status_code=504,
160
+ detail="Timeout al comunicarse con HuggingFace API. El modelo puede estar cargando."
161
+ )
162
+ except Exception as e:
163
+ raise HTTPException(
164
+ status_code=500,
165
+ detail=f"Error inesperado: {str(e)}"
166
+ )
167
+
168
+
169
+ def process_chronos_output(raw_output: Dict, prediction_length: int) -> Dict:
170
+ """
171
+ Procesa la salida de Chronos para extraer mediana y cuantiles.
172
+ """
173
+ # La API de Chronos puede devolver diferentes formatos
174
+ # Intentamos adaptarnos a ellos
175
+
176
+ if isinstance(raw_output, list):
177
+ # Si es una lista de valores, asumimos que es la predicción media
178
+ median = raw_output[:prediction_length]
179
+ return {
180
+ "median": median,
181
+ "quantiles": {
182
+ "0.1": median, # Sin cuantiles, usar median
183
+ "0.5": median,
184
+ "0.9": median
185
+ }
186
+ }
187
+
188
+ # Si tiene estructura más compleja, intentar extraer
189
+ if "forecast" in raw_output:
190
+ forecast = raw_output["forecast"]
191
+ if "median" in forecast:
192
+ median = forecast["median"][:prediction_length]
193
+ else:
194
+ median = forecast.get("mean", [0] * prediction_length)[:prediction_length]
195
+
196
+ quantiles = forecast.get("quantiles", {})
197
+ return {
198
+ "median": median,
199
+ "quantiles": quantiles
200
+ }
201
+
202
+ # Formato por defecto
203
+ return {
204
+ "median": [0] * prediction_length,
205
+ "quantiles": {
206
+ "0.1": [0] * prediction_length,
207
+ "0.5": [0] * prediction_length,
208
+ "0.9": [0] * prediction_length
209
+ }
210
+ }
211
+
212
+
213
+ # =========================
214
+ # Endpoints
215
+ # =========================
216
+
217
+ @app.get("/")
218
+ def root():
219
+ """Información básica de la API"""
220
+ return {
221
+ "name": "Chronos-2 Forecasting API",
222
+ "version": "1.0.0",
223
+ "model": MODEL_ID,
224
+ "status": "running",
225
+ "docs": "/docs",
226
+ "health": "/health"
227
+ }
228
+
229
+
230
+ @app.get("/health")
231
+ def health():
232
+ """Health check del servicio"""
233
+ return {
234
+ "status": "ok" if HF_TOKEN else "warning",
235
+ "model_id": MODEL_ID,
236
+ "hf_token_configured": HF_TOKEN is not None,
237
+ "message": "Ready" if HF_TOKEN else "HF_TOKEN not configured"
238
+ }
239
+
240
+
241
+ @app.post("/forecast_univariate", response_model=ForecastUnivariateResponse)
242
+ def forecast_univariate(req: ForecastUnivariateRequest):
243
+ """
244
+ Pronóstico para una serie temporal univariada.
245
+
246
+ Compatible con el Excel Add-in.
247
+ """
248
+ values = req.series.values
249
+ n = len(values)
250
+
251
+ if n == 0:
252
+ raise HTTPException(status_code=400, detail="La serie no puede estar vacía.")
253
+
254
+ if n < 3:
255
+ raise HTTPException(
256
+ status_code=400,
257
+ detail="La serie debe tener al menos 3 puntos históricos."
258
+ )
259
+
260
+ # Llamar a la API de HuggingFace
261
+ raw_output = call_chronos_inference(values, req.prediction_length)
262
+
263
+ # Procesar la salida
264
+ processed = process_chronos_output(raw_output, req.prediction_length)
265
+
266
+ # Generar timestamps
267
+ timestamps = [f"t+{i+1}" for i in range(req.prediction_length)]
268
+
269
+ return ForecastUnivariateResponse(
270
+ timestamps=timestamps,
271
+ median=processed["median"],
272
+ quantiles=processed["quantiles"]
273
+ )
274
+
275
+
276
+ @app.post("/detect_anomalies", response_model=AnomalyDetectionResponse)
277
+ def detect_anomalies(req: AnomalyDetectionRequest):
278
+ """
279
+ Detecta anomalías comparando valores observados con predicciones.
280
+ """
281
+ n_hist = len(req.context.values)
282
+
283
+ if n_hist == 0:
284
+ raise HTTPException(status_code=400, detail="El contexto no puede estar vacío.")
285
+
286
+ if len(req.recent_observed) != req.prediction_length:
287
+ raise HTTPException(
288
+ status_code=400,
289
+ detail="recent_observed debe tener la misma longitud que prediction_length."
290
+ )
291
+
292
+ # Hacer predicción
293
+ raw_output = call_chronos_inference(req.context.values, req.prediction_length)
294
+ processed = process_chronos_output(raw_output, req.prediction_length)
295
+
296
+ # Comparar con valores observados
297
+ anomalies: List[AnomalyPoint] = []
298
+
299
+ median = processed["median"]
300
+ # Intentar obtener cuantiles o usar aproximaciones
301
+ q_low = processed["quantiles"].get(str(req.quantile_low), median)
302
+ q_high = processed["quantiles"].get(str(req.quantile_high), median)
303
+
304
+ for i, obs in enumerate(req.recent_observed):
305
+ if i < len(median):
306
+ lower = q_low[i] if i < len(q_low) else median[i] * 0.8
307
+ upper = q_high[i] if i < len(q_high) else median[i] * 1.2
308
+ predicted = median[i]
309
+ is_anom = (obs < lower) or (obs > upper)
310
+
311
+ anomalies.append(
312
+ AnomalyPoint(
313
+ index=i,
314
+ value=obs,
315
+ predicted_median=predicted,
316
+ lower=lower,
317
+ upper=upper,
318
+ is_anomaly=is_anom,
319
+ )
320
+ )
321
+
322
+ return AnomalyDetectionResponse(anomalies=anomalies)
323
+
324
+
325
+ @app.post("/backtest_simple", response_model=BacktestResponse)
326
+ def backtest_simple(req: BacktestRequest):
327
+ """
328
+ Backtesting simple: divide la serie en train/test y evalúa métricas.
329
+ """
330
+ values = np.array(req.series.values, dtype=float)
331
+ n = len(values)
332
+
333
+ if n <= req.test_length:
334
+ raise HTTPException(
335
+ status_code=400,
336
+ detail="La serie debe ser más larga que test_length."
337
+ )
338
+
339
+ # Dividir en train/test
340
+ train = values[: n - req.test_length].tolist()
341
+ test = values[n - req.test_length :].tolist()
342
+
343
+ # Hacer predicción
344
+ raw_output = call_chronos_inference(train, req.test_length)
345
+ processed = process_chronos_output(raw_output, req.test_length)
346
+
347
+ forecast = np.array(processed["median"], dtype=float)
348
+ test_arr = np.array(test, dtype=float)
349
+
350
+ # Calcular métricas
351
+ mae = float(np.mean(np.abs(test_arr - forecast)))
352
+ rmse = float(np.sqrt(np.mean((test_arr - forecast) ** 2)))
353
+
354
+ eps = 1e-8
355
+ mape = float(np.mean(np.abs((test_arr - forecast) / (test_arr + eps)))) * 100.0
356
+
357
+ timestamps = [f"test_t{i+1}" for i in range(req.test_length)]
358
+
359
+ metrics = BacktestMetrics(mae=mae, mape=mape, rmse=rmse)
360
+
361
+ return BacktestResponse(
362
+ metrics=metrics,
363
+ forecast_median=forecast.tolist(),
364
+ forecast_timestamps=timestamps,
365
+ actuals=test,
366
+ )
367
+
368
+
369
+ # =========================
370
+ # Endpoints simplificados para testing
371
+ # =========================
372
+
373
+ @app.post("/simple_forecast")
374
+ def simple_forecast(series: List[float], prediction_length: int = 7):
375
+ """
376
+ Endpoint simplificado para testing rápido.
377
+ """
378
+ if not series:
379
+ raise HTTPException(status_code=400, detail="Serie vacía")
380
+
381
+ raw_output = call_chronos_inference(series, prediction_length)
382
+ processed = process_chronos_output(raw_output, prediction_length)
383
+
384
+ return {
385
+ "input_series": series,
386
+ "prediction_length": prediction_length,
387
+ "forecast": processed["median"],
388
+ "model": MODEL_ID
389
+ }
390
+
391
+
392
+ if __name__ == "__main__":
393
+ import uvicorn
394
+ port = int(os.getenv("PORT", 7860))
395
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi>=0.104.0
2
+ uvicorn[standard]>=0.24.0
3
+ pandas>=2.0.0
4
+ numpy>=1.24.0
5
+ huggingface_hub>=0.20.0
6
+ pydantic>=2.0.0
7
+ python-dotenv>=1.0.0