github-actions[bot] commited on
Commit
60f6561
·
1 Parent(s): bd74fdc

🚀 Deploy from GitHub Actions - 2026-02-03 14:01:57

Browse files
Files changed (1) hide show
  1. app.py +44 -74
app.py CHANGED
@@ -3,7 +3,7 @@ Wakee API - Production
3
  ONNX Runtime UNIQUEMENT (pas de PyTorch)
4
  """
5
 
6
- from fastapi import FastAPI, File, UploadFile, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from pydantic import BaseModel, Field
9
  from typing import List, Optional
@@ -96,16 +96,16 @@ class PredictionResponse(BaseModel):
96
  frustration: float = Field(..., ge=0, le=3)
97
  timestamp: str
98
 
99
- class AnnotationInsert(BaseModel):
100
- image_base64: str
101
- predicted_boredom: float = Field(..., ge=0, le=3)
102
- predicted_confusion: float = Field(..., ge=0, le=3)
103
- predicted_engagement: float = Field(..., ge=0, le=3)
104
- predicted_frustration: float = Field(..., ge=0, le=3)
105
- user_boredom: float = Field(..., ge=0, le=3)
106
- user_confusion: float = Field(..., ge=0, le=3)
107
- user_engagement: float = Field(..., ge=0, le=3)
108
- user_frustration: float = Field(..., ge=0, le=3)
109
 
110
  class InsertResponse(BaseModel):
111
  status: str
@@ -246,38 +246,6 @@ async def health_check():
246
  "timestamp": datetime.now().isoformat()
247
  }
248
 
249
- @app.post("/predict", response_model=PredictionResponse)
250
- async def predict_emotion(file: UploadFile = File(...)):
251
- if not onnx_session:
252
- raise HTTPException(status_code=503, detail="Model not loaded")
253
-
254
- if not file.content_type.startswith('image/'):
255
- raise HTTPException(status_code=400, detail="File must be an image")
256
-
257
- try:
258
- # Load image
259
- image_bytes = await file.read()
260
- image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
261
-
262
- # Preprocess (SANS PyTorch !)
263
- input_tensor = preprocess_image(image)
264
-
265
- # Inference ONNX
266
- outputs = onnx_session.run(['output'], {'input': input_tensor})
267
- scores_array = outputs[0][0]
268
-
269
- return PredictionResponse(
270
- boredom=round(float(scores_array[0]), 2),
271
- confusion=round(float(scores_array[1]), 2),
272
- engagement=round(float(scores_array[2]), 2),
273
- frustration=round(float(scores_array[3]), 2),
274
- timestamp=datetime.now().isoformat()
275
- )
276
-
277
- except Exception as e:
278
- print(f"❌ Erreur prédiction : {e}")
279
- raise HTTPException(status_code=500, detail=str(e))
280
-
281
  @app.post("/predict", response_model=PredictionResponse)
282
  async def predict_emotion(file: UploadFile = File(...)):
283
  """
@@ -327,15 +295,21 @@ async def predict_emotion(file: UploadFile = File(...)):
327
  raise HTTPException(status_code=500, detail=str(e))
328
 
329
  @app.post("/insert", response_model=InsertResponse)
330
- async def insert_annotation(annotation: AnnotationInsert):
 
 
 
 
 
 
 
 
 
 
331
  """
332
  Insert annotation utilisateur
333
 
334
- Ce endpoint fait 2 choses :
335
- 1. Upload image vers Cloudflare R2
336
- 2. Insert labels (predicted + user) dans NeonDB
337
-
338
- ✅ Appelé uniquement quand l'utilisateur clique "Valider"
339
  """
340
 
341
  # Vérifications
@@ -345,19 +319,20 @@ async def insert_annotation(annotation: AnnotationInsert):
345
  if not s3_client:
346
  raise HTTPException(status_code=503, detail="Storage not available")
347
 
 
 
 
348
  try:
349
- # 1. Decode image base64
350
- try:
351
- image_bytes = base64.b64decode(annotation.image_base64)
352
- except Exception as e:
353
- raise HTTPException(status_code=400, detail=f"Invalid base64 image: {e}")
354
 
355
- # 2. Generate unique filename
356
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
357
- img_name = f"{timestamp}_{hash(annotation.image_base64) % 10000:04d}.jpg"
 
358
  s3_key = f"collected/{img_name}"
359
 
360
- # 3. Upload image to Cloudflare R2
361
  print(f"📤 Upload vers R2 : {s3_key}")
362
  try:
