|
|
|
|
|
""" |
|
|
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"] |
|
|
}) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|