YellowAlberto commited on
Commit
3687fa1
·
0 Parent(s):

Despliegue HF

Browse files
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ protection-environment-robot-holding-lightbulb-with-recycle-icon-sustainable-and-nature_10461459.jpg!sw800 filter=lfs diff=lfs merge=lfs -text
.github/workflows/sync_to_hf_yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ force_push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ sync-to-hub:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v3
13
+ with:
14
+ fetch-depth: 0
15
+ lfs: true
16
+ - name: Push to hub
17
+ env:
18
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
19
+ run: git push --force https://YellowAlberto:$HF_TOKEN@huggingface.co/spaces/YellowAlberto/ReciclA main
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+
3
+ test.db
4
+ sql_app.db
5
+
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ venv/
10
+ .venv/
11
+
12
+ imagenes_subidas/
13
+
14
+ *.png
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Instalamos las dependencias actualizadas para OpenCV y ONNX
4
+ RUN apt-get update && apt-get install -y \
5
+ libgl1 \
6
+ libglib2.0-0 \
7
+ libpq-dev \
8
+ gcc \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ WORKDIR /app
12
+
13
+
14
+ COPY requirements.txt .
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
+ COPY . .
17
+ RUN mkdir -p imagenes_subidas && chmod 777 imagenes_subidas
18
+ RUN chmod +x run.sh
19
+ EXPOSE 7860
20
+ CMD ["./run.sh"]
ProyectoBasura.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Depends, FastAPI, HTTPException, status, Request, File, UploadFile
2
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3
+ from pydantic import BaseModel
4
+ import jwt
5
+ from datetime import datetime, timedelta, timezone
6
+ import uvicorn
7
+ import modelsProyecto
8
+ from databaseProyecto import SessionLocal, engine
9
+ from sqlalchemy.orm import Session
10
+ import SeguridadProyecto
11
+ from SeguridadProyecto import generar_hash
12
+ from typing import Annotated
13
+ import os
14
+ import shutil
15
+ import io
16
+ import numpy as np
17
+ import onnxruntime as ort
18
+ from PIL import Image
19
+ from ultralytics import YOLO
20
+ from fastapi.responses import StreamingResponse
21
+ from dotenv import load_dotenv
22
+ from groq import Groq
23
+ from fastapi.middleware.cors import CORSMiddleware
24
+ load_dotenv()
25
+
26
+
27
+ model = YOLO("best.onnx", task="detect")
28
+
29
+ client_groq = Groq(api_key=os.getenv("GROQ_API_KEY"))
30
+
31
+ SECRET_KEY = os.getenv("SECRET_KEY")
32
+ ALGORITHM = os.getenv("ALGORITHM")
33
+
34
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
35
+ app = FastAPI()
36
+
37
+ app.add_middleware(
38
+ CORSMiddleware,
39
+ allow_origins=["*"],
40
+ allow_credentials=True,
41
+ allow_methods=["*"],
42
+ allow_headers=["*"],
43
+ )
44
+
45
+
46
+ modelsProyecto.Base.metadata.create_all(bind=engine)
47
+
48
+ def get_db():
49
+ db = SessionLocal()
50
+ try:
51
+ yield db
52
+ finally:
53
+ db.close()
54
+
55
+ class Token(BaseModel):
56
+ access_token: str
57
+ token_type: str
58
+
59
+ registros_mensajes= {}
60
+
61
+ @app.post("/login",summary="Login de usuario", description="Comprueba las credenciales y genera un token JWT de acceso", response_model=Token)
62
+ async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
63
+
64
+
65
+ verificar_frecuencia(form_data.username, segundos=10)
66
+
67
+ usuario_db = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == form_data.username).first()
68
+
69
+ if not usuario_db or not SeguridadProyecto.verificar_password(form_data.password, usuario_db.password_hash):
70
+ raise HTTPException(
71
+ status_code=status.HTTP_401_UNAUTHORIZED,
72
+ detail="ID de usuario o contraseña incorrectos",
73
+ headers={"WWW-Authenticate": "Bearer"},
74
+ )
75
+
76
+ expire = datetime.now(timezone.utc) + timedelta(minutes=30)
77
+ to_encode = {
78
+ "sub": usuario_db.username,
79
+ "exp": expire,
80
+ "rol": usuario_db.es_admin
81
+ }
82
+
83
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
84
+ return {"access_token": encoded_jwt, "token_type": "bearer"}
85
+
86
+ async def verificar_admin(token: str = Depends(oauth2_scheme)):
87
+ try:
88
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
89
+ username: str = payload.get("sub")
90
+ es_admin: bool = payload.get("rol")
91
+
92
+ except jwt.PyJWTError:
93
+ raise HTTPException(status_code=401, detail="Token inválido o expirado")
94
+
95
+ if es_admin is not True:
96
+ raise HTTPException(
97
+ status_code=status.HTTP_403_FORBIDDEN,
98
+ detail="No tienes permisos de administrador"
99
+ )
100
+ return {"username": username, "rol": es_admin}
101
+
102
+
103
+
104
+ @app.get("/revisarUsuarios",
105
+ summary="Listar usuarios (Solo Admin)",
106
+ description="Retorna una lista de todos los usuarios registrados. Requiere privilegios de administrador.")
107
+ async def revisar_Usuarios(admin_info: dict = Depends(verificar_admin), db: Session = Depends(get_db)):
108
+ usuarios = db.query(modelsProyecto.Usuario).all()
109
+ return usuarios
110
+
111
+ def verificar_frecuencia(usuario_actual: str, segundos: int = 5):
112
+ ahora = datetime.now()
113
+ if usuario_actual in registros_mensajes:
114
+ ultima_solicitud = registros_mensajes[usuario_actual]
115
+ tiempo_transcurrido = ahora - ultima_solicitud
116
+
117
+ if tiempo_transcurrido < timedelta(seconds=segundos):
118
+ segundos_restantes = segundos - int(tiempo_transcurrido.total_seconds())
119
+ raise HTTPException(
120
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
121
+ detail=f"Demasiado rápido. Espera {segundos_restantes}s."
122
+ )
123
+ registros_mensajes[usuario_actual] = ahora
124
+
125
+ def agente_inteligente_llm(deteccion: str, confianza: float):
126
+
127
+ saludo = "¡Bienvenido! Soy ReciclIA. Estoy aquí para ayudarte a transformar tus hábitos y cuidar el planeta de forma experta. Vamos a hacer que cada residuo cuente."
128
+
129
+ prompt = f"""
130
+ Eres un ReciclIA, un Agente experto en sostenibilidad y reciclaje.
131
+ Se ha detectado un objeto mediante visión artificial:
132
+ - Objeto: {deteccion}
133
+ - Confianza: {confianza:.2f}
134
+
135
+ INSTRUCCIÓN DE SALUDO:
136
+ Comienza SIEMPRE tu respuesta con esta frase exacta: "{saludo}"
137
+
138
+
139
+ BASE DE CONOCIMIENTO (ESTRICTA):
140
+ 1. CONTENEDOR MARRÓN (Orgánico): Restos de comida, cáscaras de fruta, posos de café, tapones de corcho, servilletas sucias y cualquier residuo de origen BIOLÓGICO.
141
+ 2. CONTENEDOR VERDE (Vidrio): Botellas de vidrio, frascos de conservas, tarros de perfume. (¡NUNCA para orgánicos!).
142
+ 3. CONTENEDOR AMARILLO (Envases): Plásticos, latas, briks y envases metálicos.
143
+ 4. CONTENEDOR AZUL (Papel/Cartón): Cajas de cartón, periódicos, folletos.
144
+ 5. CONTENEDOR GRIS (Resto): Pañales, colillas, objetos rotos que no se reciclan.
145
+ 6. PUNTO LIMPIO: Pilas, electrónica, muebles, ropa, aceite usado.
146
+
147
+
148
+ Explica al usuario de forma breve y amable:
149
+ 1. En qué contenedor debe tirarlo, añade el color especifico del contenedor utilizando la base de conocimientos.
150
+ 2. Un dato curioso sobre el reciclaje de ese material.
151
+
152
+ REGLA CRÍTICA PARA EL PUNTO 3:
153
+ - SI la confianza es menor a 0.5: Añade una advertencia breve diciendo que podrías estar equivocado debido a la baja precisión.
154
+ - SI la confianza es 0.5 o mayor: NO menciones nada sobre la confianza, ni sobre umbrales, ni des explicaciones de por qué estás seguro. Omite este punto totalmente.
155
+
156
+ Termina la explicación siempre con esta frase:
157
+ -Recuerda, cada pequeño gesto cuenta, y reciclar correctamente es un paso importante hacia un futuro más sostenible. ¡Gracias por tu contribución!
158
+ """
159
+
160
+ chat_completion = client_groq.chat.completions.create(
161
+ messages=[{"role": "user", "content": prompt}],
162
+ model="llama-3.3-70b-versatile", # Un modelo rápido y capaz
163
+ )
164
+ return chat_completion.choices[0].message.content
165
+
166
+
167
+
168
+ #CRUD Usuarios
169
+
170
+ @app.post("/registrar",
171
+ summary="Registrar nuevo usuario",
172
+ description="Crea la cuenta de un usuario y lo guarda en la base de datos.",
173
+ status_code=status.HTTP_201_CREATED)
174
+ async def registrar_usuario(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
175
+ existe = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == form_data.username).first()
176
+ if existe:
177
+ raise HTTPException(status_code=400, detail="El nombre de usuario ya existe")
178
+
179
+ password_con_hash = generar_hash(form_data.password)
180
+
181
+ nuevo_usuario = modelsProyecto.Usuario(
182
+ username=form_data.username,
183
+ password_hash=password_con_hash,
184
+ es_admin=True
185
+ )
186
+ db.add(nuevo_usuario)
187
+ db.commit()
188
+ return {"mensaje": "Usuario creado con éxito usando HASH"}
189
+
190
+
191
+ @app.put("/usuarios/actualizarUsuario",
192
+ summary="Actualizar nombre de usuario",
193
+ description="Actualiza el nombre de usuario del perfil activo.",
194
+ responses={400: {"description": "Nombre en uso"}, 404: {"description": "No encontrado"}})
195
+ async def actualizar_username(
196
+ nuevo_username: str,
197
+ token: str = Depends(oauth2_scheme),
198
+ db: Session = Depends(get_db)
199
+ ):
200
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
201
+ username_actual = payload.get("sub")
202
+
203
+ usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == username_actual).first()
204
+
205
+ if not usuario:
206
+ raise HTTPException(status_code=404, detail="Usuario no encontrado")
207
+
208
+ # 3. Verificar si el nuevo nombre ya está pillado por OTRO usuario
209
+ existe = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == nuevo_username).first()
210
+ if existe and existe.username != username_actual:
211
+ raise HTTPException(status_code=400, detail="Ese nombre de usuario ya está en uso")
212
+
213
+ usuario.username = nuevo_username
214
+ db.commit()
215
+ db.refresh(usuario)
216
+
217
+ return {"message": "Nombre actualizado", "nuevo_username": nuevo_username}
218
+
219
+ @app.delete("/usuarios/{user}",
220
+ summary="Eliminar cuenta",
221
+ description="Borra un usuario. Los administradores pueden borrar cualquier cuenta; usuarios normales solo la suya.",
222
+ status_code=status.HTTP_204_NO_CONTENT)
223
+ async def eliminar_usuario(
224
+ user: str,
225
+ token: str = Depends(oauth2_scheme),
226
+ db: Session = Depends(get_db),
227
+ ):
228
+
229
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
230
+ username_token = payload.get("sub")
231
+ es_admin: bool = payload.get("rol")
232
+
233
+ usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == user).first()
234
+
235
+ if not usuario:
236
+ raise HTTPException(status_code=404, detail="Usuario no encontrado")
237
+
238
+ if usuario.username != username_token and not es_admin:
239
+ raise HTTPException(status_code=403, detail="No tienes permiso para eliminar esta cuenta")
240
+
241
+ db.delete(usuario)
242
+ db.commit()
243
+
244
+ return None
245
+
246
+ @app.post("/detectar-visual",
247
+ summary="Detección de objetos y Reciclaje",
248
+ description="Analiza una imagen, devuelve la predicción + respuesta del agente y guarda en historial.",
249
+ responses={
250
+ 200: {"content": {"image/jpeg": {}}, "description": "Imagen procesada con cajas de detección."},
251
+ 429: {"description": "Límite de velocidad excedido"}
252
+ })
253
+ async def detectar_visual(file: UploadFile = File(...), token: str = Depends(oauth2_scheme),db: Session = Depends(get_db)):
254
+ try:
255
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
256
+ username: str = payload.get("sub")
257
+ usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == username).first()
258
+ if not usuario:
259
+ raise HTTPException(status_code=404, detail="Usuario no encontrado")
260
+ except jwt.PyJWTError:
261
+ raise HTTPException(status_code=401, detail="Token inválido")
262
+
263
+
264
+ verificar_frecuencia(username, segundos=30)
265
+
266
+ contents = await file.read()
267
+ image = Image.open(io.BytesIO(contents)).convert("RGB")
268
+
269
+ results = model.predict(image, imgsz=640, conf=0.25)[0]
270
+
271
+ prediccion_nombre = "Sin detecciones"
272
+ confianza_valor = 0.0
273
+
274
+ if len(results.boxes) > 0:
275
+ box = results.boxes[0]
276
+ prediccion_nombre = results.names[int(box.cls)]
277
+ confianza_valor = float(box.conf)
278
+ respuesta_agente = agente_inteligente_llm(prediccion_nombre, confianza_valor)
279
+ else:
280
+ respuesta_agente = "No veo nada claro aquí. ¿Puedes acercar más la cámara?"
281
+
282
+
283
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
284
+
285
+ ext = file.filename.split(".")[-1]
286
+ nombre_unico = f"img_{timestamp}.{ext}"
287
+
288
+
289
+ if not os.path.exists("imagenes_subidas"):
290
+ os.makedirs("imagenes_subidas")
291
+
292
+ ruta_archivo = f"imagenes_subidas/{nombre_unico}"
293
+ with open(ruta_archivo, "wb") as buffer:
294
+ buffer.write(contents)
295
+
296
+ nueva_imagen = modelsProyecto.Imagen(
297
+ name=nombre_unico,
298
+ ruta=ruta_archivo,
299
+ usuario_id=usuario.id,
300
+ prediccion=prediccion_nombre,
301
+ confianza=confianza_valor
302
+ )
303
+ db.add(nueva_imagen)
304
+
305
+ nuevo_historial = modelsProyecto.HistorialChat(
306
+ usuario_id=usuario.id,
307
+ mensaje_usuario=f"Detección de {prediccion_nombre}",
308
+ respuesta_agente=respuesta_agente
309
+ )
310
+ db.add(nuevo_historial)
311
+ db.commit()
312
+
313
+ im_array = results.plot()
314
+ im_rgb = Image.fromarray(im_array[..., ::-1])
315
+
316
+ img_io = io.BytesIO()
317
+ im_rgb.save(img_io, 'JPEG', quality=70)
318
+ img_io.seek(0)
319
+ return StreamingResponse(
320
+ img_io,
321
+ media_type="image/jpeg",
322
+ headers={
323
+ "X-Agent-Reply": respuesta_agente.replace("\n", " | "),
324
+ "X-Detection": prediccion_nombre
325
+ }
326
+ )
327
+
328
+ @app.get("/historial",
329
+ summary="Obtener historial de chat",
330
+ description="Recupera las últimas 10 consultas del usuario autenticado.")
331
+ async def obtener_mi_historial(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
332
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
333
+ username = payload.get("sub")
334
+ usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == username).first()
335
+
336
+ # Trae los últimos 10 mensajes
337
+ return db.query(modelsProyecto.HistorialChat).filter(
338
+ modelsProyecto.HistorialChat.usuario_id == usuario.id
339
+ ).order_by(modelsProyecto.HistorialChat.fecha.desc()).limit(10).all()
340
+
341
+
342
+ @app.delete("/historial",
343
+ summary="Vaciar historial",
344
+ description="Borra permanentemente todo el historial del usuari.")
345
+ async def vaciar_todo_el_historial(
346
+ token: str = Depends(oauth2_scheme),
347
+ db: Session = Depends(get_db)
348
+ ):
349
+
350
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
351
+ username = payload.get("sub")
352
+ usuario = db.query(modelsProyecto.Usuario).filter(modelsProyecto.Usuario.username == username).first()
353
+
354
+ registros = db.query(modelsProyecto.HistorialChat).filter(
355
+ modelsProyecto.HistorialChat.usuario_id == usuario.id
356
+ )
357
+
358
+ cantidad = registros.count()
359
+ registros.delete(synchronize_session=False)
360
+ db.commit()
361
+
362
+ return {"mensaje": f"Se han eliminado {cantidad} registros de tu historial"}
363
+
364
+
365
+ if __name__ == "__main__":
366
+ uvicorn.run("ProyectoBasura:app", host="127.0.0.1", port=8000, reload=True)
README.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Agente Inteligente Reciclaje
3
+ emoji: ♻️
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # ReciclA: Asistente Inteligente de Reciclaje
12
+
13
+ ![Sync to Hugging Face](https://github.com/YellowAlberto/ReciclA---Proyecto-Agente/actions/workflows/sync_to_hf.yml/badge.svg)
14
+
15
+ **ReciclA** es un ecosistema inteligente diseñado para facilitar el reciclaje mediante el uso de Inteligencia Artificial avanzada. El sistema integra visión artificial para la detección de residuos y un agente conversacional que asesora al usuario sobre la gestión de desechos.
16
+
17
+ ### Componentes Clave:
18
+
19
+ * **Agente Inteligente:** Implementado con **Groq** para ofrecer respuestas expertas y personalizadas en lenguaje natural.
20
+ * **Visión Artificial:** Utiliza modelos **YOLOv8** optimizados para identificar objetos en imágenes subidas por el usuario.
21
+ * **Base de Datos Híbrida:** Configurada para trabajar con **PostgreSQL (Supabase)** en producción o **SQLite** de forma local y automática si no se detectan credenciales.
22
+ * **Contenedorización:** Sistema orquestado con **Docker Compose** para garantizar la portabilidad sin dependencias manuales.
23
+ * **CI/CD Automático:** Sincronización automatizada mediante **GitHub Actions** que despliega cada actualización directamente en **Hugging Face Spaces**.
24
+
25
+ ---
26
+
27
+ # Instrucciones de Instalación
28
+
29
+ ### Requisitos previos:
30
+ * **Docker Desktop:** Instalado y en funcionamiento.
31
+ * **Groq API Key:** Necesaria para el funcionamiento del agente conversacional.
32
+
33
+ ### Pasos para el despliegue:
34
+
35
+ 1. **Configuración de entorno:** Crea un archivo `.env` en la raíz del proyecto con el siguiente contenido:
36
+ ```env
37
+ GROQ_API_KEY=tu_api_key_aqui
38
+ SECRET_KEY=un_secreto_aleatorio_para_jwt
39
+ # Opcional: Si se omite DATABASE_URL, el sistema iniciará en modo SQLite local
40
+ DATABASE_URL=postgresql://tu_url_de_supabase
41
+ ```
42
+
43
+ 2. **Lanzamiento:** Abre una terminal en la carpeta del proyecto y ejecuta el siguiente comando:
44
+ ```bash
45
+ docker-compose up --build
46
+ ```
47
+
48
+ 3. **Acceso a las interfaces:**
49
+ * **Despliegue Online (Hugging Face):** [https://huggingface.co/spaces/YellowAlberto/ReciclA]
50
+ * **Frontend (Gradio):** [http://localhost:7860](http://localhost:7860)
51
+ * **API Docs (Swagger):** [http://localhost:8000/docs](http://localhost:8000/docs)
52
+
53
+ > **Nota sobre el despliegue:** Este repositorio está configurado con **GitHub Actions**. Cualquier commit en la rama `main` dispara automáticamente una actualización del contenedor en Hugging Face.
54
+
55
+ ---
56
+
57
+ # Uso de la API
58
+
59
+ La API sigue los principios **RESTful**, utilizando los verbos HTTP para definir las acciones sobre cada recurso.
60
+
61
+ | Método | Endpoint | Descripción |
62
+ | :--- | :--- | :--- |
63
+ | **POST** | `/registrar` | Crea la cuenta de un usuario y lo guarda en la base de datos. |
64
+ | **POST** | `/login` | Comprueba las credenciales y genera un token JWT de acceso. |
65
+ | **POST** | `/detectar-visual` | Analiza una imagen, devuelve la predicción + respuesta del agente y guarda en historial. |
66
+ | **GET** | `/historial` | Recupera las últimas 10 consultas del usuario autenticado. |
67
+ | **PUT** | `/usuarios/actualizarUsuario` | Actualiza el nombre de usuario del perfil activo. |
68
+ | **DELETE** | `/historial` | Borra permanentemente todo el historial del usuario. |
69
+
70
+ ---
71
+
72
+ ### Persistencia de Datos
73
+ El proyecto utiliza **volúmenes de Docker** para asegurar que la persistencia sea efectiva:
74
+ * **SQLite:** Si se usa el modo local, se creará un archivo `test.db` en tu carpeta raíz que no se borrará al apagar el contenedor.
75
+ * **Interfaz:** El logo e iconos se cargan mediante codificación **Base64** para garantizar su visualización correcta sin errores de rutas locales o bloqueos de seguridad del navegador.
SeguridadProyecto.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from passlib.context import CryptContext
2
+
3
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
4
+
5
+ def generar_hash(password: str):
6
+ return pwd_context.hash(password)
7
+
8
+ def verificar_password(password_plano, password_hasheado):
9
+ return pwd_context.verify(password_plano, password_hasheado)
app_front.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import io
4
+ from PIL import Image
5
+ import re
6
+ import base64
7
+
8
+ # URL interna de la API
9
+ API_URL = "http://127.0.0.1:8000"
10
+
11
+ # --- Imagenes ---
12
+ def get_base64_image(image_path):
13
+ try:
14
+ with open(image_path, "rb") as img_file:
15
+ return base64.b64encode(img_file.read()).decode('utf-8')
16
+ except Exception:
17
+ return ""
18
+
19
+ nombre_archivo = "protection-environment-robot-holding-lightbulb-with-recycle-icon-sustainable-and-nature_10461459.jpg!sw800"
20
+ img_base64 = get_base64_image(nombre_archivo)
21
+
22
+ robot_html = f"<img src='data:image/jpeg;base64,{img_base64}' width='40' style='display:inline-block; vertical-align:middle; border-radius:50%; margin-right:10px;'>"
23
+ # --- Estilos ---
24
+ custom_css = """
25
+ #titulo-proyecto h1, #respuesta-agente h2 {
26
+ font-family: 'Charter', 'Bitstream Charter', 'Cambria', 'Georgia', serif !important;
27
+ font-weight: 700 !important;
28
+ letter-spacing: -0.5px !important;
29
+ color: #F3F4F6 !important; /* El color blanco hueso que elegiste */
30
+ }
31
+ #respuesta-agente p {
32
+ font-family: 'Charter', 'Georgia', serif !important;
33
+ font-size: 1.1em;
34
+ }
35
+ #btn-actualizar {
36
+ background: #2D5A27 !important;
37
+ color: white !important;
38
+ border: none !important;
39
+ }
40
+ #btn-actualizar:hover {
41
+ background: #3e7a36 !important;
42
+ }
43
+ """
44
+
45
+ # --- FUNCIONES ---
46
+
47
+ def registrar_usuario(new_user, new_pass):
48
+ if not new_user or not new_pass:
49
+ return " Por favor, rellena ambos campos.", gr.update()
50
+ try:
51
+ data = {"username": new_user, "password": new_pass}
52
+ response = requests.post(f"{API_URL}/registrar", data=data)
53
+
54
+ if response.ok:
55
+ return f" Usuario '{new_user}' registrado. ¡Inicia sesión!", gr.Tabs(selected=1)
56
+
57
+ error_msg = response.json().get('detail', 'No se pudo registrar')
58
+ return f" Error: {error_msg}", gr.update()
59
+
60
+ except Exception as e:
61
+ return f" Error de conexión: {str(e)}", gr.update()
62
+
63
+ def login_usuario(username, password):
64
+ if not username or not password:
65
+ return None, " Introduce tus credenciales.", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
66
+ try:
67
+ login_data = {"username": username, "password": password}
68
+ res = requests.post(f"{API_URL}/login", data=login_data)
69
+ if res.ok:
70
+ token = res.json().get("access_token")
71
+ user_txt = f" **Usuario:** {username}"
72
+ return token, f" Hola {username}", gr.Tabs(selected=2), user_txt, user_txt, user_txt, user_txt, gr.update(visible=False), gr.update(visible=True), " Preparado para analizar imágenes"
73
+ return None, " Error", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
74
+ except Exception as e:
75
+ return None, f" Error: {str(e)}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
76
+
77
+ def analizar_foto(token, imagen):
78
+ if not token: return " Debes iniciar sesión primero.", None, gr.Tabs(selected=1)
79
+ if imagen is None: return " Por favor, sube una foto.", None, gr.update()
80
+ try:
81
+ buf = io.BytesIO()
82
+ imagen.save(buf, format="JPEG")
83
+ buf.seek(0)
84
+ headers = {"Authorization": f"Bearer {token}"}
85
+ files = {"file": ("image.jpg", buf, "image/jpeg")}
86
+ res = requests.post(f"{API_URL}/detectar-visual", headers=headers, files=files)
87
+ if res.ok:
88
+ msg_crudo = res.headers.get("X-Agent-Reply", "Sin respuesta")
89
+ det = res.headers.get("X-Detection", "Desconocido")
90
+ msg_formateado = msg_crudo.replace("|", "\n\n")
91
+ msg_formateado = re.sub(r' +', ' ', msg_formateado).strip()
92
+ img_res = Image.open(io.BytesIO(res.content))
93
+ return f"## {robot_html} ReciclA Dice:\n\n{msg_formateado}\n\n**Objeto:** {det}", img_res, gr.update()
94
+ return f" Error: {res.text}", None, gr.update()
95
+ except Exception as e: return f" Error: {str(e)}", None, gr.update()
96
+
97
+ def ver_historial(token):
98
+ if not token: return " Inicia sesión primero.", gr.Tabs(selected=1)
99
+ try:
100
+ headers = {"Authorization": f"Bearer {token}"}
101
+ res = requests.get(f"{API_URL}/historial", headers=headers)
102
+ if res.ok:
103
+ h = res.json()
104
+ if not h: return "Aún no tienes historial.", gr.update()
105
+ texto = "### Tu historial de reciclaje:\n"
106
+ for item in h:
107
+ fecha = item.get('fecha', 'Sin fecha')[:10]
108
+ resp = item.get('respuesta_agente', 'Sin respuesta')
109
+ texto += f"**{fecha}**:\n> {robot_html} {resp}\n\n--- \n"
110
+ return texto, gr.update()
111
+ return f" Error: {res.text}", gr.update()
112
+ except Exception as e: return f" Error: {str(e)}", gr.update()
113
+
114
+ def logout_usuario():
115
+ no_user = " **Usuario:** No identificado"
116
+ return None, "Sesión cerrada.", gr.Tabs(selected=1), no_user, no_user, no_user, no_user, gr.update(visible=True), gr.update(visible=False)
117
+
118
+ def update_usuario(token, nuevo_nombre):
119
+ no_user = " **Usuario:** No identificado"
120
+ if not token or not nuevo_nombre:
121
+ return token, " Error: Datos incompletos", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
122
+ try:
123
+ headers = {"Authorization": f"Bearer {token}"}
124
+ nuevo_nombre = nuevo_nombre.strip()
125
+ res = requests.put(f"{API_URL}/usuarios/actualizarUsuario?nuevo_username={nuevo_nombre}", headers=headers)
126
+ if res.ok:
127
+ mensaje_exito = f" Nombre cambiado a '{nuevo_nombre}'. Reinicia sesión por seguridad."
128
+ return None, mensaje_exito, gr.Tabs(selected=1), no_user, no_user, no_user, no_user, gr.update(visible=True), gr.update(visible=False)
129
+ error_api = res.json().get('detail', 'Error desconocido')
130
+ return token, f" Error: {error_api}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
131
+ except Exception as e:
132
+ return token, f" Error de conexion: {str(e)}", gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
133
+
134
+ def vaciar_historial_usuario(token):
135
+ if not token: return " Inicia sesión primero."
136
+ headers = {"Authorization": f"Bearer {token}"}
137
+ res = requests.delete(f"{API_URL}/historial", headers=headers)
138
+ return " Historial vaciado correctamente." if res.ok else f" Error: {res.text}"
139
+
140
+ # --- INTERFAZ ---
141
+
142
+ with gr.Blocks(title="ReciclA", css=custom_css) as demo:
143
+ token_state = gr.State()
144
+ gr.Markdown(f"# {robot_html} ReciclA : Tu Asistente Inteligente de Reciclaje", elem_id="titulo-proyecto")
145
+
146
+ with gr.Tabs() as main_tabs:
147
+
148
+ # PESTAÑA 0: REGISTRO
149
+ with gr.TabItem("📝 Registro", id=0) as tab_registro:
150
+ user_info_0 = gr.Markdown(" **Usuario:** No identificado")
151
+ gr.Markdown("---")
152
+ reg_user = gr.Textbox(label="Usuario")
153
+ reg_pass = gr.Textbox(label="Contraseña", type="password")
154
+ reg_btn = gr.Button("Registrarme")
155
+ reg_out = gr.Markdown()
156
+
157
+ # PESTAÑA 4: PERFIL
158
+ with gr.TabItem("👤 Mi Perfil", id=4, visible=False) as tab_perfil:
159
+ user_info_p = gr.Markdown(" **Usuario:** No identificado")
160
+ with gr.Group():
161
+ gr.Markdown("### Configuración de Perfil")
162
+ nuevo_nombre_input = gr.Textbox(label="Nuevo nombre de usuario", placeholder="Escribe tu nuevo nombre aquí...")
163
+ btn_cambiar_nombre = gr.Button("Actualizar Nombre", variant="primary", elem_id="btn-actualizar")
164
+ gr.Markdown("<center><small> Por seguridad, se cerrará la sesión tras el cambio.</small></center>")
165
+ logout_btn = gr.Button("Cerrar Sesión", variant="stop")
166
+
167
+ # PESTAÑA 1: LOGIN
168
+ with gr.TabItem("🔐 Login", id=1):
169
+ user_info_1 = gr.Markdown(" **Usuario:** No identificado")
170
+ gr.Markdown("---")
171
+ l_user = gr.Textbox(label="Usuario")
172
+ l_pass = gr.Textbox(label="Contraseña", type="password")
173
+ with gr.Row():
174
+ l_btn = gr.Button("Iniciar Sesión", variant="primary")
175
+ l_status = gr.Markdown(visible=False)
176
+
177
+ # PESTAÑA 2: ANALISIS
178
+ with gr.TabItem("🔍 Análisis", id=2):
179
+ user_info_2 = gr.Markdown(" **Usuario:** No identificado")
180
+ gr.Markdown("---")
181
+ img_input = gr.Image(type="pil", show_label=False)
182
+ btn_scan = gr.Button("Analizar", variant="primary")
183
+ txt_output = gr.Markdown("Inicia sesión para poder analizar tus imagenes", elem_id="respuesta-agente")
184
+ img_output = gr.Image(show_label=False)
185
+
186
+ # PESTAÑA 3: HISTORIAL
187
+ with gr.TabItem("📜 Historial", id=3):
188
+ user_info_3 = gr.Markdown(" **Usuario:** No identificado")
189
+ gr.Markdown("---")
190
+ with gr.Row():
191
+ btn_hist = gr.Button(" Cargar mi Historial", variant="primary")
192
+ btn_vaciar = gr.Button(" Vaciar Historial", variant="stop")
193
+ out_hist = gr.Markdown("Dale a 'Cargar' para ver tus movimientos.")
194
+
195
+ # --- Botones ---
196
+ reg_btn.click(registrar_usuario, [reg_user, reg_pass], [reg_out, main_tabs])
197
+
198
+ l_btn.click(
199
+ login_usuario,
200
+ [l_user, l_pass],
201
+ [token_state, l_status, main_tabs, user_info_0, user_info_1, user_info_2, user_info_3, tab_registro, tab_perfil]
202
+ )
203
+
204
+ logout_btn.click(
205
+ logout_usuario,
206
+ None,
207
+ [token_state, l_status, main_tabs, user_info_0, user_info_1, user_info_2, user_info_3, tab_registro, tab_perfil]
208
+ )
209
+
210
+ btn_scan.click(analizar_foto, [token_state, img_input], [txt_output, img_output, main_tabs])
211
+ btn_hist.click(ver_historial, [token_state], [out_hist, main_tabs])
212
+ btn_vaciar.click(vaciar_historial_usuario, [token_state], [out_hist])
213
+
214
+ btn_cambiar_nombre.click(
215
+ fn=update_usuario,
216
+ inputs=[token_state, nuevo_nombre_input],
217
+ outputs=[token_state, l_status, main_tabs, user_info_0, user_info_1, user_info_2, user_info_3, tab_registro, tab_perfil]
218
+ )
219
+
220
+ if __name__ == "__main__":
221
+ demo.launch(
222
+ server_name="0.0.0.0",
223
+ server_port=7860,
224
+ theme=gr.themes.Soft(),
225
+ favicon_path="favicon.png",
226
+ allowed_paths=["/app"]
227
+ )
best.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:464baadf8248ab7e76647ab35256326eb81bbba197450ebaeb353649ccc4758f
3
+ size 10611218
csc ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/.github/workflows/sync_to_hf_yml b/.github/workflows/sync_to_hf_yml
2
+ new file mode 100644
3
+ index 0000000..61eb009
4
+ --- /dev/null
5
+ +++ b/.github/workflows/sync_to_hf_yml
6
+ @@ -0,0 +1,19 @@
7
+ +name: Sync to Hugging Face hub
8
+ +on:
9
+ + push:
10
+ + branches: [main]
11
+ + force_push:
12
+ + branches: [main]
13
+ +
14
+ +jobs:
15
+ + sync-to-hub:
16
+ + runs-on: ubuntu-latest
17
+ + steps:
18
+ + - uses: actions/checkout@v3
19
+ + with:
20
+ + fetch-depth: 0
21
+ + lfs: true
22
+ + - name: Push to hub
23
+ + env:
24
+ + HF_TOKEN: ${{ secrets.HF_TOKEN }}
25
+ + run: git push --force https://YellowAlberto:$HF_TOKEN@huggingface.co/spaces/YellowAlberto/ReciclA main
26
+ 
databaseProyecto.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from sqlalchemy import create_engine
4
+ from sqlalchemy.ext.declarative import declarative_base
5
+ from sqlalchemy.orm import sessionmaker
6
+
7
+ load_dotenv()
8
+ SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL")
9
+
10
+ # Si no hay URL (local), usamos SQLite por si acaso
11
+ if not SQLALCHEMY_DATABASE_URL:
12
+ SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
13
+
14
+ engine = create_engine(SQLALCHEMY_DATABASE_URL)
15
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
16
+ Base = declarative_base()
docker-compose.yaml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ app:
3
+ build: .
4
+ ports:
5
+ - "7860:7860"
6
+ - "8000:8000"
7
+ env_file:
8
+ - .env
9
+ environment:
10
+ - DATABASE_URL=${DATABASE_URL}
11
+ - GROQ_API_KEY=${GROQ_API_KEY}
12
+ volumes:
13
+ - .:/app
modelsProyecto.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, ForeignKey, Boolean, DateTime, Float, Text
2
+ from sqlalchemy.orm import relationship
3
+ from databaseProyecto import Base
4
+ from datetime import datetime
5
+ from pydantic import BaseModel
6
+
7
+ class Usuario(Base):
8
+ __tablename__ = "usuarios"
9
+ id = Column(Integer, primary_key=True, index=True)
10
+ username = Column(String, unique=True, index=True)
11
+ password_hash = Column(String)
12
+ es_admin = Column(Boolean, default=False)
13
+
14
+ imagenes = relationship("Imagen", back_populates="usuario", cascade="all, delete-orphan")
15
+ historial = relationship("HistorialChat", back_populates="usuario")
16
+
17
+ class UsuarioSchema(BaseModel):
18
+ username: str
19
+ es_admin: bool = False
20
+
21
+ class Imagen(Base):
22
+ __tablename__ = "imagenes"
23
+ id = Column(Integer, primary_key=True, index=True)
24
+ name = Column(String, unique=True, index=True)
25
+ ruta = Column(String, unique=True)
26
+
27
+ fecha_subida = Column(DateTime, default=datetime.utcnow)
28
+
29
+ prediccion = Column(String, nullable=True)
30
+ confianza = Column(Float, nullable=True)
31
+
32
+ usuario_id = Column(Integer, ForeignKey("usuarios.id"), nullable=False)
33
+
34
+ usuario = relationship("Usuario", back_populates="imagenes")
35
+
36
+ class HistorialChat(Base):
37
+ __tablename__ = "historial_chat"
38
+ id = Column(Integer, primary_key=True, index=True)
39
+ usuario_id = Column(Integer, ForeignKey("usuarios.id"))
40
+ mensaje_usuario = Column(Text)
41
+ respuesta_agente = Column(Text)
42
+ fecha = Column(DateTime, default=datetime.utcnow)
43
+
44
+ usuario = relationship("Usuario", back_populates="historial")
protection-environment-robot-holding-lightbulb-with-recycle-icon-sustainable-and-nature_10461459.jpg!sw800 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53a7cb9ff6536d515c9378d61ee25f8709ba7080a63d5abe381000a304f41a10
3
+ size 294919
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ sqlalchemy
4
+ psycopg2-binary
5
+ PyJWT[crypto]
6
+ passlib[bcrypt]==1.7.4
7
+ bcrypt==4.0.1
8
+ ultralytics
9
+ onnxruntime
10
+ groq
11
+ python-multipart
12
+ python-dotenv
13
+ requests
14
+ gradio
15
+ pillow
16
+ numpy
run.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Iniciar FastAPI
4
+ uvicorn ProyectoBasura:app --host 0.0.0.0 --port 8000 &
5
+
6
+ sleep 5
7
+
8
+ # Iniciar frontend
9
+ python app_front.py