File size: 3,370 Bytes
2edb151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
676f5d4
 
 
 
 
 
 
 
2edb151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
from __future__ import annotations

from pathlib import Path

from pydantic_settings import BaseSettings, SettingsConfigDict

ROOT = Path(__file__).resolve().parent.parent

CATEGORIES = (
    "groceries",
    "dining",
    "transport",
    "household",
    "health",
    "entertainment",
    "utilities",
    "office",
    "travel",
    "other",
)

DOC_KINDS = ("receipt", "invoice", "document")

ACCEPTED_SUFFIXES = {
    ".jpg",
    ".jpeg",
    ".png",
    ".webp",
    ".heic",
    ".heif",
    ".tif",
    ".tiff",
    ".pdf",
    ".txt",
}


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="RECEIPT_",
        env_file=str(ROOT / ".env"),
        env_file_encoding="utf-8",
        extra="ignore",
    )

    root_dir: Path = ROOT
    data_dir: Path = ROOT / "data"
    inbox_dir: Path = ROOT / "inbox"
    processing_dir: Path = ROOT / "processing"
    processed_dir: Path = ROOT / "processed"
    failed_dir: Path = ROOT / "failed"
    exports_dir: Path = ROOT / "exports"

    idle_seconds: float = 30.0

    # Gemma 4 12B Unified is the default omni brain (vision extract + embed).
    # It does not fit on the Lamp (6 GB). Point these URLs at the GPU box.
    llm_backend: str = "gemma"
    llm_base_url: str = "http://127.0.0.1:8080/v1"
    llm_model: str = "google/gemma-4-12B-it"
    llm_api_key: str = "local"
    llm_accepts_images: bool = True
    llm_max_tokens: int = 8192
    llm_timeout_s: float = 180.0
    # auto: Studio API if up, else vLLM, else Hermes custom_providers.
    # direct: RECEIPT_LLM_BASE_URL. hermes: Hermes-discovered Gemma. studio: POST /api/inbox.
    llm_route: str = "auto"
    studio_url: str = ""
    gpu_host: str = ""
    hermes_base_url: str = ""
    hermes_model: str = "google/gemma-4-12B-it"
    hermes_config_path: Path = Path.home() / ".hermes" / "config.yaml"

    embed_backend: str = "omni"
    embed_base_url: str = ""
    embed_model: str = "google/gemma-4-12B-it"
    embed_dim: int = 3840
    embed_api_key: str = "local"
    embed_timeout_s: float = 120.0
    embed_prefix: bool = True

    ocr_backend: str = "none"
    ocr_base_url: str = "http://127.0.0.1:11434/v1"
    ocr_model: str = ""
    ocr_api_key: str = "local"

    camera_url: str = "http://127.0.0.1:5001"
    snapshot_width: int = 1280
    snapshot_quality: int = 85

    sku_auto: float = 0.88
    sku_review: float = 0.72
    vendor_auto: float = 0.82
    vendor_review: float = 0.65

    ui_host: str = "127.0.0.1"
    ui_port: int = 7860
    ui_share_lan: bool = False

    jpeg_max_edge: int = 2048

    def model_post_init(self, _context: object) -> None:
        if not self.embed_base_url:
            object.__setattr__(self, "embed_base_url", self.llm_base_url)
        if not self.embed_model:
            object.__setattr__(self, "embed_model", self.llm_model)

    @property
    def db_path(self) -> Path:
        return self.data_dir / "receipts.db"

    def ensure_dirs(self) -> None:
        for path in (
            self.data_dir,
            self.inbox_dir,
            self.processing_dir,
            self.processed_dir,
            self.failed_dir,
            self.exports_dir,
        ):
            path.mkdir(parents=True, exist_ok=True)


def load_settings(**overrides: object) -> Settings:
    settings = Settings(**overrides)
    settings.ensure_dirs()
    return settings