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, resources={r"/api/*": {"origins": "*"}}) | |
| HF_SPACE = os.environ.get("SPACE_ID") is not None | |
| PORT = 7860 if HF_SPACE else int(os.environ.get("PORT", 5000)) | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("barcode-api") | |
| # ================== UTILS ================== | |
| def safe_b64decode(data: str) -> bytes: | |
| """Base64 decode safe (Flutter/Web compatible)""" | |
| return base64.b64decode(data + "=" * (-len(data) % 4)) | |
| def decode_barcode(image_bytes: bytes) -> dict: | |
| """Decode barcode using PIL then OpenCV""" | |
| try: | |
| # --- PIL first --- | |
| try: | |
| pil_image = Image.open(io.BytesIO(image_bytes)) | |
| barcodes = decode(pil_image) | |
| if barcodes: | |
| b = barcodes[0] | |
| return { | |
| "success": True, | |
| "barcode": b.data.decode("utf-8"), | |
| "type": b.type, | |
| "method": "pil" | |
| } | |
| except Exception as e: | |
| logger.debug(f"PIL failed: {e}") | |
| # --- OpenCV fallback --- | |
| 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: | |
| b = barcodes[0] | |
| return { | |
| "success": True, | |
| "barcode": b.data.decode("utf-8"), | |
| "type": b.type, | |
| "method": "opencv" | |
| } | |
| # Contrast enhancement | |
| clahe = cv2.createCLAHE(2.0, (8, 8)) | |
| enhanced = clahe.apply(img) | |
| barcodes = decode(enhanced) | |
| if barcodes: | |
| b = barcodes[0] | |
| return { | |
| "success": True, | |
| "barcode": b.data.decode("utf-8"), | |
| "type": b.type, | |
| "method": "enhanced" | |
| } | |
| except Exception as e: | |
| logger.debug(f"OpenCV failed: {e}") | |
| return {"success": False, "error": "No barcode detected"} | |
| except Exception as e: | |
| logger.error(e) | |
| return {"success": False, "error": str(e)} | |
| # ================== FLUTTER API ================== | |
| def api_decode_barcode(): | |
| if not request.is_json: | |
| return jsonify({"success": False, "error": "JSON required"}), 400 | |
| data = request.get_json() | |
| image_data = data.get("image") | |
| if not image_data: | |
| return jsonify({"success": False, "error": "Image missing"}), 400 | |
| if "," in image_data: | |
| image_data = image_data.split(",")[1] | |
| if len(image_data) > 3 * 1024 * 1024: | |
| return jsonify({"success": False, "error": "Image too large"}), 400 | |
| image_bytes = safe_b64decode(image_data) | |
| result = decode_barcode(image_bytes) | |
| return jsonify(result) | |
| def api_product_info(barcode): | |
| local_db = { | |
| "3017620422003": { | |
| "name": "Nutella", | |
| "brand": "Ferrero", | |
| "category": "Food", | |
| "price": 4.99, | |
| "description": "Hazelnut spread" | |
| }, | |
| "5901234123457": { | |
| "name": "Milk UHT", | |
| "brand": "Candia", | |
| "category": "Food", | |
| "price": 1.20, | |
| "description": "UHT milk" | |
| } | |
| } | |
| if barcode in local_db: | |
| return jsonify({"success": True, "product": local_db[barcode], "source": "local"}) | |
| # OpenFoodFacts fallback | |
| try: | |
| r = requests.get( | |
| f"https://world.openfoodfacts.org/api/v0/product/{barcode}.json", | |
| timeout=4 | |
| ) | |
| if r.status_code == 200: | |
| data = r.json() | |
| if data.get("status") == 1: | |
| p = data["product"] | |
| return jsonify({ | |
| "success": True, | |
| "source": "openfoodfacts", | |
| "product": { | |
| "name": p.get("product_name"), | |
| "brand": p.get("brands"), | |
| "category": p.get("categories"), | |
| "description": p.get("generic_name"), | |
| "image": p.get("image_url") | |
| } | |
| }) | |
| except Exception as e: | |
| logger.debug(e) | |
| return jsonify({"success": False, "error": "Product not found"}), 404 | |
| # ================== WEB SCAN ================== | |
| def api_scan(): | |
| if not request.is_json: | |
| return jsonify({"success": False, "error": "JSON required"}), 400 | |
| image_data = request.json.get("image") | |
| if not image_data: | |
| return jsonify({"success": False, "error": "Image missing"}), 400 | |
| if "," in image_data: | |
| image_data = image_data.split(",")[1] | |
| image_bytes = safe_b64decode(image_data) | |
| return jsonify(decode_barcode(image_bytes)) | |
| # ================== HEALTH ================== | |
| def health(): | |
| return jsonify({ | |
| "status": "ok", | |
| "service": "Barcode Scanner API", | |
| "platform": "HuggingFace" if HF_SPACE else "Local", | |
| "endpoints": [ | |
| "/api/decode-barcode", | |
| "/api/product-info/<barcode>", | |
| "/api/scan", | |
| "/api/health" | |
| ] | |
| }) | |
| def home(): | |
| return jsonify({ | |
| "message": "Barcode Scanner API", | |
| "use": "/api/decode-barcode (POST)" | |
| }) | |
| # ================== MAIN ================== | |
| if __name__ == "__main__": | |
| app.run(host="0.0.0.0", port=PORT, debug=False) | |