Edoruin commited on
Commit
55e1c6e
·
1 Parent(s): fdcaf7e

CLASSROOMMAKER TOOL create

Browse files
app/main.py CHANGED
@@ -311,6 +311,43 @@ class LoanManager(HFDatasetManager):
311
 
312
  loan_mgr = LoanManager()
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  # --- INTEGRACIÓN CON TELEGRAM ---
315
  TG_TOKEN = os.getenv("TELEGRAM_TOKEN")
316
  TG_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
@@ -611,6 +648,44 @@ def prestamos():
611
  loans = loan_mgr.get_all()
612
  return render_template('prestamos.html', title="Préstamos", loans=loans)
613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  @app.route('/api/prestamo', methods=['POST'])
615
  def api_prestamo():
616
  data = request.json
 
311
 
312
  loan_mgr = LoanManager()
313
 
314
+ # --- GESTOR DE ROSTROS (Asistencia) ---
315
+ class FaceManager(HFDatasetManager):
316
+ """Clase para manejar descriptores de rostros para asistencia."""
317
+
318
+ def __init__(self):
319
+ super().__init__(
320
+ dataset_name="HF_DATASET_FACES",
321
+ data_key="faces",
322
+ local_filename="faces.json"
323
+ )
324
+ self.faces = self._load()
325
+
326
+ def _load(self):
327
+ if self.use_hf:
328
+ data = self._load_from_hf()
329
+ if data is not None:
330
+ return data
331
+ return self._load_from_local()
332
+
333
+ def save(self):
334
+ self._save_to_local(self.faces)
335
+ if self.use_hf:
336
+ self._save_to_hf(self.faces)
337
+
338
+ def add_face(self, label, descriptor):
339
+ # El descriptor es una lista de 128 floats (face-api.js)
340
+ self.faces.append({
341
+ "label": label,
342
+ "descriptor": descriptor
343
+ })
344
+ self.save()
345
+
346
+ def get_all(self):
347
+ return self.faces
348
+
349
+ face_mgr = FaceManager()
350
+
351
  # --- INTEGRACIÓN CON TELEGRAM ---
352
  TG_TOKEN = os.getenv("TELEGRAM_TOKEN")
353
  TG_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
 
648
  loans = loan_mgr.get_all()
649
  return render_template('prestamos.html', title="Préstamos", loans=loans)
650
 
651
+ # --- CLASSROOM MAKER ---
652
+
653
+ @app.route('/classroom')
654
+ @login_required
655
+ def classroom():
656
+ return render_template('classroom.html', title="Classroom Maker")
657
+
658
+ @app.route('/asistencia')
659
+ @login_required
660
+ def asistencia():
661
+ return render_template('asistencia.html', title="Toma de Asistencia")
662
+
663
+ @app.route('/api/faces', methods=['GET', 'POST'])
664
+ @login_required
665
+ def api_faces():
666
+ if request.method == 'POST':
667
+ data = request.json
668
+ label = data.get('label')
669
+ descriptor = data.get('descriptor')
670
+ if label and descriptor:
671
+ face_mgr.add_face(label, descriptor)
672
+ return jsonify({"status": "success"})
673
+ return jsonify({"status": "error", "message": "Datos faltantes"}), 400
674
+
675
+ return jsonify(face_mgr.get_all())
676
+
677
+ @app.route('/api/attendance', methods=['POST'])
678
+ @login_required
679
+ def api_record_attendance():
680
+ data = request.json
681
+ label = data.get('label')
682
+ if label:
683
+ # Aquí se podría guardar en un historial de asistencia
684
+ print(f"[ASISTENCIA] {label} presente a las {datetime.datetime.now()}")
685
+ socketio.emit('notification', {"text": f"Asistencia: {label} registrado", "color": "green"})
686
+ return jsonify({"status": "success"})
687
+ return jsonify({"status": "error"}), 400
688
+
689
  @app.route('/api/prestamo', methods=['POST'])
690
  def api_prestamo():
691
  data = request.json
