Spaces:
Sleeping
Sleeping
| from flask import Flask, request, jsonify | |
| from flask_cors import CORS | |
| import cv2 | |
| import numpy as np | |
| import base64 | |
| import io | |
| from pyzbar.pyzbar import decode | |
| from PIL import Image | |
| import os | |
| import logging | |
| import requests | |
| # Initialisation | |
| app = Flask(__name__) | |
| CORS(app) | |
| # Configuration pour HF Spaces | |
| HF_SPACE = os.environ.get('SPACE_ID') is not None | |
| PORT = 7860 if HF_SPACE else int(os.environ.get('PORT', 5000)) | |
| # Setup logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| def decode_barcode(image_bytes): | |
| """Fonction principale de décodage""" | |
| try: | |
| # Essayer avec PIL d'abord (plus léger) | |
| try: | |
| pil_image = Image.open(io.BytesIO(image_bytes)) | |
| barcodes = decode(pil_image) | |
| if barcodes: | |
| barcode = barcodes[0] | |
| return { | |
| 'success': True, | |
| 'barcode': barcode.data.decode('utf-8'), | |
| 'type': barcode.type, | |
| 'count': len(barcodes), | |
| 'method': 'pil' | |
| } | |
| except Exception as e: | |
| logger.debug(f"PIL method: {e}") | |
| # Essayer avec OpenCV si disponible | |
| try: | |
| nparr = np.frombuffer(image_bytes, np.uint8) | |
| img = cv2.imdecode(nparr, cv2.IMREAD_GRAYSCALE) | |
| if img is not None: | |
| barcodes = decode(img) | |
| if barcodes: | |
| barcode = barcodes[0] | |
| return { | |
| 'success': True, | |
| 'barcode': barcode.data.decode('utf-8'), | |
| 'type': barcode.type, | |
| 'count': len(barcodes), | |
| 'method': 'opencv' | |
| } | |
| # Améliorer le contraste et réessayer | |
| clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) | |
| enhanced = clahe.apply(img) | |
| barcodes = decode(enhanced) | |
| if barcodes: | |
| barcode = barcodes[0] | |
| return { | |
| 'success': True, | |
| 'barcode': barcode.data.decode('utf-8'), | |
| 'type': barcode.type, | |
| 'count': len(barcodes), | |
| 'method': 'enhanced' | |
| } | |
| except Exception as e: | |
| logger.debug(f"OpenCV method: {e}") | |
| return {'success': False, 'error': 'Aucun code-barres détecté'} | |
| except Exception as e: | |
| logger.error(f"Decode error: {e}") | |
| return {'success': False, 'error': str(e)} | |
| def scan(): | |
| """Endpoint principal - supporte GET pour l'interface web""" | |
| if request.method == 'GET': | |
| return ''' | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Barcode Scanner</title> | |
| <style> | |
| body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } | |
| .container { background: #f5f5f5; padding: 20px; border-radius: 10px; } | |
| input, button { margin: 10px 0; padding: 10px; width: 100%; } | |
| #preview { max-width: 300px; margin: 20px 0; } | |
| #result { margin-top: 20px; padding: 10px; background: white; border-radius: 5px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>📷 Barcode Scanner</h1> | |
| <input type="file" id="imageInput" accept="image/*" capture="environment"> | |
| <video id="preview" style="display:none;"></video> | |
| <button onclick="captureImage()">📸 Prendre une photo</button> | |
| <button onclick="uploadImage()">📁 Uploader une image</button> | |
| <div id="result"></div> | |
| </div> | |
| <script> | |
| let currentImage = null; | |
| // Accéder à la caméra | |
| async function startCamera() { | |
| const video = document.getElementById('preview'); | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ | |
| video: { facingMode: 'environment' } | |
| }); | |
| video.srcObject = stream; | |
| video.style.display = 'block'; | |
| video.play(); | |
| return video; | |
| } catch (err) { | |
| alert('Erreur caméra: ' + err.message); | |
| } | |
| } | |
| // Capturer une image | |
| async function captureImage() { | |
| const video = document.getElementById('preview'); | |
| if (!video.srcObject) { | |
| await startCamera(); | |
| return; | |
| } | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = video.videoWidth; | |
| canvas.height = video.videoHeight; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(video, 0, 0); | |
| currentImage = canvas.toDataURL('image/jpeg'); | |
| scanBarcode(currentImage); | |
| } | |
| // Uploader une image | |
| function uploadImage() { | |
| const input = document.getElementById('imageInput'); | |
| input.click(); | |
| input.onchange = function(e) { | |
| const file = e.target.files[0]; | |
| const reader = new FileReader(); | |
| reader.onload = function(event) { | |
| currentImage = event.target.result; | |
| scanBarcode(currentImage); | |
| }; | |
| reader.readAsDataURL(file); | |
| }; | |
| } | |
| // Scanner le code-barres | |
| async function scanBarcode(imageData) { | |
| const resultDiv = document.getElementById('result'); | |
| resultDiv.innerHTML = '⌛ Analyse en cours...'; | |
| try { | |
| const response = await fetch('/api/scan', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ image: imageData }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| resultDiv.innerHTML = ` | |
| <h3>✅ Code-barres trouvé !</h3> | |
| <p><strong>Code:</strong> ${data.barcode}</p> | |
| <p><strong>Type:</strong> ${data.type}</p> | |
| <button onclick="getProductInfo('${data.barcode}')"> | |
| 🔍 Voir les infos produit | |
| </button> | |
| `; | |
| } else { | |
| resultDiv.innerHTML = ` | |
| <h3>❌ Aucun code-barres détecté</h3> | |
| <p>Erreur: ${data.error}</p> | |
| <p>Essayez avec une image plus claire.</p> | |
| `; | |
| } | |
| } catch (error) { | |
| resultDiv.innerHTML = `❌ Erreur: ${error.message}`; | |
| } | |
| } | |
| // Obtenir les infos produit | |
| async function getProductInfo(barcode) { | |
| const resultDiv = document.getElementById('result'); | |
| resultDiv.innerHTML += '<p>🔍 Recherche des infos produit...</p>'; | |
| try { | |
| const response = await fetch(`/api/product/${barcode}`); | |
| const data = await response.json(); | |
| if (data.success) { | |
| const product = data.product; | |
| resultDiv.innerHTML += ` | |
| <h4>📦 Informations produit:</h4> | |
| <p><strong>Nom:</strong> ${product.name || 'Inconnu'}</p> | |
| <p><strong>Marque:</strong> ${product.brand || 'Inconnue'}</p> | |
| <p><strong>Catégorie:</strong> ${product.category || 'Non catégorisé'}</p> | |
| ${product.price ? `<p><strong>Prix:</strong> ${product.price} €</p>` : ''} | |
| ${product.description ? `<p><strong>Description:</strong> ${product.description}</p>` : ''} | |
| `; | |
| } else { | |
| resultDiv.innerHTML += `<p>ℹ️ ${data.error}</p>`; | |
| } | |
| } catch (error) { | |
| resultDiv.innerHTML += `<p>❌ Erreur recherche: ${error.message}</p>`; | |
| } | |
| } | |
| // Démarrer la caméra au chargement | |
| window.onload = startCamera; | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| # POST request - API endpoint | |
| try: | |
| if not request.is_json: | |
| return jsonify({'success': False, 'error': 'Content-Type must be application/json'}), 400 | |
| data = request.get_json() | |
| if not data or 'image' not in data: | |
| return jsonify({'success': False, 'error': 'No image provided'}), 400 | |
| image_data = data['image'] | |
| if ',' in image_data: | |
| image_data = image_data.split(',')[1] | |
| # Limiter la taille pour HF Spaces | |
| if len(image_data) > 3 * 1024 * 1024: # 3MB max | |
| return jsonify({'success': False, 'error': 'Image too large (max 3MB)'}), 400 | |
| image_bytes = base64.b64decode(image_data) | |
| result = decode_barcode(image_bytes) | |
| return jsonify(result) | |
| except Exception as e: | |
| logger.error(f"Scan error: {e}") | |
| return jsonify({'success': False, 'error': str(e)}), 500 | |
| def product_info(barcode): | |
| """Obtenir les infos produit""" | |
| try: | |
| # Base de données locale | |
| products = { | |
| '5901234123457': { | |
| 'name': 'Lait UHT Demi-écrémé', | |
| 'brand': 'Candia', | |
| 'category': 'Alimentation', | |
| 'price': 1.20, | |
| 'description': 'Lait stérilisé UHT' | |
| }, | |
| '9780201379624': { | |
| 'name': 'Flutter Cookbook', | |
| 'brand': "O'Reilly", | |
| 'category': 'Livres', | |
| 'price': 45.99, | |
| 'description': 'Guide de développement Flutter' | |
| }, | |
| '3017620422003': { | |
| 'name': 'Nutella', | |
| 'brand': 'Ferrero', | |
| 'category': 'Alimentation', | |
| 'price': 4.99, | |
| 'description': 'Pâte à tartiner aux noisettes' | |
| } | |
| } | |
| if barcode in products: | |
| return jsonify({ | |
| 'success': True, | |
| 'product': products[barcode], | |
| 'source': 'local' | |
| }) | |
| # Essayer OpenFoodFacts | |
| try: | |
| response = requests.get( | |
| f'https://world.openfoodfacts.org/api/v0/product/{barcode}.json', | |
| timeout=3 | |
| ) | |
| if response.status_code == 200: | |
| data = response.json() | |
| if data.get('status') == 1: | |
| product = data.get('product', {}) | |
| return jsonify({ | |
| 'success': True, | |
| 'product': { | |
| 'name': product.get('product_name', 'Produit inconnu'), | |
| 'brand': product.get('brands', 'Marque inconnue'), | |
| 'category': product.get('categories', 'Non catégorisé'), | |
| 'description': product.get('generic_name', ''), | |
| 'image_url': product.get('image_url', '') | |
| }, | |
| 'source': 'openfoodfacts' | |
| }) | |
| except Exception as e: | |
| logger.debug(f"OpenFoodFacts error: {e}") | |
| return jsonify({ | |
| 'success': False, | |
| 'error': 'Produit non trouvé', | |
| 'barcode': barcode | |
| }) | |
| except Exception as e: | |
| logger.error(f"Product info error: {e}") | |
| return jsonify({'success': False, 'error': str(e)}), 500 | |
| def health(): | |
| """Health check endpoint""" | |
| return jsonify({ | |
| 'status': 'healthy', | |
| 'service': 'Barcode Scanner API', | |
| 'version': '2.0', | |
| 'deployed_on': 'Hugging Face Spaces' if HF_SPACE else 'Local' | |
| }) | |
| def home(): | |
| """Page d'accueil""" | |
| return jsonify({ | |
| 'message': 'Barcode Scanner API', | |
| 'documentation': { | |
| 'scan': 'POST /api/scan - Scanner un code-barres', | |
| 'product': 'GET /api/product/<barcode> - Infos produit', | |
| 'health': 'GET /api/health - Health check' | |
| }, | |
| 'web_interface': 'Visitez /api/scan pour l\'interface web' | |
| }) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=PORT, debug=False) |