Server Management¶
The Python server is managed entirely by the Swift app. Users never need to interact with it directly.
ServerManager¶
ServerManager (@MainActor) manages the Python server process lifecycle. It tracks four states:
| Status | Meaning |
|---|---|
stopped |
Server is not running |
starting |
Server process launched, waiting for model to load |
running |
Model loaded, ready to accept predictions |
error |
Server crashed too many times |
Startup Sequence¶
- Check for existing server: GET
http://127.0.0.1:8765/health— if it responds withmodel_loaded: true, skip launching (server may be running from a previous session or manual start) - Find Python binary (resolution order):
- Managed venv:
~/Library/Application Support/Mathy/venv/bin/python3 - UserDefaults
pythonPathkey (user override from settings) - Project
.venv/bin/python3(developer workflow) /opt/homebrew/bin/python3(Homebrew on Apple Silicon)/usr/local/bin/python3(Homebrew on Intel)/usr/bin/python3(macOS system)which python3(runs off main thread)
- Managed venv:
- Find server script: Bundled copy in
Bundle.main, orserver/mathy_server.pyin the project root - Launch process:
python3 mathy_server.py 8765 - Start health polling: Timer fires every 1 second, GET
/health, waiting formodel_loaded == true
Health Polling¶
Once the server process is launched, ServerManager polls the health endpoint every second:
GET /health
Response: {"status": "ok", "model_loaded": true}
The model_loaded field starts as false while pix2tex loads its model weights (~10-15s on first run). Once it becomes true, ServerManager sets status to .running and cancels the polling timer.
Auto-Restart¶
If the server process terminates unexpectedly, the Process.terminationHandler triggers auto-restart with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st | 2 seconds |
| 2nd | 4 seconds |
| 3rd | 6 seconds |
| 4th+ | Gives up, sets status to .error |
The restart counter resets when the server successfully reaches .running status.
Shutdown¶
On app quit, AppState.deinit calls serverManager.stop():
- Invalidates the health polling timer
- Calls
process.terminate()on the child process - Sets status to
.stopped
Process I/O¶
- stdout is redirected to
/dev/null(not needed by the app) - stderr is captured via a
PipewithreadabilityHandlerand logged to console as[mathy-server]prefixed lines - The
readabilityHandlerpattern prevents pipe buffer deadlocks
Project Root Resolution¶
To find the server script and project .venv during development, ServerManager resolves the project root by:
- Checking the
SOURCE_ROOTenvironment variable (set by Xcode) - Walking up from
Bundle.main.bundleURLup to 8 parent directories, looking forserver/mathy_server.py
Python Server (mathy_server.py)¶
The server is a FastAPI application served by uvicorn, bound to 127.0.0.1:8765 (localhost only — not exposed to the network).
Model Loading¶
Model loading happens at startup in a FastAPI lifespan context manager:
@asynccontextmanager
async def lifespan(app):
model = LatexOCR() # Loads pix2tex model
model_loaded = True
yield
- Imports
pix2tex.cli.LatexOCRand instantiates it - First run downloads model weights (~200MB) to the pix2tex cache
- Subsequent launches reuse cached weights (~10-15s load time)
- If loading fails,
model_loadedstaysFalseand/predictreturns 503
API Endpoints¶
GET /health¶
Returns server status. Used by ServerManager for health polling.
{"status": "ok", "model_loaded": true}
POST /predict¶
Accepts an image file and returns recognized LaTeX.
Request: multipart/form-data with a file field containing the image (PNG, JPEG, etc.)
Response (200):
{"latex": "\\frac{1}{2}"}
Error responses:
| Code | Condition | Body |
|---|---|---|
| 400 | Image couldn't be decoded | {"detail": "Invalid image: ..."} |
| 503 | Model not loaded yet | {"detail": "Model not loaded yet"} |
| 500 | Prediction failed | {"detail": "Prediction failed: ..."} |
Prediction flow:
- Read uploaded file bytes
- Open with PIL (
Image.open), convert to RGB - Call
model(img)— pix2tex inference - Return LaTeX string
Running Manually¶
For development, the server can be started independently:
# Using the project venv
source .venv/bin/activate
python server/mathy_server.py
# Or with a custom port
python server/mathy_server.py 9000
Test endpoints:
curl http://127.0.0.1:8765/health
curl -X POST -F "file=@equation.png" http://127.0.0.1:8765/predict