| from __future__ import annotations |
|
|
| import base64 |
| from pathlib import Path |
|
|
| import httpx |
|
|
| from app.config import Settings |
| from backends.base import OCRResult |
| from backends.openai_compat import OpenAICompatEmbed, OpenAICompatLLM |
|
|
|
|
| def _is_lightning(model: str) -> bool: |
| return "lightning" in model.lower() |
|
|
|
|
| class OllamaLLM(OpenAICompatLLM): |
| def __init__(self, settings: Settings, *, client: httpx.Client | None = None) -> None: |
| accepts = settings.llm_accepts_images and not _is_lightning(settings.llm_model) |
| super().__init__( |
| settings, |
| name="ollama", |
| accepts_images=accepts, |
| extra_body={}, |
| client=client, |
| ) |
|
|
|
|
| class OllamaEmbed(OpenAICompatEmbed): |
| def __init__(self, settings: Settings, *, client: httpx.Client | None = None) -> None: |
| super().__init__(settings, name="ollama-embed", client=client) |
|
|
|
|
| class OllamaOCR: |
| name = "ollama-ocr" |
|
|
| def __init__(self, settings: Settings, *, client: httpx.Client | None = None) -> None: |
| self.settings = settings |
| self._client = client |
|
|
| def ocr(self, path: Path) -> OCRResult: |
| if not self.settings.ocr_model: |
| raise NotImplementedError("set RECEIPT_OCR_MODEL for Ollama vision OCR") |
| jpeg = path.read_bytes() |
| b64 = base64.b64encode(jpeg).decode("ascii") |
| own = self._client is None |
| http = self._client or httpx.Client( |
| base_url=self.settings.ocr_base_url.rstrip("/"), |
| timeout=self.settings.llm_timeout_s, |
| headers={"Authorization": f"Bearer {self.settings.ocr_api_key}"}, |
| ) |
| try: |
| response = http.post( |
| "/chat/completions", |
| json={ |
| "model": self.settings.ocr_model, |
| "messages": [ |
| { |
| "role": "user", |
| "content": [ |
| { |
| "type": "image_url", |
| "image_url": { |
| "url": f"data:image/jpeg;base64,{b64}" |
| }, |
| }, |
| { |
| "type": "text", |
| "text": "Transcribe this document verbatim.", |
| }, |
| ], |
| } |
| ], |
| "temperature": 0, |
| "max_tokens": 4096, |
| }, |
| ) |
| response.raise_for_status() |
| text = response.json()["choices"][0]["message"]["content"] |
| finally: |
| if own: |
| http.close() |
| return OCRResult(text=text or "", engine=self.name) |
|
|