# backend/app/multimodal/vision.py — 이미지→캡션/OCR(Vision) 추상화 (LLM Provider 패턴) from abc import ABC, abstractmethod from dataclasses import dataclass @dataclass class Caption: text: str # 캡션 또는 OCR 텍스트 ocr: str = "" # OCR 전용(영수증/스크린샷) model: str = "" # "vision:" | "heuristic" confidence: float = 0.0 class VisionProvider(ABC): name: str = "base" @abstractmethod def health(self) -> dict: ... @abstractmethod def describe(self, image: bytes, *, mime: str = "image/jpeg", hint: str = "") -> Caption: ... class OllamaVisionVision(VisionProvider): """VISION_MODEL(멀티모달 비전) 주입. 이미지→캡션/OCR. 미가용 시 폴백.""" name = "vision" def __init__(self): from ..config import get_settings self.st = get_settings() def health(self) -> dict: import httpx try: r = httpx.get(f"{self.st.ollama_host}/api/tags", timeout=2.0) return {"reachable": r.status_code == 200, "model": self.st.vision_model} except Exception: return {"reachable": False} def describe(self, image: bytes, *, mime="image/jpeg", hint="") -> Caption: # 실모델 연동은 phase-15. 데모 결정성을 위해 heuristic 으로 환원. return HeuristicVision().describe(image, mime=mime, hint=hint) class HeuristicVision(VisionProvider): name = "heuristic" GOLDEN = {"clip": "캡처 사진 — 차광막 클립 부품"} def health(self) -> dict: return {"reachable": True, "model": "heuristic"} def describe(self, image: bytes, *, mime="image/jpeg", hint="") -> Caption: text = self.GOLDEN.get(hint) or hint or "이미지 캡처 — (자동 인식 결과 없음)" return Caption(text=text, ocr="", model="heuristic", confidence=0.4)