app/templates/asistencia.html ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="hero section">
5
+ <h1>TOMA DE ASISTENCIA</h1>
6
+ <p class="text-dim">Reconocimiento facial en tiempo real para el Maker Space.</p>
7
+ </div>
8
+
9
+ <div class="attendance-container glass" style="padding: 2rem; border-radius: 1rem; max-width: 800px; margin: 0 auto;">
10
+ <div id="loading-overlay" style="text-align: center; padding: 2rem;">
11
+ <i class="fas fa-spinner fa-spin fa-3x" style="color: #38bdf8;"></i>
12
+ <p style="margin-top: 1rem;">Cargando modelos de IA...</p>
13
+ </div>
14
+
15
+ <div id="camera-container"
16
+ style="position: relative; display: none; margin-bottom: 2rem; border-radius: 0.5rem; overflow: hidden; background: #000;">
17
+ <video id="video" width="720" height="560" autoplay muted
18
+ style="width: 100%; height: auto; display: block;"></video>
19
+ <canvas id="overlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></canvas>
20
+ </div>
21
+
22
+ <div class="controls-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem;">
23
+ <button id="btn-attendance" class="btn glass active" style="background: #10b981; color: white;">
24
+ <i class="fas fa-id-card"></i> MODO ASISTENCIA
25
+ </button>
26
+ <button id="btn-register" class="btn glass">
27
+ <i class="fas fa-user-plus"></i> REGISTRAR ROSTRO
28
+ </button>
29
+ </div>
30
+
31
+ <div id="register-form"
32
+ style="display: none; margin-top: 2rem; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1.5rem;">
33
+ <h3>Nuevo Registro</h3>
34
+ <p class="text-dim">Mira a la cámara y escribe tu nombre.</p>
35
+ <div style="display: flex; gap: 1rem; margin-top: 1rem;">
36
+ <input type="text" id="user-name" class="glass" placeholder="Tu nombre completo"
37
+ style="flex: 1; padding: 0.8rem; border: none; outline: none; color: white;">
38
+ <button id="btn-save-face" class="btn" style="background: #38bdf8; color: white;">GUARDAR</button>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Scripts de Face API -->
44
+ <script defer src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
45
+
46
+ <script>
47
+ const video = document.getElementById('video');
48
+ const overlay = document.getElementById('overlay');
49
+ const loadingOverlay = document.getElementById('loading-overlay');
50
+ const cameraContainer = document.getElementById('camera-container');
51
+ const btnAttendance = document.getElementById('btn-attendance');
52
+ const btnRegister = document.getElementById('btn-register');
53
+ const registerForm = document.getElementById('register-form');
54
+ const btnSaveFace = document.getElementById('btn-save-face');
55
+ const userNameInput = document.getElementById('user-name');
56
+
57
+ let mode = 'attendance'; // 'attendance' o 'register'
58
+ let labeledFaceDescriptors = [];
59
+ let faceMatcher = null;
60
+ let detectionActive = false;
61
+ let currentDescriptor = null;
62
+
63
+ // Cargar modelos y arrancar
64
+ async function init() {
65
+ try {
66
+ const MODEL_URL = 'https://raw.githubusercontent.com/justadudewhohacks/face-api.js/master/weights';
67
+ await Promise.all([
68
+ faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL),
69
+ faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL),
70
+ faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL)
71
+ ]);
72
+
73
+ await loadSavedFaces();
74
+ startVideo();
75
+ } catch (err) {
76
+ console.error("Error inicializando Face API:", err);
77
+ alert("No se pudieron cargar los modelos de IA.");
78
+ }
79
+ }
80
+
81
+ async function loadSavedFaces() {
82
+ const res = await fetch('/api/faces');
83
+ const data = await res.json();
84
+
85
+ if (data.length > 0) {
86
+ labeledFaceDescriptors = data.map(df => {
87
+ // El descriptor se guarda como array simple en JSON
88
+ const desc = new Float32Array(df.descriptor);
89
+ return new faceapi.LabeledFaceDescriptors(df.label, [desc]);
90
+ });
91
+ faceMatcher = new faceapi.FaceMatcher(labeledFaceDescriptors, 0.6);
92
+ }
93
+ }
94
+
95
+ function startVideo() {
96
+ navigator.mediaDevices.getUserMedia({ video: {} })
97
+ .then(stream => {
98
+ video.srcObject = stream;
99
+ loadingOverlay.style.display = 'none';
100
+ cameraContainer.style.display = 'block';
101
+ detectionActive = true;
102
+ onPlay();
103
+ })
104
+ .catch(err => {
105
+ console.error("Error acceso cámara:", err);
106
+ alert("Es necesario acceso a la cámara.");
107
+ });
108
+ }
109
+
110
+ async function onPlay() {
111
+ if (!detectionActive) return;
112
+
113
+ const displaySize = { width: video.clientWidth, height: video.clientHeight };
114
+ faceapi.matchDimensions(overlay, displaySize);
115
+
116
+ setInterval(async () => {
117
+ if (!detectionActive) return;
118
+
119
+ const detections = await faceapi.detectAllFaces(video, new faceapi.TinyFaceDetectorOptions())
120
+ .withFaceLandmarks()
121
+ .withFaceDescriptors();
122
+
123
+ const resizedDetections = faceapi.resizeResults(detections, displaySize);
124
+ const ctx = overlay.getContext('2d');
125
+ ctx.clearRect(0, 0, overlay.width, overlay.height);
126
+
127
+ if (mode === 'attendance' && faceMatcher && detections.length > 0) {
128
+ const results = resizedDetections.map(d => faceMatcher.findBestMatch(d.descriptor));
129
+ results.forEach((result, i) => {
130
+ const box = resizedDetections[i].detection.box;
131
+ const drawBox = new faceapi.draw.DrawBox(box, { label: result.toString() });
132
+ drawBox.draw(overlay);
133
+
134
+ if (result.label !== 'unknown') {
135
+ // Enviar asistencia si no se ha enviado recientemente
136
+ throttleAttendance(result.label);
137
+ }
138
+ });
139
+ } else if (mode === 'register' && detections.length > 0) {
140
+ const box = resizedDetections[0].detection.box;
141
+ new faceapi.draw.DrawBox(box, { label: "Listo para registrar" }).draw(overlay);
142
+ currentDescriptor = detections[0].descriptor;
143
+ } else {
144
+ faceapi.draw.drawDetections(overlay, resizedDetections);
145
+ }
146
+ }, 300);
147
+ }
148
+
149
+ let lastAttendance = {};
150
+ function throttleAttendance(label) {
151
+ const now = Date.now();
152
+ if (!lastAttendance[label] || (now - lastAttendance[label] > 30000)) { // 30 seg
153
+ lastAttendance[label] = now;
154
+ fetch('/api/attendance', {
155
+ method: 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify({ label: label })
158
+ });
159
+ }
160
+ }
161
+
162
+ // UI Events
163
+ btnAttendance.addEventListener('click', () => {
164
+ mode = 'attendance';
165
+ btnAttendance.classList.add('active');
166
+ btnRegister.classList.remove('active');
167
+ registerForm.style.display = 'none';
168
+ btnAttendance.style.background = '#10b981';
169
+ btnRegister.style.background = 'rgba(255,255,255,0.05)';
170
+ });
171
+
172
+ btnRegister.addEventListener('click', () => {
173
+ mode = 'register';
174
+ btnRegister.classList.add('active');
175
+ btnAttendance.classList.remove('active');
176
+ registerForm.style.display = 'block';
177
+ btnRegister.style.background = '#38bdf8';
178
+ btnAttendance.style.background = 'rgba(255,255,255,0.05)';
179
+ });
180
+
181
+ btnSaveFace.addEventListener('click', async () => {
182
+ const name = userNameInput.value.trim();
183
+ if (!name || !currentDescriptor) {
184
+ alert("Escribe un nombre y asegúrate de que tu rostro es visible.");
185
+ return;
186
+ }
187
+
188
+ btnSaveFace.disabled = true;
189
+ btnSaveFace.innerText = "GUARDANDO...";
190
+
191
+ const response = await fetch('/api/faces', {
192
+ method: 'POST',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ body: JSON.stringify({
195
+ label: name,
196
+ descriptor: Array.from(currentDescriptor)
197
+ })
198
+ });
199
+
200
+ if (response.ok) {
201
+ alert("Rostro registrado con éxito.");
202
+ userNameInput.value = "";
203
+ await loadSavedFaces();
204
+ btnAttendance.click();
205
+ } else {
206
+ alert("Error al guardar.");
207
+ }
208
+ btnSaveFace.disabled = false;
209
+ btnSaveFace.innerText = "GUARDAR";
210
+ });
211
+
212
+ init();
213
+ </script>
214
+
215
+ <style>
216
+ .btn.active {
217
+ border: 1px solid white;
218
+ }
219
+
220
+ .controls-grid .btn {
221
+ padding: 1rem;
222
+ border-radius: 0.5rem;
223
+ }
224
+
225
+ input.glass::placeholder {
226
+ color: rgba(255, 255, 255, 0.4);
227
+ }
228
+ </style>
229
+ {% endblock %}
app/templates/base.html CHANGED
@@ -37,6 +37,7 @@
37
  <div class="nav-links">
38
  <a href="/prestamos" class="nav-item">PRÉSTAMOS</a>
39
  <a href="/repos" class="nav-item">PROYECTOS</a>
 
40
 
41
  <!-- Botón de Logout (solo si el usuario está autenticado) -->
42
  {% if current_user.is_authenticated %}
 
37
  <div class="nav-links">
38
  <a href="/prestamos" class="nav-item">PRÉSTAMOS</a>
39
  <a href="/repos" class="nav-item">PROYECTOS</a>
40
+ <a href="/classroom" class="nav-item">AULAS</a>
41
 
42
  <!-- Botón de Logout (solo si el usuario está autenticado) -->
43
  {% if current_user.is_authenticated %}
app/templates/classroom.html ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="hero section">
5
+ <h1>CLASSROOM MAKER</h1>
6
+ <p class="text-dim">Gestión inteligente de aulas y asistencia en tiempo real.</p>
7
+ </div>
8
+
9
+ <div class="card-grid">
10
+ <!-- Tarjeta para Toma de Asistencia -->
11
+ <a href="/asistencia" class="card glass" style="border-bottom: 4px solid #10b981;">
12
+ <i class="fas fa-user-check"></i>
13
+ <h2>TOMA DE ASISTENCIA</h2>
14
+ <p class="text-dim">Usa reconocimiento facial para registrar la asistencia del aula.</p>
15
+ </a>
16
+
17
+ <!-- Tarjeta para Configuración de Aula (Próximamente) -->
18
+ <div class="card glass" style="opacity: 0.7; cursor: not-allowed;">
19
+ <i class="fas fa- chalkboard"></i>
20
+ <h2>GESTIÓN DE AULAS</h2>
21
+ <p class="text-dim">Configura horarios y grupos (Próximamente).</p>
22
+ </div>
23
+ </div>
24
+
25
+ <div style="margin-top: 3rem; text-align: center;">
26
+ <a href="/" class="btn glass"><i class="fas fa-arrow-left"></i> Volver al Inicio</a>
27
+ </div>
28
+ {% endblock %}
app/templates/index.html CHANGED
@@ -22,5 +22,12 @@
22
  <h2>SISTEMA PRÉSTAMOS</h2>
23
  <p class="text-dim">Solicita herramientas y dispositivos para tus proyectos.</p>
24
  </a>
 
 
 
 
 
 
 
25
  </div>
26
  {% endblock %}
 
22
  <h2>SISTEMA PRÉSTAMOS</h2>
23
  <p class="text-dim">Solicita herramientas y dispositivos para tus proyectos.</p>
24
  </a>
25
+
26
+ <!-- Tarjeta para Classroom Maker -->
27
+ <a href="/classroom" class="card glass" style="border-bottom: 4px solid #10b981;">
28
+ <i class="fas fa-chalkboard-teacher"></i>
29
+ <h2>CLASSROOM MAKER</h2>
30
+ <p class="text-dim">Control de asistencia y gestión de aulas inteligentes.</p>
31
+ </a>
32
  </div>
33
  {% endblock %}