363
  s3_client.put_object(
@@ -371,7 +346,7 @@ async def insert_annotation(annotation: AnnotationInsert):
371
  print(f"❌ Erreur upload R2 : {e}")
372
  raise HTTPException(status_code=500, detail=f"R2 upload failed: {e}")
373
 
374
- # 4. Insert labels in NeonDB
375
  query = text("""
376
  INSERT INTO emotion_labels
377
  (img_name, s3_path,
@@ -389,30 +364,25 @@ async def insert_annotation(annotation: AnnotationInsert):
389
  conn.execute(query, {
390
  'img_name': img_name,
391
  's3_path': s3_key,
392
- 'pred_boredom': annotation.predicted_boredom,
393
- 'pred_confusion': annotation.predicted_confusion,
394
- 'pred_engagement': annotation.predicted_engagement,
395
- 'pred_frustration': annotation.predicted_frustration,
396
- 'user_boredom': annotation.user_boredom,
397
- 'user_confusion': annotation.user_confusion,
398
- 'user_engagement': annotation.user_engagement,
399
- 'user_frustration': annotation.user_frustration,
400
  'timestamp': datetime.now()
401
  })
402
  conn.commit()
403
 
404
  print(f"✅ Insert NeonDB réussi : {img_name}")
405
 
406
- # 5. Generate public URL (si tu as activé l'accès public)
407
- # public_url = f"https://pub-{R2_ACCOUNT_ID}.r2.dev/{s3_key}"
408
- # Ou None si pas d'accès public
409
- public_url = None
410
-
411
  return InsertResponse(
412
  status="success",
413
- message="Image uploaded to R2 and labels saved to NeonDB",
414
- img_name=img_name,
415
- s3_url=public_url
416
  )
417
 
418
  except SQLAlchemyError as e:
 
3
  ONNX Runtime UNIQUEMENT (pas de PyTorch)
4
  """
5
 
6
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Form
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from pydantic import BaseModel, Field
9
  from typing import List, Optional
 
96
  frustration: float = Field(..., ge=0, le=3)
97
  timestamp: str
98
 
99
+ # class AnnotationInsert(BaseModel):
100
+ # image_base64: str
101
+ # predicted_boredom: float = Field(..., ge=0, le=3)
102
+ # predicted_confusion: float = Field(..., ge=0, le=3)
103
+ # predicted_engagement: float = Field(..., ge=0, le=3)
104
+ # predicted_frustration: float = Field(..., ge=0, le=3)
105
+ # user_boredom: float = Field(..., ge=0, le=3)
106
+ # user_confusion: float = Field(..., ge=0, le=3)
107
+ # user_engagement: float = Field(..., ge=0, le=3)
108
+ # user_frustration: float = Field(..., ge=0, le=3)
109
 
110
  class InsertResponse(BaseModel):
111
  status: str
 
246
  "timestamp": datetime.now().isoformat()
247
  }
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  @app.post("/predict", response_model=PredictionResponse)
250
  async def predict_emotion(file: UploadFile = File(...)):
251
  """
 
295
  raise HTTPException(status_code=500, detail=str(e))
296
 
297
  @app.post("/insert", response_model=InsertResponse)
298
+ async def insert_annotation(
299
+ file: UploadFile = File(...),
300
+ predicted_boredom: float = Form(...),
301
+ predicted_confusion: float = Form(...),
302
+ predicted_engagement: float = Form(...),
303
+ predicted_frustration: float = Form(...),
304
+ user_boredom: float = Form(...),
305
+ user_confusion: float = Form(...),
306
+ user_engagement: float = Form(...),
307
+ user_frustration: float = Form(...)
308
+ ):
309
  """
310
  Insert annotation utilisateur
311
 
312
+ NOUVEAU : Reçoit directement l'image (pas de base64)
 
 
 
 
313
  """
314
 
315
  # Vérifications
 
319
  if not s3_client:
320
  raise HTTPException(status_code=503, detail="Storage not available")
321
 
322
+ if not file.content_type.startswith('image/'):
323
+ raise HTTPException(status_code=400, detail="File must be an image")
324
+
325
  try:
326
+ # 1. Lire l'image
327
+ image_bytes = await file.read()
 
 
 
328
 
329
+ # 2. Générer nom unique
330
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
331
+ random_suffix = hash(image_bytes) % 10000
332
+ img_name = f"{timestamp}_{random_suffix:04d}.jpg"
333
  s3_key = f"collected/{img_name}"
334
 
335
+ # 3. Upload vers Cloudflare R2
336
  print(f"📤 Upload vers R2 : {s3_key}")
337
  try:
338
  s3_client.put_object(
 
346
  print(f"❌ Erreur upload R2 : {e}")
347
  raise HTTPException(status_code=500, detail=f"R2 upload failed: {e}")
348
 
349
+ # 4. Insert dans NeonDB avec img_name
350
  query = text("""
351
  INSERT INTO emotion_labels
352
  (img_name, s3_path,
 
364
  conn.execute(query, {
365
  'img_name': img_name,
366
  's3_path': s3_key,
367
+ 'pred_boredom': predicted_boredom,
368
+ 'pred_confusion': predicted_confusion,
369
+ 'pred_engagement': predicted_engagement,
370
+ 'pred_frustration': predicted_frustration,
371
+ 'user_boredom': user_boredom,
372
+ 'user_confusion': user_confusion,
373
+ 'user_engagement': user_engagement,
374
+ 'user_frustration': user_frustration,
375
  'timestamp': datetime.now()
376
  })
377
  conn.commit()
378
 
379
  print(f"✅ Insert NeonDB réussi : {img_name}")
380
 
 
 
 
 
 
381
  return InsertResponse(
382
  status="success",
383
+ message="Image uploaded and labels saved",
384
+ img_name=img_name, # ← RETOURNÉ au frontend
385
+ s3_url=None
386
  )
387
 
388
  except SQLAlchemyError as e: