mohamedtsou commited on
Commit
d049900
·
verified ·
1 Parent(s): dcdefd8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -411
app.py CHANGED
@@ -10,458 +10,189 @@ import os
10
  import logging
11
  import requests
12
 
13
- # Initialisation
14
  app = Flask(__name__)
15
- CORS(app)
16
 
17
- # Configuration pour HF Spaces
18
- HF_SPACE = os.environ.get('SPACE_ID') is not None
19
- PORT = 7860 if HF_SPACE else int(os.environ.get('PORT', 5000))
20
 
21
- # Setup logging
22
  logging.basicConfig(level=logging.INFO)
23
- logger = logging.getLogger(__name__)
24
 
25
- def decode_barcode(image_bytes):
26
- """Fonction principale de décodage"""
 
 
 
 
 
 
 
27
  try:
28
- # Essayer avec PIL d'abord (plus léger)
29
  try:
30
  pil_image = Image.open(io.BytesIO(image_bytes))
31
  barcodes = decode(pil_image)
32
-
33
  if barcodes:
34
- barcode = barcodes[0]
35
  return {
36
- 'success': True,
37
- 'barcode': barcode.data.decode('utf-8'),
38
- 'type': barcode.type,
39
- 'count': len(barcodes),
40
- 'method': 'pil'
41
  }
42
  except Exception as e:
43
- logger.debug(f"PIL method: {e}")
44
-
45
- # Essayer avec OpenCV si disponible
46
  try:
47
  nparr = np.frombuffer(image_bytes, np.uint8)
48
  img = cv2.imdecode(nparr, cv2.IMREAD_GRAYSCALE)
49
-
50
  if img is not None:
51
  barcodes = decode(img)
52
  if barcodes:
53
- barcode = barcodes[0]
54
  return {
55
- 'success': True,
56
- 'barcode': barcode.data.decode('utf-8'),
57
- 'type': barcode.type,
58
- 'count': len(barcodes),
59
- 'method': 'opencv'
60
  }
61
-
62
- # Améliorer le contraste et réessayer
63
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
64
  enhanced = clahe.apply(img)
65
  barcodes = decode(enhanced)
66
-
67
  if barcodes:
68
- barcode = barcodes[0]
69
  return {
70
- 'success': True,
71
- 'barcode': barcode.data.decode('utf-8'),
72
- 'type': barcode.type,
73
- 'count': len(barcodes),
74
- 'method': 'enhanced'
75
  }
76
  except Exception as e:
77
- logger.debug(f"OpenCV method: {e}")
78
-
79
- return {'success': False, 'error': 'Aucun code-barres détecté'}
80
-
81
- except Exception as e:
82
- logger.error(f"Decode error: {e}")
83
- return {'success': False, 'error': str(e)}
84
 
85
- # ========== API POUR FLUTTER (Compatibilité) ==========
86
 
87
- @app.route('/api/decode-barcode', methods=['POST'])
88
- def decode_barcode_api():
89
- """Compatibilité avec le code Flutter existant"""
90
- try:
91
- if not request.is_json:
92
- return jsonify({'success': False, 'error': 'Content-Type must be application/json'}), 400
93
-
94
- data = request.get_json()
95
- if not data or 'image' not in data:
96
- return jsonify({'success': False, 'error': 'No image provided'}), 400
97
-
98
- image_data = data['image']
99
- if ',' in image_data:
100
- image_data = image_data.split(',')[1]
101
-
102
- # Limiter la taille
103
- if len(image_data) > 3 * 1024 * 1024:
104
- return jsonify({'success': False, 'error': 'Image too large (max 3MB)'}), 400
105
-
106
- image_bytes = base64.b64decode(image_data)
107
- result = decode_barcode(image_bytes)
108
-
109
- return jsonify(result)
110
-
111
  except Exception as e:
112
- logger.error(f"Decode barcode error: {e}")
113
- return jsonify({'success': False, 'error': str(e)}), 500
114
 
115
- @app.route('/api/product-info/<barcode>', methods=['GET'])
116
- def product_info_api(barcode):
117
- """Compatibilité avec le code Flutter existant"""
118
- try:
119
- # Base de données locale
120
- products = {
121
- '5901234123457': {
122
- 'name': 'Lait UHT Demi-écrémé',
123
- 'brand': 'Candia',
124
- 'category': 'Alimentation',
125
- 'price': 1.20,
126
- 'description': 'Lait stérilisé UHT'
127
- },
128
- '9780201379624': {
129
- 'name': 'Flutter Cookbook',
130
- 'brand': "O'Reilly",
131
- 'category': 'Livres',
132
- 'price': 45.99,
133
- 'description': 'Guide de développement Flutter'
134
- },
135
- '3017620422003': {
136
- 'name': 'Nutella',
137
- 'brand': 'Ferrero',
138
- 'category': 'Alimentation',
139
- 'price': 4.99,
140
- 'description': 'Pâte à tartiner aux noisettes'
141
- },
142
- '1234567890123': {
143
- 'name': 'iPhone 13 Pro',
144
- 'brand': 'Apple',
145
- 'category': 'Électronique',
146
- 'price': 1099.99,
147
- 'description': 'Smartphone Apple avec écran Super Retina XDR'
148
- }
 
 
 
 
 
149
  }
150
-
151
- if barcode in products:
152
- return jsonify({
153
- 'success': True,
154
- 'product': products[barcode]
155
- })
156
-
157
- # Essayer OpenFoodFacts
158
- try:
159
- response = requests.get(
160
- f'https://world.openfoodfacts.org/api/v0/product/{barcode}.json',
161
- timeout=3
162
- )
163
-
164
- if response.status_code == 200:
165
- data = response.json()
166
- if data.get('status') == 1:
167
- product = data.get('product', {})
168
- return jsonify({
169
- 'success': True,
170
- 'product': {
171
- 'name': product.get('product_name', 'Produit inconnu'),
172
- 'brand': product.get('brands', 'Marque inconnue'),
173
- 'category': product.get('categories', 'Non catégorisé'),
174
- 'description': product.get('generic_name', ''),
175
- 'quantity': product.get('quantity', ''),
176
- 'image_url': product.get('image_url', '')
177
- },
178
- 'source': 'openfoodfacts'
179
- })
180
- except Exception as e:
181
- logger.debug(f"OpenFoodFacts error: {e}")
182
-
183
- return jsonify({
184
- 'success': False,
185
- 'error': 'Produit non trouvé',
186
- 'barcode': barcode
187
- })
188
-
189
- except Exception as e:
190
- logger.error(f"Product info error: {e}")
191
- return jsonify({'success': False, 'error': str(e)}), 500
192
-
193
- # ========== API STANDARD ==========
194
-
195
- @app.route('/api/scan', methods=['POST', 'GET'])
196
- def scan():
197
- """Endpoint principal - supporte GET pour l'interface web"""
198
- if request.method == 'GET':
199
- return '''
200
- <!DOCTYPE html>
201
- <html>
202
- <head>
203
- <title>Barcode Scanner</title>
204
- <style>
205
- body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
206
- .container { background: #f5f5f5; padding: 20px; border-radius: 10px; }
207
- input, button { margin: 10px 0; padding: 10px; width: 100%; }
208
- #preview { max-width: 300px; margin: 20px 0; }
209
- #result { margin-top: 20px; padding: 10px; background: white; border-radius: 5px; }
210
- </style>
211
- </head>
212
- <body>
213
- <div class="container">
214
- <h1>📷 Barcode Scanner</h1>
215
- <input type="file" id="imageInput" accept="image/*" capture="environment">
216
- <video id="preview" style="display:none;"></video>
217
- <button onclick="captureImage()">📸 Prendre une photo</button>
218
- <button onclick="uploadImage()">📁 Uploader une image</button>
219
- <div id="result"></div>
220
- </div>
221
-
222
- <script>
223
- let currentImage = null;
224
-
225
- async function startCamera() {
226
- const video = document.getElementById('preview');
227
- try {
228
- const stream = await navigator.mediaDevices.getUserMedia({
229
- video: { facingMode: 'environment' }
230
- });
231
- video.srcObject = stream;
232
- video.style.display = 'block';
233
- video.play();
234
- return video;
235
- } catch (err) {
236
- alert('Erreur caméra: ' + err.message);
237
- }
238
- }
239
-
240
- async function captureImage() {
241
- const video = document.getElementById('preview');
242
- if (!video.srcObject) {
243
- await startCamera();
244
- return;
245
- }
246
-
247
- const canvas = document.createElement('canvas');
248
- canvas.width = video.videoWidth;
249
- canvas.height = video.videoHeight;
250
- const ctx = canvas.getContext('2d');
251
- ctx.drawImage(video, 0, 0);
252
-
253
- currentImage = canvas.toDataURL('image/jpeg');
254
- scanBarcode(currentImage);
255
- }
256
-
257
- function uploadImage() {
258
- const input = document.getElementById('imageInput');
259
- input.click();
260
- input.onchange = function(e) {
261
- const file = e.target.files[0];
262
- const reader = new FileReader();
263
- reader.onload = function(event) {
264
- currentImage = event.target.result;
265
- scanBarcode(currentImage);
266
- };
267
- reader.readAsDataURL(file);
268
- };
269
- }
270
-
271
- async function scanBarcode(imageData) {
272
- const resultDiv = document.getElementById('result');
273
- resultDiv.innerHTML = '⌛ Analyse en cours...';
274
-
275
- try {
276
- const response = await fetch('/api/scan', {
277
- method: 'POST',
278
- headers: {'Content-Type': 'application/json'},
279
- body: JSON.stringify({ image: imageData })
280
- });
281
-
282
- const data = await response.json();
283
-
284
- if (data.success) {
285
- resultDiv.innerHTML = `
286
- <h3>✅ Code-barres trouvé !</h3>
287
- <p><strong>Code:</strong> ${data.barcode}</p>
288
- <p><strong>Type:</strong> ${data.type}</p>
289
- <button onclick="getProductInfo('${data.barcode}')">
290
- 🔍 Voir les infos produit
291
- </button>
292
- `;
293
- } else {
294
- resultDiv.innerHTML = `
295
- <h3>❌ Aucun code-barres détecté</h3>
296
- <p>Erreur: ${data.error}</p>
297
- <p>Essayez avec une image plus claire.</p>
298
- `;
299
- }
300
- } catch (error) {
301
- resultDiv.innerHTML = `❌ Erreur: ${error.message}`;
302
- }
303
- }
304
-
305
- async function getProductInfo(barcode) {
306
- const resultDiv = document.getElementById('result');
307
- resultDiv.innerHTML += '<p>🔍 Recherche des infos produit...</p>';
308
-
309
- try {
310
- const response = await fetch(`/api/product/${barcode}`);
311
- const data = await response.json();
312
-
313
- if (data.success) {
314
- const product = data.product;
315
- resultDiv.innerHTML += `
316
- <h4>📦 Informations produit:</h4>
317
- <p><strong>Nom:</strong> ${product.name || 'Inconnu'}</p>
318
- <p><strong>Marque:</strong> ${product.brand || 'Inconnue'}</p>
319
- <p><strong>Catégorie:</strong> ${product.category || 'Non catégorisé'}</p>
320
- ${product.price ? `<p><strong>Prix:</strong> ${product.price} €</p>` : ''}
321
- ${product.description ? `<p><strong>Description:</strong> ${product.description}</p>` : ''}
322
- `;
323
- } else {
324
- resultDiv.innerHTML += `<p>ℹ️ ${data.error}</p>`;
325
- }
326
- } catch (error) {
327
- resultDiv.innerHTML += `<p>❌ Erreur recherche: ${error.message}</p>`;
328
- }
329
- }
330
-
331
- window.onload = startCamera;
332
- </script>
333
- </body>
334
- </html>
335
- '''
336
-
337
- # POST request - API endpoint
338
- try:
339
- if not request.is_json:
340
- return jsonify({'success': False, 'error': 'Content-Type must be application/json'}), 400
341
-
342
- data = request.get_json()
343
- if not data or 'image' not in data:
344
- return jsonify({'success': False, 'error': 'No image provided'}), 400
345
-
346
- image_data = data['image']
347
- if ',' in image_data:
348
- image_data = image_data.split(',')[1]
349
-
350
- # Limiter la taille
351
- if len(image_data) > 3 * 1024 * 1024:
352
- return jsonify({'success': False, 'error': 'Image too large (max 3MB)'}), 400
353
-
354
- image_bytes = base64.b64decode(image_data)
355
- result = decode_barcode(image_bytes)
356
-
357
- return jsonify(result)
358
-
359
- except Exception as e:
360
- logger.error(f"Scan error: {e}")
361
- return jsonify({'success': False, 'error': str(e)}), 500
362
 
363
- @app.route('/api/product/<barcode>', methods=['GET'])
364
- def product_info(barcode):
365
- """Obtenir les infos produit"""
366
  try:
367
- # Base de données locale
368
- products = {
369
- '5901234123457': {
370
- 'name': 'Lait UHT Demi-écrémé',
371
- 'brand': 'Candia',
372
- 'category': 'Alimentation',
373
- 'price': 1.20,
374
- 'description': 'Lait stérilisé UHT'
375
- },
376
- '9780201379624': {
377
- 'name': 'Flutter Cookbook',
378
- 'brand': "O'Reilly",
379
- 'category': 'Livres',
380
- 'price': 45.99,
381
- 'description': 'Guide de développement Flutter'
382
- },
383
- '3017620422003': {
384
- 'name': 'Nutella',
385
- 'brand': 'Ferrero',
386
- 'category': 'Alimentation',
387
- 'price': 4.99,
388
- 'description': 'Pâte à tartiner aux noisettes'
389
- }
390
- }
391
-
392
- if barcode in products:
393
- return jsonify({
394
- 'success': True,
395
- 'product': products[barcode],
396
- 'source': 'local'
397
- })
398
-
399
- # Essayer OpenFoodFacts
400
- try:
401
- response = requests.get(
402
- f'https://world.openfoodfacts.org/api/v0/product/{barcode}.json',
403
- timeout=3
404
- )
405
-
406
- if response.status_code == 200:
407
- data = response.json()
408
- if data.get('status') == 1:
409
- product = data.get('product', {})
410
- return jsonify({
411
- 'success': True,
412
- 'product': {
413
- 'name': product.get('product_name', 'Produit inconnu'),
414
- 'brand': product.get('brands', 'Marque inconnue'),
415
- 'category': product.get('categories', 'Non catégorisé'),
416
- 'description': product.get('generic_name', ''),
417
- 'image_url': product.get('image_url', '')
418
- },
419
- 'source': 'openfoodfacts'
420
- })
421
- except Exception as e:
422
- logger.debug(f"OpenFoodFacts error: {e}")
423
-
424
- return jsonify({
425
- 'success': False,
426
- 'error': 'Produit non trouvé',
427
- 'barcode': barcode
428
- })
429
-
430
  except Exception as e:
431
- logger.error(f"Product info error: {e}")
432
- return jsonify({'success': False, 'error': str(e)}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
 
434
- @app.route('/api/health', methods=['GET'])
 
 
 
 
 
435
  def health():
436
- """Health check endpoint"""
437
  return jsonify({
438
- 'status': 'healthy',
439
- 'service': 'Barcode Scanner API',
440
- 'version': '2.0',
441
- 'deployed_on': 'Hugging Face Spaces' if HF_SPACE else 'Local',
442
- 'endpoints': {
443
- '/api/decode-barcode': 'POST - Scanner code-barres (Flutter)',
444
- '/api/product-info/<barcode>': 'GET - Infos produit (Flutter)',
445
- '/api/scan': 'POST/GET - Scanner code-barres',
446
- '/api/product/<barcode>': 'GET - Infos produit',
447
- '/api/health': 'GET - Health check'
448
- }
449
  })
450
 
451
- @app.route('/')
452
  def home():
453
- """Page d'accueil"""
454
  return jsonify({
455
- 'message': 'Barcode Scanner API',
456
- 'documentation': {
457
- 'decode-barcode': 'POST /api/decode-barcode - Scanner un code-barres (Flutter)',
458
- 'product-info': 'GET /api/product-info/<barcode> - Infos produit (Flutter)',
459
- 'scan': 'POST /api/scan - Scanner un code-barres',
460
- 'product': 'GET /api/product/<barcode> - Infos produit',
461
- 'health': 'GET /api/health - Health check'
462
- },
463
- 'web_interface': 'Visitez /api/scan pour l\'interface web'
464
  })
465
 
466
- if __name__ == '__main__':
467
- app.run(host='0.0.0.0', port=PORT, debug=False)
 
 
 
10
  import logging
11
  import requests
12
 
13
+ # ================== INITIALISATION ==================
14
  app = Flask(__name__)
15
+ CORS(app, resources={r"/api/*": {"origins": "*"}})
16
 
17
+ HF_SPACE = os.environ.get("SPACE_ID") is not None
18
+ PORT = 7860 if HF_SPACE else int(os.environ.get("PORT", 5000))
 
19
 
 
20
  logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger("barcode-api")
22
 
23
+ # ================== UTILS ==================
24
+
25
+ def safe_b64decode(data: str) -> bytes:
26
+ """Base64 decode safe (Flutter/Web compatible)"""
27
+ return base64.b64decode(data + "=" * (-len(data) % 4))
28
+
29
+
30
+ def decode_barcode(image_bytes: bytes) -> dict:
31
+ """Decode barcode using PIL then OpenCV"""
32
  try:
33
+ # --- PIL first ---
34
  try:
35
  pil_image = Image.open(io.BytesIO(image_bytes))
36
  barcodes = decode(pil_image)
 
37
  if barcodes:
38
+ b = barcodes[0]
39
  return {
40
+ "success": True,
41
+ "barcode": b.data.decode("utf-8"),
42
+ "type": b.type,
43
+ "method": "pil"
 
44
  }
45
  except Exception as e:
46
+ logger.debug(f"PIL failed: {e}")
47
+
48
+ # --- OpenCV fallback ---
49
  try:
50
  nparr = np.frombuffer(image_bytes, np.uint8)
51
  img = cv2.imdecode(nparr, cv2.IMREAD_GRAYSCALE)
 
52
  if img is not None:
53
  barcodes = decode(img)
54
  if barcodes:
55
+ b = barcodes[0]
56
  return {
57
+ "success": True,
58
+ "barcode": b.data.decode("utf-8"),
59
+ "type": b.type,
60
+ "method": "opencv"
 
61
  }
62
+
63
+ # Contrast enhancement
64
+ clahe = cv2.createCLAHE(2.0, (8, 8))
65
  enhanced = clahe.apply(img)
66
  barcodes = decode(enhanced)
 
67
  if barcodes:
68
+ b = barcodes[0]
69
  return {
70
+ "success": True,
71
+ "barcode": b.data.decode("utf-8"),
72
+ "type": b.type,
73
+ "method": "enhanced"
 
74
  }
75
  except Exception as e:
76
+ logger.debug(f"OpenCV failed: {e}")
 
 
 
 
 
 
77
 
78
+ return {"success": False, "error": "No barcode detected"}
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  except Exception as e:
81
+ logger.error(e)
82
+ return {"success": False, "error": str(e)}
83
 
84
+ # ================== FLUTTER API ==================
85
+
86
+ @app.route("/api/decode-barcode", methods=["POST"])
87
+ def api_decode_barcode():
88
+ if not request.is_json:
89
+ return jsonify({"success": False, "error": "JSON required"}), 400
90
+
91
+ data = request.get_json()
92
+ image_data = data.get("image")
93
+ if not image_data:
94
+ return jsonify({"success": False, "error": "Image missing"}), 400
95
+
96
+ if "," in image_data:
97
+ image_data = image_data.split(",")[1]
98
+
99
+ if len(image_data) > 3 * 1024 * 1024:
100
+ return jsonify({"success": False, "error": "Image too large"}), 400
101
+
102
+ image_bytes = safe_b64decode(image_data)
103
+ result = decode_barcode(image_bytes)
104
+ return jsonify(result)
105
+
106
+
107
+ @app.route("/api/product-info/<barcode>", methods=["GET"])
108
+ def api_product_info(barcode):
109
+ local_db = {
110
+ "3017620422003": {
111
+ "name": "Nutella",
112
+ "brand": "Ferrero",
113
+ "category": "Food",
114
+ "price": 4.99,
115
+ "description": "Hazelnut spread"
116
+ },
117
+ "5901234123457": {
118
+ "name": "Milk UHT",
119
+ "brand": "Candia",
120
+ "category": "Food",
121
+ "price": 1.20,
122
+ "description": "UHT milk"
123
  }
124
+ }
125
+
126
+ if barcode in local_db:
127
+ return jsonify({"success": True, "product": local_db[barcode], "source": "local"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ # OpenFoodFacts fallback
 
 
130
  try:
131
+ r = requests.get(
132
+ f"https://world.openfoodfacts.org/api/v0/product/{barcode}.json",
133
+ timeout=4
134
+ )
135
+ if r.status_code == 200:
136
+ data = r.json()
137
+ if data.get("status") == 1:
138
+ p = data["product"]
139
+ return jsonify({
140
+ "success": True,
141
+ "source": "openfoodfacts",
142
+ "product": {
143
+ "name": p.get("product_name"),
144
+ "brand": p.get("brands"),
145
+ "category": p.get("categories"),
146
+ "description": p.get("generic_name"),
147
+ "image": p.get("image_url")
148
+ }
149
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  except Exception as e:
151
+ logger.debug(e)
152
+
153
+ return jsonify({"success": False, "error": "Product not found"}), 404
154
+
155
+ # ================== WEB SCAN ==================
156
+
157
+ @app.route("/api/scan", methods=["POST"])
158
+ def api_scan():
159
+ if not request.is_json:
160
+ return jsonify({"success": False, "error": "JSON required"}), 400
161
+
162
+ image_data = request.json.get("image")
163
+ if not image_data:
164
+ return jsonify({"success": False, "error": "Image missing"}), 400
165
+
166
+ if "," in image_data:
167
+ image_data = image_data.split(",")[1]
168
 
169
+ image_bytes = safe_b64decode(image_data)
170
+ return jsonify(decode_barcode(image_bytes))
171
+
172
+ # ================== HEALTH ==================
173
+
174
+ @app.route("/api/health")
175
  def health():
 
176
  return jsonify({
177
+ "status": "ok",
178
+ "service": "Barcode Scanner API",
179
+ "platform": "HuggingFace" if HF_SPACE else "Local",
180
+ "endpoints": [
181
+ "/api/decode-barcode",
182
+ "/api/product-info/<barcode>",
183
+ "/api/scan",
184
+ "/api/health"
185
+ ]
 
 
186
  })
187
 
188
+ @app.route("/")
189
  def home():
 
190
  return jsonify({
191
+ "message": "Barcode Scanner API",
192
+ "use": "/api/decode-barcode (POST)"
 
 
 
 
 
 
 
193
  })
194
 
195
+ # ================== MAIN ==================
196
+
197
+ if __name__ == "__main__":
198
+ app.run(host="0.0.0.0", port=PORT, debug=False)