mohamedtsou commited on
Commit
09ec0b8
·
verified ·
1 Parent(s): c8db785

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +354 -0
app.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from flask_cors import CORS
3
+ import cv2
4
+ import numpy as np
5
+ import base64
6
+ import io
7
+ from pyzbar.pyzbar import decode
8
+ from PIL import Image
9
+ 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
+ @app.route('/api/scan', methods=['POST', 'GET'])
86
+ def scan():
87
+ """Endpoint principal - supporte GET pour l'interface web"""
88
+ if request.method == 'GET':
89
+ return '''
90
+ <!DOCTYPE html>
91
+ <html>
92
+ <head>
93
+ <title>Barcode Scanner</title>
94
+ <style>
95
+ body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
96
+ .container { background: #f5f5f5; padding: 20px; border-radius: 10px; }
97
+ input, button { margin: 10px 0; padding: 10px; width: 100%; }
98
+ #preview { max-width: 300px; margin: 20px 0; }
99
+ #result { margin-top: 20px; padding: 10px; background: white; border-radius: 5px; }
100
+ </style>
101
+ </head>
102
+ <body>
103
+ <div class="container">
104
+ <h1>📷 Barcode Scanner</h1>
105
+ <input type="file" id="imageInput" accept="image/*" capture="environment">
106
+ <video id="preview" style="display:none;"></video>
107
+ <button onclick="captureImage()">📸 Prendre une photo</button>
108
+ <button onclick="uploadImage()">📁 Uploader une image</button>
109
+ <div id="result"></div>
110
+ </div>
111
+
112
+ <script>
113
+ let currentImage = null;
114
+
115
+ // Accéder à la caméra
116
+ async function startCamera() {
117
+ const video = document.getElementById('preview');
118
+ try {
119
+ const stream = await navigator.mediaDevices.getUserMedia({
120
+ video: { facingMode: 'environment' }
121
+ });
122
+ video.srcObject = stream;
123
+ video.style.display = 'block';
124
+ video.play();
125
+ return video;
126
+ } catch (err) {
127
+ alert('Erreur caméra: ' + err.message);
128
+ }
129
+ }
130
+
131
+ // Capturer une image
132
+ async function captureImage() {
133
+ const video = document.getElementById('preview');
134
+ if (!video.srcObject) {
135
+ await startCamera();
136
+ return;
137
+ }
138
+
139
+ const canvas = document.createElement('canvas');
140
+ canvas.width = video.videoWidth;
141
+ canvas.height = video.videoHeight;
142
+ const ctx = canvas.getContext('2d');
143
+ ctx.drawImage(video, 0, 0);
144
+
145
+ currentImage = canvas.toDataURL('image/jpeg');
146
+ scanBarcode(currentImage);
147
+ }
148
+
149
+ // Uploader une image
150
+ function uploadImage() {
151
+ const input = document.getElementById('imageInput');
152
+ input.click();
153
+ input.onchange = function(e) {
154
+ const file = e.target.files[0];
155
+ const reader = new FileReader();
156
+ reader.onload = function(event) {
157
+ currentImage = event.target.result;
158
+ scanBarcode(currentImage);
159
+ };
160
+ reader.readAsDataURL(file);
161
+ };
162
+ }
163
+
164
+ // Scanner le code-barres
165
+ async function scanBarcode(imageData) {
166
+ const resultDiv = document.getElementById('result');
167
+ resultDiv.innerHTML = '⌛ Analyse en cours...';
168
+
169
+ try {
170
+ const response = await fetch('/api/scan', {
171
+ method: 'POST',
172
+ headers: {'Content-Type': 'application/json'},
173
+ body: JSON.stringify({ image: imageData })
174
+ });
175
+
176
+ const data = await response.json();
177
+
178
+ if (data.success) {
179
+ resultDiv.innerHTML = `
180
+ <h3>✅ Code-barres trouvé !</h3>
181
+ <p><strong>Code:</strong> ${data.barcode}</p>
182
+ <p><strong>Type:</strong> ${data.type}</p>
183
+ <button onclick="getProductInfo('${data.barcode}')">
184
+ 🔍 Voir les infos produit
185
+ </button>
186
+ `;
187
+ } else {
188
+ resultDiv.innerHTML = `
189
+ <h3>❌ Aucun code-barres détecté</h3>
190
+ <p>Erreur: ${data.error}</p>
191
+ <p>Essayez avec une image plus claire.</p>
192
+ `;
193
+ }
194
+ } catch (error) {
195
+ resultDiv.innerHTML = `❌ Erreur: ${error.message}`;
196
+ }
197
+ }
198
+
199
+ // Obtenir les infos produit
200
+ async function getProductInfo(barcode) {
201
+ const resultDiv = document.getElementById('result');
202
+ resultDiv.innerHTML += '<p>🔍 Recherche des infos produit...</p>';
203
+
204
+ try {
205
+ const response = await fetch(`/api/product/${barcode}`);
206
+ const data = await response.json();
207
+
208
+ if (data.success) {
209
+ const product = data.product;
210
+ resultDiv.innerHTML += `
211
+ <h4>📦 Informations produit:</h4>
212
+ <p><strong>Nom:</strong> ${product.name || 'Inconnu'}</p>
213
+ <p><strong>Marque:</strong> ${product.brand || 'Inconnue'}</p>
214
+ <p><strong>Catégorie:</strong> ${product.category || 'Non catégorisé'}</p>
215
+ ${product.price ? `<p><strong>Prix:</strong> ${product.price} €</p>` : ''}
216
+ ${product.description ? `<p><strong>Description:</strong> ${product.description}</p>` : ''}
217
+ `;
218
+ } else {
219
+ resultDiv.innerHTML += `<p>ℹ️ ${data.error}</p>`;
220
+ }
221
+ } catch (error) {
222
+ resultDiv.innerHTML += `<p>❌ Erreur recherche: ${error.message}</p>`;
223
+ }
224
+ }
225
+
226
+ // Démarrer la caméra au chargement
227
+ window.onload = startCamera;
228
+ </script>
229
+ </body>
230
+ </html>
231
+ '''
232
+
233
+ # POST request - API endpoint
234
+ try:
235
+ if not request.is_json:
236
+ return jsonify({'success': False, 'error': 'Content-Type must be application/json'}), 400
237
+
238
+ data = request.get_json()
239
+ if not data or 'image' not in data:
240
+ return jsonify({'success': False, 'error': 'No image provided'}), 400
241
+
242
+ image_data = data['image']
243
+ if ',' in image_data:
244
+ image_data = image_data.split(',')[1]
245
+
246
+ # Limiter la taille pour HF Spaces
247
+ if len(image_data) > 3 * 1024 * 1024: # 3MB max
248
+ return jsonify({'success': False, 'error': 'Image too large (max 3MB)'}), 400
249
+
250
+ image_bytes = base64.b64decode(image_data)
251
+ result = decode_barcode(image_bytes)
252
+
253
+ return jsonify(result)
254
+
255
+ except Exception as e:
256
+ logger.error(f"Scan error: {e}")
257
+ return jsonify({'success': False, 'error': str(e)}), 500
258
+
259
+ @app.route('/api/product/<barcode>', methods=['GET'])
260
+ def product_info(barcode):
261
+ """Obtenir les infos produit"""
262
+ try:
263
+ # Base de données locale
264
+ products = {
265
+ '5901234123457': {
266
+ 'name': 'Lait UHT Demi-écrémé',
267
+ 'brand': 'Candia',
268
+ 'category': 'Alimentation',
269
+ 'price': 1.20,
270
+ 'description': 'Lait stérilisé UHT'
271
+ },
272
+ '9780201379624': {
273
+ 'name': 'Flutter Cookbook',
274
+ 'brand': "O'Reilly",
275
+ 'category': 'Livres',
276
+ 'price': 45.99,
277
+ 'description': 'Guide de développement Flutter'
278
+ },
279
+ '3017620422003': {
280
+ 'name': 'Nutella',
281
+ 'brand': 'Ferrero',
282
+ 'category': 'Alimentation',
283
+ 'price': 4.99,
284
+ 'description': 'Pâte à tartiner aux noisettes'
285
+ }
286
+ }
287
+
288
+ if barcode in products:
289
+ return jsonify({
290
+ 'success': True,
291
+ 'product': products[barcode],
292
+ 'source': 'local'
293
+ })
294
+
295
+ # Essayer OpenFoodFacts
296
+ try:
297
+ response = requests.get(
298
+ f'https://world.openfoodfacts.org/api/v0/product/{barcode}.json',
299
+ timeout=3
300
+ )
301
+
302
+ if response.status_code == 200:
303
+ data = response.json()
304
+ if data.get('status') == 1:
305
+ product = data.get('product', {})
306
+ return jsonify({
307
+ 'success': True,
308
+ 'product': {
309
+ 'name': product.get('product_name', 'Produit inconnu'),
310
+ 'brand': product.get('brands', 'Marque inconnue'),
311
+ 'category': product.get('categories', 'Non catégorisé'),
312
+ 'description': product.get('generic_name', ''),
313
+ 'image_url': product.get('image_url', '')
314
+ },
315
+ 'source': 'openfoodfacts'
316
+ })
317
+ except Exception as e:
318
+ logger.debug(f"OpenFoodFacts error: {e}")
319
+
320
+ return jsonify({
321
+ 'success': False,
322
+ 'error': 'Produit non trouvé',
323
+ 'barcode': barcode
324
+ })
325
+
326
+ except Exception as e:
327
+ logger.error(f"Product info error: {e}")
328
+ return jsonify({'success': False, 'error': str(e)}), 500
329
+
330
+ @app.route('/api/health', methods=['GET'])
331
+ def health():
332
+ """Health check endpoint"""
333
+ return jsonify({
334
+ 'status': 'healthy',
335
+ 'service': 'Barcode Scanner API',
336
+ 'version': '2.0',
337
+ 'deployed_on': 'Hugging Face Spaces' if HF_SPACE else 'Local'
338
+ })
339
+
340
+ @app.route('/')
341
+ def home():
342
+ """Page d'accueil"""
343
+ return jsonify({
344
+ 'message': 'Barcode Scanner API',
345
+ 'documentation': {
346
+ 'scan': 'POST /api/scan - Scanner un code-barres',
347
+ 'product': 'GET /api/product/<barcode> - Infos produit',
348
+ 'health': 'GET /api/health - Health check'
349
+ },
350
+ 'web_interface': 'Visitez /api/scan pour l\'interface web'
351
+ })
352
+
353
+ if __name__ == '__main__':
354
+ app.run(host='0.0.0.0', port=PORT, debug=False)