File size: 4,507 Bytes
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""One-click start: optional Gemma vLLM (≤15GB), then LAN UI + browser."""

from __future__ import annotations

import os
import shutil
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
import webbrowser
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
HOST = "127.0.0.1"
PORT = int(os.environ.get("RECEIPT_UI_PORT", "7860"))
VLLM_PORT = int(os.environ.get("RECEIPT_VLLM_PORT", "8080"))
START_VLLM = os.environ.get("RECEIPT_START_VLLM", "1").lower() not in {"0", "false", "no"}
MAX_GB = os.environ.get("RECEIPT_VLLM_MAX_GB", "15")  # unused if UTIL is set in serve-gemma.sh


def _lan_ip() -> str:
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        sock.connect(("192.0.2.1", 1))
        return sock.getsockname()[0]
    except OSError:
        return "127.0.0.1"
    finally:
        sock.close()


def _port_up(port: int) -> bool:
    sock = socket.socket()
    sock.settimeout(0.4)
    try:
        sock.connect((HOST, port))
        return True
    except OSError:
        return False
    finally:
        sock.close()


def _vllm_ready() -> bool:
    try:
        with urllib.request.urlopen(
            f"http://127.0.0.1:{VLLM_PORT}/v1/models", timeout=2
        ) as response:
            return response.status == 200
    except (urllib.error.URLError, TimeoutError, OSError):
        return False


def _can_serve_gemma() -> bool:
    if shutil.which("vllm") is None:
        return False
    model = Path(os.environ.get("RECEIPT_GEMMA_PATH", str(Path.home() / "models-gemma4-12b-it")))
    return (model / "config.json").is_file()


def _spawn(cmd: list[str], log: Path, *, bash: bool = False) -> None:
    log.parent.mkdir(parents=True, exist_ok=True)
    creation = 0
    if sys.platform == "win32":
        creation = getattr(subprocess, "CREATE_NEW_CONSOLE", 0)
    argv = cmd
    if bash:
        argv = ["bash", *cmd]
    with log.open("a", encoding="utf-8") as handle:
        subprocess.Popen(
            argv,
            cwd=str(ROOT),
            env=os.environ.copy(),
            stdout=handle,
            stderr=handle,
            creationflags=creation,
            start_new_session=sys.platform != "win32",
        )


def _ensure_vllm() -> None:
    if not START_VLLM:
        return
    if _vllm_ready():
        print(f"Gemma already up on :{VLLM_PORT} (not restarted; 15GB cap applies on a fresh serve).")
        return
    if not _can_serve_gemma():
        print("No local vLLM/Gemma — skip serve. Point .env at the GPU box.")
        return
    script = ROOT / "scripts" / "serve-gemma.sh"
    if not script.is_file():
        print(f"missing {script}", file=sys.stderr)
        return
    os.environ.setdefault("RECEIPT_VLLM_MAX_GB", MAX_GB)
    print("Starting Gemma 4 12B vLLM at gpu_memory_utilization=0.15 (FP8, max-model-len 8192)…")
    _spawn([str(script)], ROOT / "data" / "vllm-gemma.log", bash=True)
    for _ in range(120):
        if _vllm_ready():
            print("Gemma ready.")
            return
        time.sleep(5)
    print(
        f"vLLM still starting. Watch {ROOT / 'data' / 'vllm-gemma.log'}",
        file=sys.stderr,
    )


def _spawn_ui() -> None:
    env = os.environ.copy()
    env["RECEIPT_UI_SHARE_LAN"] = "true"
    env.setdefault("RECEIPT_IDLE_SECONDS", "5")
    log = ROOT / "data" / "ui.log"
    log.parent.mkdir(parents=True, exist_ok=True)
    creation = 0
    if sys.platform == "win32":
        creation = getattr(subprocess, "CREATE_NEW_CONSOLE", 0)
    with log.open("a", encoding="utf-8") as handle:
        subprocess.Popen(
            [sys.executable, "-m", "app.cli", "ui"],
            cwd=str(ROOT),
            env=env,
            stdout=handle,
            stderr=handle,
            creationflags=creation,
            start_new_session=sys.platform != "win32",
        )


def main() -> None:
    os.chdir(ROOT)
    _ensure_vllm()
    if not _port_up(PORT):
        print("Starting Receipt Studio UI…")
        _spawn_ui()
        for _ in range(40):
            if _port_up(PORT):
                break
            time.sleep(0.25)
        else:
            print(f"UI did not bind :{PORT}. See {ROOT / 'data' / 'ui.log'}", file=sys.stderr)
            raise SystemExit(1)
    lan = _lan_ip()
    review = f"http://127.0.0.1:{PORT}"
    phone = f"http://{lan}:{PORT}/phone"
    print(f"Review: {review}")
    print(f"Phone:  {phone}")
    webbrowser.open(review)


if __name__ == "__main__":
    main()