barkscan / app.py
playcat's picture
Update: Korean localization (ํ•œ๊ธ€ ๋ฒ„์ „)
e2b266f verified
#!/usr/bin/env python3
"""
BarkScan - ๋ฐ˜๋ ค๋™๋ฌผ ์‚ฌ๋ฃŒ ์•ˆ์ „ ๋ถ„์„๊ธฐ
HuggingFace Spaces Gradio App (2025) - ํ•œ๊ธ€ ๋ฒ„์ „
"""
import gradio as gr
import cv2
import numpy as np
from pyzbar.pyzbar import decode
import json
from typing import Dict, List, Optional, Tuple
import sqlite3
import os
# ์ƒ˜ํ”Œ ์ œํ’ˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (์‹œ์—ฐ์šฉ ์ธ๋ฉ”๋ชจ๋ฆฌ)
SAMPLE_PRODUCTS = {
"8801234567890": {
"name": "๋กœ์–„์บ๋‹Œ ๋ฏธ๋‹ˆ ์–ด๋œํŠธ",
"brand": "๋กœ์–„์บ๋‹Œ",
"category": "๊ฐ•์•„์ง€ ์‚ฌ๋ฃŒ",
"ingredients": "์Œ€, ํƒˆ์ˆ˜๊ฐ€๊ธˆ์œก๋‹จ๋ฐฑ์งˆ, ๋™๋ฌผ์„ฑ์ง€๋ฐฉ, ์˜ฅ์ˆ˜์ˆ˜, ์‚ฌํƒ•๋ฌดํŽ„ํ”„, ๊ฐ€์ˆ˜๋ถ„ํ•ด ๋™๋ฌผ์„ฑ ๋‹จ๋ฐฑ์งˆ",
"protein": 27.0,
"fat": 16.0,
"fiber": 1.5,
"safety_score": 85,
"grade": "A",
"harmful_substances": []
},
"8801234567898": {
"name": "์ €๊ฐ€ ๊ฐ•์•„์ง€ ์‚ฌ๋ฃŒ",
"brand": "์ผ๋ฐ˜ ๋ธŒ๋žœ๋“œ",
"category": "๊ฐ•์•„์ง€ ์‚ฌ๋ฃŒ",
"ingredients": "์˜ฅ์ˆ˜์ˆ˜๊ฐ€๋ฃจ, ์œก๋ฅ˜๋ถ€์‚ฐ๋ฌผ, BHA(๋ณด์กด์ œ), ์—ํ†ก์‹œํ€ธ, ์ธ๊ณต์ƒ‰์†Œ",
"protein": 18.0,
"fat": 12.0,
"fiber": 4.0,
"safety_score": 45,
"grade": "D",
"harmful_substances": [
{"name": "BHA", "risk_level": "high", "description": "๋ฐœ์•” ๊ฐ€๋Šฅ ๋ฌผ์งˆ"},
{"name": "์—ํ†ก์‹œํ€ธ", "risk_level": "high", "description": "์‚ด์ถฉ์ œ ์„ฑ๋ถ„ - ์‚ฌ๋žŒ ์Œ์‹์—๋Š” ๊ธˆ์ง€"}
]
},
"8801234567899": {
"name": "์˜ค๋ฆฌ์   ์˜ค๋ฆฌ์ง€๋„ ๋…",
"brand": "์˜ค๋ฆฌ์  ",
"category": "๊ฐ•์•„์ง€ ์‚ฌ๋ฃŒ",
"ingredients": "์‹ ์„ ํ•œ ๋‹ญ๊ณ ๊ธฐ, ์‹ ์„ ํ•œ ์น ๋ฉด์กฐ ๊ณ ๊ธฐ, ์‹ ์„ ํ•œ ๊ณ„๋ž€, ์‹ ์„ ํ•œ ๋‹ญ ๊ฐ„",
"protein": 38.0,
"fat": 18.0,
"fiber": 4.0,
"safety_score": 95,
"grade": "A+",
"harmful_substances": []
}
}
# ์œ ํ•ด ์„ฑ๋ถ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
HARMFUL_SUBSTANCES = {
"bha": {"name": "BHA", "risk": "high", "description": "๋ถ€ํ‹ธํ™” ํžˆ๋“œ๋ก์‹œ์•„๋‹ˆ์†” - ๋ฐœ์•” ๊ฐ€๋Šฅ ๋ฌผ์งˆ"},
"bht": {"name": "BHT", "risk": "high", "description": "๋ถ€ํ‹ธํ™” ํžˆ๋“œ๋ก์‹œํ†จ๋ฃจ์—” - ๊ฐ„ ์†์ƒ ์œ„ํ—˜"},
"ethoxyquin": {"name": "์—ํ†ก์‹œํ€ธ", "risk": "high", "description": "์‚ด์ถฉ์ œ ์„ฑ๋ถ„ ๋ณด์กด๋ฃŒ - ์‚ฌ๋žŒ ์Œ์‹์—๋Š” ๊ธˆ์ง€"},
"์—ํ†ก์‹œํ€ธ": {"name": "์—ํ†ก์‹œํ€ธ", "risk": "high", "description": "์‚ด์ถฉ์ œ ์„ฑ๋ถ„ ๋ณด์กด๋ฃŒ - ์‚ฌ๋žŒ ์Œ์‹์—๋Š” ๊ธˆ์ง€"},
"propylene glycol": {"name": "ํ”„๋กœํ•„๋ Œ ๊ธ€๋ฆฌ์ฝœ", "risk": "medium", "description": "๊ณ ์–‘์ด์—๊ฒŒ ๋นˆํ˜ˆ ์œ ๋ฐœ ๊ฐ€๋Šฅ"},
"artificial color": {"name": "์ธ๊ณต์ƒ‰์†Œ", "risk": "medium", "description": "์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘ ์œ ๋ฐœ ๊ฐ€๋Šฅ"},
"์ธ๊ณต์ƒ‰์†Œ": {"name": "์ธ๊ณต์ƒ‰์†Œ", "risk": "medium", "description": "์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘ ์œ ๋ฐœ ๊ฐ€๋Šฅ"},
"corn syrup": {"name": "์˜ฅ์ˆ˜์ˆ˜ ์‹œ๋Ÿฝ", "risk": "low", "description": "๊ณ ๋‹น๋ถ„ - ๋น„๋งŒ ์œ„ํ—˜"},
"by-product": {"name": "์œก๋ฅ˜๋ถ€์‚ฐ๋ฌผ", "risk": "medium", "description": "์ €ํ’ˆ์งˆ ๋‹จ๋ฐฑ์งˆ"},
"๋ถ€์‚ฐ๋ฌผ": {"name": "์œก๋ฅ˜๋ถ€์‚ฐ๋ฌผ", "risk": "medium", "description": "์ €ํ’ˆ์งˆ ๋‹จ๋ฐฑ์งˆ"},
"carrageenan": {"name": "์นด๋ผ๊ธฐ๋‚œ", "risk": "medium", "description": "์†Œํ™”๊ธฐ ์—ผ์ฆ ์œ ๋ฐœ ๊ฐ€๋Šฅ"},
"cellulose": {"name": "์…€๋ฃฐ๋กœ์˜ค์Šค", "risk": "low", "description": "์˜์–‘๊ฐ€ ์—†๋Š” ์ถฉ์ „์žฌ"},
"rendered fat": {"name": "๋ Œ๋”๋ง ์ง€๋ฐฉ", "risk": "low", "description": "์ €ํ’ˆ์งˆ ์ง€๋ฐฉ"}
}
def detect_barcode_from_image(image: np.ndarray) -> Optional[str]:
"""
์ด๋ฏธ์ง€์—์„œ ๋ฐ”์ฝ”๋“œ ๊ฐ์ง€ (ZBar ์‚ฌ์šฉ)
"""
if image is None:
return None
# ๊ทธ๋ ˆ์ด์Šค์ผ€์ผ ๋ณ€ํ™˜์œผ๋กœ ์ธ์‹๋ฅ  ํ–ฅ์ƒ
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# ๋ฐ”์ฝ”๋“œ ๊ฐ์ง€ ๋ฐ ๋””์ฝ”๋”ฉ
barcodes = decode(gray)
if barcodes:
# ์ฒซ ๋ฒˆ์งธ ๋ฐ”์ฝ”๋“œ ๋ฐ˜ํ™˜
barcode_data = barcodes[0].data.decode('utf-8')
return barcode_data
return None
def analyze_ingredients(ingredients: str) -> Tuple[List[Dict], int, str]:
"""
์„ฑ๋ถ„ ๋ถ„์„ํ•˜์—ฌ ์œ ํ•ด ๋ฌผ์งˆ ๊ฐ์ง€
"""
ingredients_lower = ingredients.lower()
detected_harmful = []
for key, info in HARMFUL_SUBSTANCES.items():
if key in ingredients_lower:
detected_harmful.append({
"name": info["name"],
"risk_level": info["risk"],
"description": info["description"]
})
# ์•ˆ์ „ ์ ์ˆ˜ ๊ณ„์‚ฐ (0-100)
base_score = 100
for substance in detected_harmful:
if substance["risk_level"] == "high":
base_score -= 20
elif substance["risk_level"] == "medium":
base_score -= 10
elif substance["risk_level"] == "low":
base_score -= 5
safety_score = max(0, base_score)
# ๋“ฑ๊ธ‰ ๊ณ„์‚ฐ
if safety_score >= 90:
grade = "A+"
elif safety_score >= 80:
grade = "A"
elif safety_score >= 70:
grade = "B"
elif safety_score >= 60:
grade = "C"
elif safety_score >= 50:
grade = "D"
else:
grade = "F"
return detected_harmful, safety_score, grade
def get_product_info(barcode: str) -> Optional[Dict]:
"""
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ œํ’ˆ ์ •๋ณด ์กฐํšŒ
"""
return SAMPLE_PRODUCTS.get(barcode)
def format_product_result(product: Dict) -> str:
"""
์ œํ’ˆ ์ •๋ณด๋ฅผ HTML๋กœ ํฌ๋งทํŒ…
"""
# ๋“ฑ๊ธ‰๋ณ„ ์ƒ‰์ƒ
grade_colors = {
"A+": "#00b300",
"A": "#33cc33",
"B": "#66cc00",
"C": "#ffcc00",
"D": "#ff9900",
"F": "#ff3300"
}
grade = product.get("grade", "N/A")
grade_color = grade_colors.get(grade, "#999999")
html = f"""
<div style="font-family: 'Malgun Gothic', sans-serif; padding: 20px; background-color: #f9f9f9; border-radius: 10px;">
<h2 style="color: #333; margin-bottom: 10px;">{product['name']}</h2>
<p style="color: #666; font-size: 16px; margin-bottom: 20px;"><strong>์ œ์กฐ์‚ฌ:</strong> {product['brand']}</p>
<div style="background-color: {grade_color}; color: white; padding: 15px; border-radius: 8px; text-align: center; margin-bottom: 20px;">
<h1 style="margin: 0; font-size: 48px;">{grade} ๋“ฑ๊ธ‰</h1>
<p style="margin: 5px 0 0 0; font-size: 18px;">์•ˆ์ „ ์ ์ˆ˜: {product['safety_score']}/100์ </p>
</div>
<div style="background-color: white; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
<h3 style="color: #333; margin-top: 0;">๐Ÿ“Š ์˜์–‘ ์„ฑ๋ถ„</h3>
<p style="color: #555; margin: 5px 0;"><strong>๋‹จ๋ฐฑ์งˆ:</strong> {product.get('protein', 'N/A')}%</p>
<p style="color: #555; margin: 5px 0;"><strong>์ง€๋ฐฉ:</strong> {product.get('fat', 'N/A')}%</p>
<p style="color: #555; margin: 5px 0;"><strong>์„ฌ์œ ์งˆ:</strong> {product.get('fiber', 'N/A')}%</p>
</div>
<div style="background-color: white; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
<h3 style="color: #333; margin-top: 0;">๐ŸŒพ ์›์žฌ๋ฃŒ</h3>
<p style="color: #555; line-height: 1.6;">{product.get('ingredients', 'N/A')}</p>
</div>
"""
# ์œ ํ•ด ์„ฑ๋ถ„ ์„น์…˜
if product.get("harmful_substances"):
html += """
<div style="background-color: #fff3cd; border-left: 4px solid #ff9900; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
<h3 style="color: #856404; margin-top: 0;">โš ๏ธ ๊ฒ€์ถœ๋œ ์œ ํ•ด ์„ฑ๋ถ„</h3>
"""
for substance in product["harmful_substances"]:
risk_color = {
"high": "#ff3300",
"medium": "#ff9900",
"low": "#ffcc00"
}.get(substance.get("risk_level", "low"), "#999999")
html += f"""
<div style="margin-bottom: 10px; padding: 10px; background-color: white; border-radius: 5px;">
<p style="margin: 0; color: {risk_color}; font-weight: bold;">{substance['name']}</p>
<p style="margin: 5px 0 0 0; color: #666; font-size: 14px;">{substance.get('description', '')}</p>
</div>
"""
html += "</div>"
else:
html += """
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; border-radius: 8px;">
<h3 style="color: #155724; margin-top: 0;">โœ“ ์œ ํ•ด ์„ฑ๋ถ„ ๋ฏธ๊ฒ€์ถœ</h3>
<p style="color: #155724; margin: 0;">๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ธฐ์ค€ ์•ˆ์ „ํ•œ ์ œํ’ˆ์ž…๋‹ˆ๋‹ค.</p>
</div>
"""
html += "</div>"
return html
def scan_and_analyze(image) -> Tuple[str, str]:
"""
๋ฉ”์ธ ํ•จ์ˆ˜: ๋ฐ”์ฝ”๋“œ ์Šค์บ” ํ›„ ์ œํ’ˆ ๋ถ„์„
"""
if image is None:
return "์ด๋ฏธ์ง€๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค", "์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ์นด๋ฉ”๋ผ๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”"
# ๋ฐ”์ฝ”๋“œ ๊ฐ์ง€
barcode = detect_barcode_from_image(image)
if not barcode:
return "โŒ ๋ฐ”์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", """
<div style="padding: 20px; background-color: #f8d7da; border-radius: 10px; color: #721c24; font-family: 'Malgun Gothic', sans-serif;">
<h3>๋ฐ”์ฝ”๋“œ ์ธ์‹ ์‹คํŒจ</h3>
<p>๋‹ค์Œ์„ ์‹œ๋„ํ•ด๋ณด์„ธ์š”:</p>
<ul>
<li>๋ฐ”์ฝ”๋“œ๊ฐ€ ์„ ๋ช…ํ•˜๊ฒŒ ๋ณด์ด๋Š”์ง€ ํ™•์ธ</li>
<li>์กฐ๋ช…์ด ์ถฉ๋ถ„ํ•œ์ง€ ํ™•์ธ</li>
<li>์นด๋ฉ”๋ผ๋ฅผ ํ”๋“ค๋ฆฌ์ง€ ์•Š๊ฒŒ ๊ณ ์ •</li>
<li>๋ฐ”์ฝ”๋“œ์— ๋” ๊ฐ€๊นŒ์ด ์ ‘๊ทผ</li>
</ul>
</div>
"""
barcode_result = f"โœ“ ๋ฐ”์ฝ”๋“œ ์ธ์‹: {barcode}"
# ์ œํ’ˆ ์ •๋ณด ์กฐํšŒ
product = get_product_info(barcode)
if not product:
return barcode_result, f"""
<div style="padding: 20px; background-color: #fff3cd; border-radius: 10px; color: #856404; font-family: 'Malgun Gothic', sans-serif;">
<h3>์ œํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค</h3>
<p>๋ฐ”์ฝ”๋“œ <strong>{barcode}</strong>๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—†์Šต๋‹ˆ๋‹ค.</p>
<p>ํ…Œ์ŠคํŠธ์šฉ ์ƒ˜ํ”Œ ๋ฐ”์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด์„ธ์š”:</p>
<ul>
<li><strong>8801234567890</strong> - ๋กœ์–„์บ๋‹Œ (A๋“ฑ๊ธ‰)</li>
<li><strong>8801234567898</strong> - ์ €๊ฐ€ ์‚ฌ๋ฃŒ (D๋“ฑ๊ธ‰ - ์œ ํ•ด ์„ฑ๋ถ„ ํฌํ•จ)</li>
<li><strong>8801234567899</strong> - ์˜ค๋ฆฌ์   (A+๋“ฑ๊ธ‰)</li>
</ul>
</div>
"""
# ๊ฒฐ๊ณผ ํฌ๋งทํŒ… ๋ฐ ๋ฐ˜ํ™˜
product_html = format_product_result(product)
return barcode_result, product_html
# Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
def create_interface():
"""Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ ๋ฐ ์„ค์ •"""
with gr.Blocks(title="BarkScan - ๋ฐ˜๋ ค๋™๋ฌผ ์‚ฌ๋ฃŒ ์•ˆ์ „ ๋ถ„์„๊ธฐ", theme=gr.themes.Soft()) as app:
gr.Markdown("""
# ๐Ÿพ BarkScan - ๋ฐ˜๋ ค๋™๋ฌผ ์‚ฌ๋ฃŒ ์•ˆ์ „ ๋ถ„์„๊ธฐ
**๋ฐ”์ฝ”๋“œ๋ฅผ ์Šค์บ”ํ•˜์—ฌ ์‚ฌ๋ฃŒ ์„ฑ๋ถ„์˜ ์•ˆ์ „์„ฑ์„ ํ™•์ธํ•˜์„ธ์š”!**
๋ฐ”์ฝ”๋“œ ์‚ฌ์ง„์„ ์—…๋กœ๋“œํ•˜๊ฑฐ๋‚˜ ์นด๋ฉ”๋ผ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š” (๋ชจ๋ฐ”์ผ์—์„œ๋Š” ํ›„๋ฉด ์นด๋ฉ”๋ผ๊ฐ€ ์ž๋™์œผ๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค).
""")
with gr.Row():
with gr.Column():
image_input = gr.Image(
sources=["upload", "webcam"],
type="numpy",
label="๋ฐ”์ฝ”๋“œ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋˜๋Š” ์นด๋ฉ”๋ผ ์ดฌ์˜"
)
scan_button = gr.Button("๐Ÿ” ์Šค์บ” ๋ฐ ๋ถ„์„", variant="primary", size="lg")
gr.Markdown("""
### ํ…Œ์ŠคํŠธ์šฉ ์ƒ˜ํ”Œ ๋ฐ”์ฝ”๋“œ:
- **8801234567890** - ๋กœ์–„์บ๋‹Œ ๋ฏธ๋‹ˆ ์–ด๋œํŠธ (A๋“ฑ๊ธ‰)
- **8801234567898** - ์ €๊ฐ€ ๊ฐ•์•„์ง€ ์‚ฌ๋ฃŒ (D๋“ฑ๊ธ‰ - โš ๏ธ BHA ํฌํ•จ)
- **8801234567899** - ์˜ค๋ฆฌ์   ์˜ค๋ฆฌ์ง€๋„ (A+๋“ฑ๊ธ‰)
*ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ด ๋ฐ”์ฝ”๋“œ๋ฅผ ํ…์ŠคํŠธ๋กœ ์ด๋ฏธ์ง€์— ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค*
""")
with gr.Column():
barcode_output = gr.Textbox(label="์ธ์‹๋œ ๋ฐ”์ฝ”๋“œ", lines=1)
analysis_output = gr.HTML(label="์ œํ’ˆ ๋ถ„์„ ๊ฒฐ๊ณผ")
scan_button.click(
fn=scan_and_analyze,
inputs=[image_input],
outputs=[barcode_output, analysis_output]
)
gr.Markdown("""
---
### BarkScan ์†Œ๊ฐœ
BarkScan์€ ๋ฐ˜๋ ค๋™๋ฌผ ์‚ฌ๋ฃŒ ์„ฑ๋ถ„์„ ๋ถ„์„ํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์œ ํ•ด ๋ฌผ์งˆ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค:
- **BHA/BHT** (๋ณด์กด์ œ - ๋ฐœ์•” ๊ฐ€๋Šฅ ๋ฌผ์งˆ)
- **์—ํ†ก์‹œํ€ธ** (์‚ด์ถฉ์ œ - ์‚ฌ๋žŒ ์Œ์‹์—๋Š” ๊ธˆ์ง€)
- **์ธ๊ณต์ƒ‰์†Œ** (์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘ ์œ ๋ฐœ ๊ฐ€๋Šฅ)
- **ํ”„๋กœํ•„๋ Œ ๊ธ€๋ฆฌ์ฝœ** (๊ณ ์–‘์ด์—๊ฒŒ ๋…์„ฑ)
- **์ €ํ’ˆ์งˆ ์„ฑ๋ถ„** (๋ถ€์‚ฐ๋ฌผ, ์ถฉ์ „์žฌ)
**์•ˆ์ „ ๋“ฑ๊ธ‰ ์‹œ์Šคํ…œ:**
- **A+** (90-100์ ): ์ตœ์šฐ์ˆ˜ - ํ”„๋ฆฌ๋ฏธ์—„ ํ’ˆ์งˆ
- **A** (80-89์ ): ์šฐ์ˆ˜ - ๋†’์€ ํ’ˆ์งˆ, ์•ฝ๊ฐ„์˜ ์šฐ๋ ค
- **B** (70-79์ ): ์–‘ํ˜ธ - ์ˆ˜์šฉ ๊ฐ€๋Šฅํ•œ ํ’ˆ์งˆ
- **C** (60-69์ ): ๋ณดํ†ต - ์ผ๋ถ€ ์šฐ๋ ค์‚ฌํ•ญ
- **D** (50-59์ ): ๋ฏธํก - ๋‹ค์ˆ˜์˜ ์œ ํ•ด ์„ฑ๋ถ„
- **F** (0-49์ ): ๋งค์šฐ ๋ฏธํก - ํ”ผํ•ด์•ผ ํ•จ
---
**๋ฐ์ดํ„ฐ ์ถœ์ฒ˜:** Open Pet Food Facts, ์‹ํ’ˆ์•ˆ์ „๋‚˜๋ผ
**๋ฒ„์ „:** 1.0 (2025) | **๋ฐ˜๋ ค๋™๋ฌผ ์•ˆ์ „์„ ์œ„ํ•ด โค๏ธ**
""")
return app
# ์•ฑ ์‹คํ–‰
if __name__ == "__main__":
app = create_interface()
app.launch()