|
|
import json |
|
|
import re |
|
|
from typing import List, Optional, Tuple, Union |
|
|
import numpy as np |
|
|
import os |
|
|
|
|
|
import gradio as gr |
|
|
import spaces |
|
|
import torch |
|
|
from PIL import Image |
|
|
from transformers import AutoTokenizer, AutoModelForCausalLM |
|
|
from huggingface_hub import login, snapshot_download |
|
|
from paddleocr import PaddleOCR |
|
|
|
|
|
|
|
|
HF_TOKEN = os.getenv("HF_TOKEN") |
|
|
if HF_TOKEN: |
|
|
login(token=HF_TOKEN.strip()) |
|
|
|
|
|
|
|
|
MED_MODEL_ID = "google/gemma-2-2b-it" |
|
|
|
|
|
|
|
|
OCR_READER = None |
|
|
MED_MODEL = None |
|
|
MED_TOKENIZER = None |
|
|
OCR_MODEL_REPO_ID = "PaddlePaddle/korean_PP-OCRv5_mobile_rec" |
|
|
|
|
|
|
|
|
def _collect_ocr_texts(ocr_payload) -> List[str]: |
|
|
"""PaddleOCR ๊ฒฐ๊ณผ ๊ตฌ์กฐ์์ ํ
์คํธ๋ง ์ถ์ถ""" |
|
|
texts: List[str] = [] |
|
|
seen = set() |
|
|
|
|
|
def add_text(candidate: str): |
|
|
if not isinstance(candidate, str): |
|
|
return |
|
|
normalized = candidate.strip() |
|
|
if normalized and normalized not in seen: |
|
|
seen.add(normalized) |
|
|
texts.append(normalized) |
|
|
|
|
|
def walk(node): |
|
|
if isinstance(node, str): |
|
|
add_text(node) |
|
|
return |
|
|
|
|
|
if isinstance(node, dict): |
|
|
for key in ("text", "label", "transcription"): |
|
|
add_text(node.get(key)) |
|
|
|
|
|
for key in ("texts", "labels"): |
|
|
values = node.get(key) |
|
|
if isinstance(values, (list, tuple)): |
|
|
for value in values: |
|
|
add_text(value) |
|
|
|
|
|
for key in ("text_recognition", "rec_results", "data", "results"): |
|
|
if key in node: |
|
|
walk(node[key]) |
|
|
return |
|
|
|
|
|
if isinstance(node, (list, tuple)): |
|
|
if len(node) >= 2: |
|
|
second = node[1] |
|
|
if isinstance(second, str): |
|
|
add_text(second) |
|
|
elif isinstance(second, (list, tuple)) and second: |
|
|
maybe_text = second[0] |
|
|
add_text(maybe_text) |
|
|
|
|
|
for item in node: |
|
|
walk(item) |
|
|
|
|
|
walk(ocr_payload) |
|
|
return texts |
|
|
|
|
|
def load_models(): |
|
|
"""๋ชจ๋ธ๋ค์ ํ ๋ฒ๋ง ๋ก๋""" |
|
|
global OCR_READER, MED_MODEL, MED_TOKENIZER |
|
|
|
|
|
if OCR_READER is None: |
|
|
print("๐ Loading PaddleOCR (Korean PP-OCRv5 mobile recognition)...") |
|
|
rec_model_dir = snapshot_download( |
|
|
OCR_MODEL_REPO_ID, |
|
|
allow_patterns=[ |
|
|
"*.pdmodel", |
|
|
"*.pdiparams", |
|
|
"*.pdparams", |
|
|
"*.json", |
|
|
"*.yml", |
|
|
], |
|
|
) |
|
|
OCR_READER = PaddleOCR( |
|
|
lang='korean', |
|
|
use_textline_orientation=True, |
|
|
text_recognition_model_dir=rec_model_dir, |
|
|
text_recognition_model_name="korean_PP-OCRv5_mobile_rec", |
|
|
) |
|
|
print("โ
PaddleOCR loaded!") |
|
|
|
|
|
if MED_MODEL is None: |
|
|
print("๐ Loading Gemma-2-2B for medical analysis (8bit quantization)...") |
|
|
MED_MODEL = AutoModelForCausalLM.from_pretrained( |
|
|
MED_MODEL_ID, |
|
|
torch_dtype=torch.bfloat16, |
|
|
device_map="auto", |
|
|
load_in_8bit=True |
|
|
) |
|
|
MED_TOKENIZER = AutoTokenizer.from_pretrained(MED_MODEL_ID) |
|
|
print("โ
Medical model loaded!") |
|
|
|
|
|
|
|
|
load_models() |
|
|
|
|
|
|
|
|
def _extract_assistant_content(decoded: str) -> str: |
|
|
"""์ด์์คํดํธ ์๋ต ์ถ์ถ""" |
|
|
if "<|im_start|>assistant" in decoded: |
|
|
content = decoded.split("<|im_start|>assistant")[-1] |
|
|
content = content.replace("<|im_end|>", "").strip() |
|
|
return content |
|
|
return decoded.strip() |
|
|
|
|
|
|
|
|
def _extract_json_block(text: str) -> Optional[str]: |
|
|
"""JSON ๋ธ๋ก ์ถ์ถ""" |
|
|
match = re.search(r"\{.*\}", text, re.DOTALL) |
|
|
if not match: |
|
|
return None |
|
|
return match.group(0) |
|
|
|
|
|
|
|
|
@spaces.GPU(duration=120) |
|
|
def analyze_medication_image(image: Image.Image) -> Tuple[str, str]: |
|
|
"""์ด๋ฏธ์ง์์ OCR ์ถ์ถ ํ ์ฝ ์ ๋ณด ๋ถ์""" |
|
|
import time |
|
|
try: |
|
|
|
|
|
start_time = time.time() |
|
|
img_array = np.array(image) |
|
|
|
|
|
try: |
|
|
ocr_results = OCR_READER.predict(img_array) |
|
|
except (TypeError, AttributeError): |
|
|
ocr_results = OCR_READER.ocr(img_array) |
|
|
ocr_time = time.time() - start_time |
|
|
print(f"โฑ๏ธ OCR took {ocr_time:.2f}s") |
|
|
|
|
|
if not ocr_results: |
|
|
return "ํ
์คํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.", "" |
|
|
|
|
|
|
|
|
texts = _collect_ocr_texts(ocr_results) |
|
|
|
|
|
if not texts: |
|
|
return "ํ
์คํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.", "" |
|
|
|
|
|
ocr_text = "\n".join(texts) |
|
|
|
|
|
|
|
|
analysis_start = time.time() |
|
|
|
|
|
analysis_prompt = f"""๋ค์์ ์ฝ ๋ดํฌ๋ ์ฒ๋ฐฉ์ ์์ ์ถ์ถํ ํ
์คํธ์
๋๋ค: |
|
|
|
|
|
{ocr_text} |
|
|
|
|
|
์ ํ
์คํธ์์ ์ฝ ์ด๋ฆ์ ์ฐพ์์, ๊ฐ ์ฝ์ ๋ํด **๋
ธ์ธ๊ณผ ์ด๋ฆฐ์ด ๋ชจ๋ ์ฝ๊ฒ ์ดํดํ ์ ์๋๋ก** ์ฌ๋ฏธ์๊ณ ์น๊ทผํ๊ฒ ์ค๋ช
ํด์ฃผ์ธ์: |
|
|
|
|
|
๐ **๊ฐ ์ฝ๋ง๋ค ๋ค์ ์ ๋ณด๋ฅผ ํฌํจํด์ฃผ์ธ์:** |
|
|
|
|
|
1. ๐ **์ฝ ์ด๋ฆ**: ์ ํํ ์ฝ ์ด๋ฆ |
|
|
2. ๐ฏ **ํจ๋ฅ**: ์ด ์ฝ์ด ๋ฌด์์ ์น๋ฃํ๊ณ ์ด๋ป๊ฒ ๋์์ด ๋๋์ง |
|
|
3. โ ๏ธ **๋ถ์์ฉ**: ์ฃผ์ํด์ผ ํ ๋ถ์์ฉ๋ค |
|
|
4. ๐ก **๋ณต์ฉ ๋ฐฉ๋ฒ**: ์ธ์ , ์ด๋ป๊ฒ ๋จน์ด์ผ ํ๋์ง (์์ /์ํ, ํ๋ฃจ ๋ช ๋ฒ ๋ฑ) |
|
|
5. ๐ซ **์ฃผ์์ฌํญ**: ์ด ์ฝ๊ณผ ํจ๊ป ๋จน์ผ๋ฉด ์ ๋๋ ๊ฒ๋ค (์์, ๋ค๋ฅธ ์ฝ ๋ฑ) |
|
|
|
|
|
**์คํ์ผ ๊ฐ์ด๋:** |
|
|
- ์ด๋ชจ์ง๋ฅผ ์ ๊ทน ํ์ฉํ์ฌ ์ฌ๋ฏธ์๊ฒ ์์ฑ |
|
|
- ํ ๋จธ๋ ํ ์๋ฒ์ง๋ ์ด๋ฑํ์๋ ์ดํดํ ์ ์๋ ์ฌ์ด ๋จ์ด ์ฌ์ฉ |
|
|
- ๊ฐ ์ฝ๋ง๋ค ๊ตฌ๋ถ์ ์ผ๋ก ๊ตฌ๋ถ |
|
|
- ์น๊ทผํ๊ณ ๋ฐ๋ปํ ๋งํฌ ์ฌ์ฉ |
|
|
- ๋งํฌ๋ค์ด ํ์์ผ๋ก ์์ฑ |
|
|
|
|
|
์์ํด์ฃผ์ธ์!""" |
|
|
|
|
|
messages = [ |
|
|
{"role": "user", "content": analysis_prompt} |
|
|
] |
|
|
|
|
|
input_text = MED_TOKENIZER.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) |
|
|
inputs = MED_TOKENIZER(input_text, return_tensors="pt").to(MED_MODEL.device) |
|
|
|
|
|
with torch.no_grad(): |
|
|
outputs = MED_MODEL.generate( |
|
|
**inputs, |
|
|
max_new_tokens=768, |
|
|
temperature=0.7, |
|
|
top_p=0.9, |
|
|
do_sample=True |
|
|
) |
|
|
|
|
|
analysis_text = MED_TOKENIZER.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True) |
|
|
|
|
|
analysis_time = time.time() - analysis_start |
|
|
total_time = time.time() - start_time |
|
|
print(f"โฑ๏ธ Medical analysis took {analysis_time:.2f}s") |
|
|
print(f"โฑ๏ธ Total processing time: {total_time:.2f}s") |
|
|
|
|
|
return ocr_text.strip(), analysis_text.strip() |
|
|
|
|
|
except Exception as e: |
|
|
raise Exception(f"๋ถ์ ์ค๋ฅ: {str(e)}") |
|
|
|
|
|
|
|
|
def extract_medications_from_text(text: str) -> List[str]: |
|
|
"""Stage 2: Qwen2.5๋ก ํ
์คํธ์์ ์ฝ ์ด๋ฆ๋ง ์ถ์ถ""" |
|
|
try: |
|
|
messages = [ |
|
|
{ |
|
|
"role": "system", |
|
|
"content": "You are a medical text analyzer. Extract only medication names from the given text and return them as a JSON array. Return ONLY valid JSON format." |
|
|
}, |
|
|
{ |
|
|
"role": "user", |
|
|
"content": f"Extract all medication names from this text:\n\n{text}\n\nReturn format: {{\"medications\": [\"name1\", \"name2\"]}}" |
|
|
} |
|
|
] |
|
|
|
|
|
prompt = LLM_TOKENIZER.apply_chat_template( |
|
|
messages, |
|
|
tokenize=False, |
|
|
add_generation_prompt=True |
|
|
) |
|
|
|
|
|
inputs = LLM_TOKENIZER(prompt, return_tensors="pt").to(LLM_MODEL.device) |
|
|
|
|
|
with torch.no_grad(): |
|
|
outputs = LLM_MODEL.generate( |
|
|
**inputs, |
|
|
max_new_tokens=512, |
|
|
temperature=0.3, |
|
|
top_p=0.9, |
|
|
do_sample=True, |
|
|
pad_token_id=LLM_TOKENIZER.eos_token_id, |
|
|
) |
|
|
|
|
|
response = LLM_TOKENIZER.decode(outputs[0], skip_special_tokens=True) |
|
|
|
|
|
|
|
|
if "<|im_start|>assistant" in response: |
|
|
response = response.split("<|im_start|>assistant")[-1] |
|
|
response = response.replace("<|im_end|>", "").strip() |
|
|
|
|
|
|
|
|
json_match = re.search(r'\{.*?\}', response, re.DOTALL) |
|
|
if json_match: |
|
|
data = json.loads(json_match.group(0)) |
|
|
medications = data.get("medications", []) |
|
|
if isinstance(medications, list) and medications: |
|
|
return [str(m).strip() for m in medications if str(m).strip()] |
|
|
|
|
|
return ["์ฝ ์ด๋ฆ์ ์ฐพ์ง ๋ชปํ์ต๋๋ค."] |
|
|
|
|
|
except Exception as e: |
|
|
raise Exception(f"LLM ๋ถ์ ์ค๋ฅ: {str(e)}") |
|
|
|
|
|
|
|
|
@spaces.GPU(duration=120) |
|
|
def extract_medication_names(image: Image.Image) -> Tuple[str, List[str]]: |
|
|
"""2๋จ๊ณ ํ์ดํ๋ผ์ธ: OCR โ LLM ๋ถ์""" |
|
|
try: |
|
|
|
|
|
extracted_text = extract_text_from_image(image) |
|
|
|
|
|
if not extracted_text: |
|
|
return "", ["ํ
์คํธ๋ฅผ ์ถ์ถํ์ง ๋ชปํ์ต๋๋ค."] |
|
|
|
|
|
|
|
|
medications = extract_medications_from_text(extracted_text) |
|
|
|
|
|
return extracted_text, medications |
|
|
|
|
|
except Exception as e: |
|
|
return "", [f"์ค๋ฅ ๋ฐ์: {str(e)}"] |
|
|
|
|
|
|
|
|
def format_results(extracted_text: str, medications: List[str]) -> Tuple[str, str]: |
|
|
"""๊ฒฐ๊ณผ๋ฅผ ํฌ๋งทํ
""" |
|
|
|
|
|
text_output = f"### ๐ ์ถ์ถ๋ ํ
์คํธ\n\n```\n{extracted_text}\n```" |
|
|
|
|
|
|
|
|
if not medications or medications[0].startswith("์ค๋ฅ") or medications[0].startswith("์ฝ ์ด๋ฆ์ ์ฐพ์ง") or medications[0].startswith("ํ
์คํธ๋ฅผ"): |
|
|
med_output = f"### โ ๏ธ {medications[0] if medications else '์ฝ ์ด๋ฆ์ ์ฐพ์ง ๋ชปํ์ต๋๋ค.'}" |
|
|
else: |
|
|
med_output = f"### ๐ ๊ฒ์ถ๋ ์ฝ๋ฌผ ({len(medications)}๊ฐ)\n\n" |
|
|
for idx, med_name in enumerate(medications, 1): |
|
|
med_output += f"{idx}. **{med_name}**\n" |
|
|
|
|
|
return text_output, med_output |
|
|
|
|
|
|
|
|
def _ensure_pil(image_input: Optional[Union[Image.Image, np.ndarray, str]]) -> Optional[Image.Image]: |
|
|
"""Gradio ์
๋ ฅ์ PIL ์ด๋ฏธ์ง๋ก ๋ณํ""" |
|
|
if image_input is None: |
|
|
return None |
|
|
|
|
|
if isinstance(image_input, Image.Image): |
|
|
return image_input |
|
|
|
|
|
if isinstance(image_input, np.ndarray): |
|
|
if image_input.dtype != np.uint8: |
|
|
image_input = np.clip(image_input, 0, 255).astype(np.uint8) |
|
|
return Image.fromarray(image_input).convert("RGB") |
|
|
|
|
|
if isinstance(image_input, str): |
|
|
if not os.path.exists(image_input): |
|
|
return None |
|
|
with Image.open(image_input) as img: |
|
|
return img.convert("RGB") |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
def run_analysis(image: Optional[Union[Image.Image, np.ndarray, str]], progress=gr.Progress()): |
|
|
"""๋ฉ์ธ ๋ถ์ ํ์ดํ๋ผ์ธ: OCR + ์ฝ ์ ๋ณด ๋ถ์""" |
|
|
pil_image = _ensure_pil(image) |
|
|
|
|
|
if pil_image is None: |
|
|
return "๐ท ์ฝ ๋ดํฌ๋ ์ฒ๋ฐฉ์ ์ฌ์ง์ ์
๋ก๋ํด์ฃผ์ธ์.", "" |
|
|
|
|
|
progress(0.3, desc="๐ธ 1๋จ๊ณ: OCR ํ
์คํธ ์ถ์ถ ์ค...") |
|
|
progress(0.6, desc="๐ค 2๋จ๊ณ: ์ฝ ์ ๋ณด ๋ถ์ ์ค...") |
|
|
|
|
|
try: |
|
|
ocr_text, analysis = analyze_medication_image(pil_image) |
|
|
progress(1.0, desc="โ
์๋ฃ!") |
|
|
|
|
|
ocr_output = f"### ๐ ์ถ์ถ๋ ํ
์คํธ\n\n```\n{ocr_text}\n```" |
|
|
analysis_output = f"### ๐ ์ฝ ์ ๋ณด ์ค๋ช
\n\n{analysis}" |
|
|
|
|
|
return ocr_output, analysis_output |
|
|
except Exception as e: |
|
|
return f"### โ ๏ธ ์ค๋ฅ ๋ฐ์\n\n{str(e)}", "" |
|
|
|
|
|
|
|
|
|
|
|
CUSTOM_CSS = """ |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); |
|
|
|
|
|
:root { |
|
|
--primary: #6366f1; |
|
|
--secondary: #8b5cf6; |
|
|
} |
|
|
|
|
|
body { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
|
|
} |
|
|
|
|
|
.gradio-container { |
|
|
max-width: 900px !important; |
|
|
margin: auto; |
|
|
background: rgba(255, 255, 255, 0.98); |
|
|
border-radius: 24px; |
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.3); |
|
|
padding: 40px; |
|
|
} |
|
|
|
|
|
.hero { |
|
|
text-align: center; |
|
|
padding: 30px 20px; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
border-radius: 20px; |
|
|
color: white; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.hero h1 { |
|
|
font-size: 2.5rem; |
|
|
font-weight: 700; |
|
|
margin-bottom: 10px; |
|
|
} |
|
|
|
|
|
.hero p { |
|
|
font-size: 1.1rem; |
|
|
opacity: 0.95; |
|
|
} |
|
|
|
|
|
.upload-section { |
|
|
background: white; |
|
|
border-radius: 16px; |
|
|
padding: 30px; |
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07); |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.result-section { |
|
|
background: white; |
|
|
border-radius: 16px; |
|
|
padding: 30px; |
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07); |
|
|
min-height: 200px; |
|
|
} |
|
|
|
|
|
.analyze-btn button { |
|
|
background: linear-gradient(135deg, var(--primary), var(--secondary)) !important; |
|
|
color: white !important; |
|
|
font-weight: 600 !important; |
|
|
font-size: 1.1rem !important; |
|
|
padding: 18px 40px !important; |
|
|
border-radius: 12px !important; |
|
|
border: none !important; |
|
|
box-shadow: 0 10px 20px -5px rgba(99, 102, 241, 0.5) !important; |
|
|
transition: all 0.3s ease !important; |
|
|
} |
|
|
|
|
|
.analyze-btn button:hover { |
|
|
transform: translateY(-2px) !important; |
|
|
box-shadow: 0 15px 30px -5px rgba(99, 102, 241, 0.6) !important; |
|
|
} |
|
|
|
|
|
.gr-image { |
|
|
border-radius: 12px !important; |
|
|
} |
|
|
""" |
|
|
|
|
|
HERO_HTML = """ |
|
|
<div class="hero"> |
|
|
<h1>๐ ์ฐ๋ฆฌ ๊ฐ์กฑ ์ฝ ๋์ฐ๋ฏธ</h1> |
|
|
<p>์ฝ๋ดํฌ/์ฒ๋ฐฉ์ ์ฌ์ง์์ ์ฝ ์ ๋ณด๋ฅผ ์ฝ๊ณ ์ฌ๋ฏธ์๊ฒ ์๋ ค๋๋ ค์!</p> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
with gr.Blocks(theme=gr.themes.Soft(), css=CUSTOM_CSS) as demo: |
|
|
gr.HTML(HERO_HTML) |
|
|
|
|
|
with gr.Column(elem_classes=["upload-section"]): |
|
|
gr.Markdown("### ๐ธ ์ฌ์ง ์
๋ก๋") |
|
|
image_input = gr.Image(type="numpy", image_mode="RGB", label="์ฝ๋ดํฌ ๋๋ ์ฒ๋ฐฉ์ ์ฌ์ง", height=350) |
|
|
analyze_button = gr.Button("๐ ์ฝ ์ ๋ณด ๋ถ์ํ๊ธฐ", elem_classes=["analyze-btn"], size="lg") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(elem_classes=["result-section"]): |
|
|
gr.Markdown("### ๐ 1๋จ๊ณ: ์ถ์ถ๋ ํ
์คํธ") |
|
|
ocr_output = gr.Markdown("OCR๋ก ์ถ์ถ๋ ํ
์คํธ๊ฐ ์ฌ๊ธฐ ํ์๋ฉ๋๋ค.") |
|
|
|
|
|
with gr.Column(elem_classes=["result-section"]): |
|
|
gr.Markdown("### ๐ 2๋จ๊ณ: ์ฌ์ด ์ฝ ์ค๋ช
") |
|
|
analysis_output = gr.Markdown("๋
ธ์ธ๊ณผ ์ด๋ฆฐ์ด๋ ์ดํดํ๊ธฐ ์ฌ์ด ์ฝ ์ ๋ณด๊ฐ ์ฌ๊ธฐ ํ์๋ฉ๋๋ค.") |
|
|
|
|
|
analyze_button.click( |
|
|
run_analysis, |
|
|
inputs=image_input, |
|
|
outputs=[ocr_output, analysis_output], |
|
|
) |
|
|
|
|
|
gr.Markdown(""" |
|
|
--- |
|
|
|
|
|
**โน๏ธ ์ฌ์ฉ ๋ฐฉ๋ฒ** |
|
|
1. ์ฝ ๋ดํฌ๋ ์ฒ๋ฐฉ์ ์ฌ์ง์ ์
๋ก๋ํ์ธ์ |
|
|
2. '์ฝ ์ ๋ณด ๋ถ์ํ๊ธฐ' ๋ฒํผ์ ํด๋ฆญํ์ธ์ |
|
|
3. ์ผ์ชฝ์๋ ์ถ์ถ๋ ํ
์คํธ, ์ค๋ฅธ์ชฝ์๋ ์ฌ์ด ์ค๋ช
์ด ๋ํ๋ฉ๋๋ค! |
|
|
|
|
|
**โ ๏ธ ์ฃผ์์ฌํญ** |
|
|
- ์ด ์ฑ์ ์ฐธ๊ณ ์ฉ์ด๋ฉฐ, ์ค์ ๋ณต์ฝ์ ๋ฐ๋์ ์์ฌ๋ ์ฝ์ฌ์ ์ง์๋ฅผ ๋ฐ๋ฅด์ธ์ |
|
|
- AI๊ฐ ์์ฑํ ์ ๋ณด์ด๋ฏ๋ก ์ ํํ์ง ์์ ์ ์์ต๋๋ค |
|
|
|
|
|
**๐ค ๊ธฐ์ ์คํ** |
|
|
- PaddleOCR PP-OCRv5 (ํ๊ตญ์ด ์ต์ ํ OCR) |
|
|
- Google Gemma-2-2B-IT (8bit ์์ํ, ๋น ๋ฅธ ์๋ฃ ์ ๋ณด ๋ถ์) |
|
|
|
|
|
**๐ ์ค์ ๋ฐฉ๋ฒ** |
|
|
- Hugging Face Spaces์ Settings โ Repository secrets์์ `HF_TOKEN` ์ถ๊ฐ ํ์ |
|
|
""") |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.queue().launch() |
|
|
|