feat: initial scaffold — FastAPI shell + stub vision routers

Phase 4a of the BTE refactor (audit 2026-05-24 §3 V). svrnty-vision is a
sovereign HTTP gateway in front of four vision capabilities — VLM (Spark 2
Qwen3-VL), FLUX image gen (Spark 1 ComfyUI), palette extraction, and
background removal. This commit lays only the scaffold: FastAPI app,
/healthz, four 501-stub routers, pydantic-settings config, pytest smoke.

Real implementations land in Phase 4b. BTE code is untouched in 4a.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Svrnty 2026-05-24 13:25:57 -04:00
commit 2a90c3f884
15 changed files with 416 additions and 0 deletions

18
.env.example Normal file
View File

@ -0,0 +1,18 @@
# svrnty-vision configuration
#
# svrnty-vision is an HTTP gateway. It does NOT run ML models in-process.
# It calls existing Spark services (FLUX, Qwen3-VL) over HTTP.
# Server
SVRNTY_VISION_HOST=0.0.0.0
SVRNTY_VISION_PORT=8090
# Spark 1 — FLUX image generation (ComfyUI HTTP endpoint)
SPARK1_FLUX_URL=http://spark1.lan:8188
# Spark 2 — Qwen3-VL via vLLM (OpenAI-compatible HTTP endpoint)
SPARK2_VLM_URL=http://spark2.lan:8000
SPARK2_VLM_MODEL=Qwen/Qwen3-VL-7B-Instruct
# Defaults
VISION_REQUEST_TIMEOUT_SECONDS=120

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Distribution
build/
dist/
*.egg-info/
*.egg
wheels/
# Virtual environments
.venv/
venv/
env/
# Tests / cache
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
# Environment
.env
.env.local
# IDE
.vscode/
.idea/
*.swp
.DS_Store

78
CLAUDE.md Normal file
View File

@ -0,0 +1,78 @@
# svrnty-vision — orientation for Claude
*Inherits Karpathy 4 rules from `~/.claude/CLAUDE.md` and the workspace
contract from `/home/svrnty/workspaces/hermes/CLAUDE.md`. Read both before
touching anything here.*
## What this repo is
A FastAPI HTTP gateway in front of four vision capabilities (VLM analysis,
FLUX image generation, palette extraction, background removal). It is a
**sibling of `bte/`**, not a child. BTE calls it over HTTP.
## Hard invariants
- **Thin gateway only.** Qwen3-VL runs on Spark 2 (vLLM). FLUX runs on
Spark 1 (ComfyUI). svrnty-vision proxies — it does NOT load model
weights or pull torch/transformers/diffusers in-process. Two exceptions
permitted in Phase 4b: `palette` (Pillow + colorthief) and `rembg`
(rembg lib) — both CPU-light, no GPU.
- **No cloud VLM providers.** The whole point of this extraction is to
delete Anthropic/OpenAI/Google/Higgsfield SDK dependencies from BTE.
Do not reintroduce them here. Sovereign-first.
- **Secrets via env only.** No keys in code, logs, or argv. Use
`pydantic-settings` + `.env` (gitignored).
- **Stay in Python ≥3.11.** Workspace standard.
## Phase status
| Phase | Scope | State |
|---|---|---|
| 4a | Scaffold: FastAPI shell, `/healthz`, four 501 stubs, tests | **done (this commit)** |
| 4b | Port real implementations from BTE; HTTP clients for Spark 1/2 | not started |
| 4c | Delete the corresponding .NET code from BTE | not started |
| 4d | Wire BTE to call svrnty-vision over HTTP via thin adapter | not started |
See `/home/svrnty/workspaces/hermes/sot/01-ROADMAP/BTE-REFACTOR-EXECUTION-PLAN.md`
and `/home/svrnty/workspaces/hermes/bte/docs/REFACTOR-AUDIT-2026-05-24.md` §3 V.
## Layout
```
src/svrnty_vision/
server.py # FastAPI app + /healthz + router includes
settings.py # pydantic-settings (env-driven)
routers/
vlm.py # POST /vlm/analyze (501 stub → Spark 2)
flux.py # POST /flux/render (501 stub → Spark 1)
palette.py # POST /palette/extract (501 stub → in-process)
rembg.py # POST /rembg/cutout (501 stub → in-process)
tests/
test_healthz.py # TestClient smoke
```
## Run / test
```sh
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pip install -e . # required: src/ layout
uvicorn svrnty_vision.server:app --port 8090 # serve
pytest tests/ # test
```
## Git
- Default branch: `jp` (workspace convention).
- Local-only until JP authorises the gitea push:
`git remote add openharbor git@git.openharbor.io:svrnty/svrnty-vision.git`
then `git push -u openharbor jp`.
## When extending
- New endpoint? Add a router under `src/svrnty_vision/routers/`, register
it in `server.py`, add a test in `tests/`.
- New Spark dependency? Add the URL to `settings.py` and `.env.example`,
never hardcode.
- Surgical changes only. Don't refactor adjacent stubs while implementing
one — each phase has its own commit.

67
README.md Normal file
View File

@ -0,0 +1,67 @@
# svrnty-vision
Sovereign vision HTTP gateway. FastAPI shell that exposes four endpoints:
| Endpoint | Purpose | Implementation |
|---|---|---|
| `POST /vlm/analyze` | Vision-language model evaluation | Proxies to Spark 2 (Qwen3-VL via vLLM) |
| `POST /flux/render` | Image generation | Proxies to Spark 1 (FLUX on ComfyUI) |
| `POST /palette/extract` | Dominant-color palette | In-process (Pillow + colorthief) |
| `POST /rembg/cutout` | Background removal | In-process (rembg) |
Plus `GET /healthz` for liveness.
## Why it exists
Brand Truth Engine (BTE) is a .NET 10 + Svrnty.CQRS service. Vision tooling
(VLM, image gen, palette, rembg) was originally embedded in BTE with cloud
provider SDKs (Anthropic, OpenAI, Google, Higgsfield). Per the
2026-05-24 refactor audit, that surface is extracted into this sibling repo
so BTE stays narrow and sovereign-first.
## Architecture invariant
**svrnty-vision is a thin HTTP gateway — it does NOT run heavy ML models
in-process.** Qwen3-VL runs on Spark 2 (vLLM). FLUX runs on Spark 1
(ComfyUI). svrnty-vision orchestrates HTTP calls to those services and
normalises the responses for BTE.
The two in-process exceptions (palette, rembg) are CPU-light Python
libraries — Pillow/colorthief and rembg respectively. They land in 4b.
## Status
**Phase 4a (this commit):** scaffold only. All four endpoints return
HTTP 501 Not Implemented. `/healthz` returns 200.
Phase 4b will port the real logic. Phase 4c deletes the corresponding
.NET code from BTE. Phase 4d wires BTE to call svrnty-vision over HTTP.
See `/home/svrnty/workspaces/hermes/sot/01-ROADMAP/BTE-REFACTOR-EXECUTION-PLAN.md`.
## Run
```sh
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e . # required: src/ layout
# either:
python -m svrnty_vision.server
# or:
uvicorn svrnty_vision.server:app --port 8090
curl http://localhost:8090/healthz
# {"status":"ok","version":"0.1.0"}
```
## Test
```sh
pytest tests/
```
## Configuration
Copy `.env.example` to `.env` and edit. All settings have safe defaults
for local development (Spark 1/2 hostnames are placeholders).

33
pyproject.toml Normal file
View File

@ -0,0 +1,33 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "svrnty-vision"
version = "0.1.0"
description = "Sovereign vision HTTP gateway — VLM analysis, FLUX image gen, palette extraction, background removal. Calls Spark services over HTTP."
readme = "README.md"
requires-python = ">=3.11"
license = { text = "Proprietary" }
authors = [
{ name = "Svrnty", email = "mathias@openharbor.io" },
]
dependencies = [
"fastapi>=0.115,<1.0",
"uvicorn[standard]>=0.32,<1.0",
"pydantic>=2.9,<3.0",
"pydantic-settings>=2.6,<3.0",
"httpx>=0.27,<1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3,<9.0",
]
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
fastapi>=0.115,<1.0
uvicorn[standard]>=0.32,<1.0
pydantic>=2.9,<3.0
pydantic-settings>=2.6,<3.0
httpx>=0.27,<1.0
# Test deps (kept here for simplicity in Phase 4a)
pytest>=8.3,<9.0

View File

@ -0,0 +1,3 @@
"""svrnty-vision — sovereign vision HTTP gateway."""
__version__ = "0.1.0"

View File

@ -0,0 +1 @@
"""Vision endpoint routers — all 501-stubs in Phase 4a."""

View File

@ -0,0 +1,17 @@
"""FLUX image generation — stub until Phase 4b wires the ComfyUI HTTP client."""
from fastapi import APIRouter, HTTPException, status
router = APIRouter(prefix="/flux", tags=["flux"])
@router.post("/render")
async def render() -> None:
"""Render an image via Spark 1 (FLUX on ComfyUI).
Phase 4a: stub. Phase 4b: proxies to Spark 1 ComfyUI workflow.
"""
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="flux.render not implemented in Phase 4a — see BTE-REFACTOR-EXECUTION-PLAN Phase 4b",
)

View File

@ -0,0 +1,17 @@
"""Palette extraction (ColorThief-equivalent) — stub until Phase 4b."""
from fastapi import APIRouter, HTTPException, status
router = APIRouter(prefix="/palette", tags=["palette"])
@router.post("/extract")
async def extract() -> None:
"""Extract a dominant-color palette from an image.
Phase 4a: stub. Phase 4b: runs in-process (Pillow + colorthief).
"""
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="palette.extract not implemented in Phase 4a — see BTE-REFACTOR-EXECUTION-PLAN Phase 4b",
)

View File

@ -0,0 +1,17 @@
"""Background removal — stub until Phase 4b."""
from fastapi import APIRouter, HTTPException, status
router = APIRouter(prefix="/rembg", tags=["rembg"])
@router.post("/cutout")
async def cutout() -> None:
"""Remove the background of an image (alpha cutout).
Phase 4a: stub. Phase 4b: runs in-process (rembg) or proxies to a Spark service.
"""
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="rembg.cutout not implemented in Phase 4a — see BTE-REFACTOR-EXECUTION-PLAN Phase 4b",
)

View File

@ -0,0 +1,17 @@
"""VLM (vision-language model) analysis — stub until Phase 4b moves Qwen3-VL code."""
from fastapi import APIRouter, HTTPException, status
router = APIRouter(prefix="/vlm", tags=["vlm"])
@router.post("/analyze")
async def analyze() -> None:
"""Analyze an image with a vision-language model.
Phase 4a: stub. Phase 4b: proxies to Spark 2 (Qwen3-VL via vLLM).
"""
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="vlm.analyze not implemented in Phase 4a — see BTE-REFACTOR-EXECUTION-PLAN Phase 4b",
)

View File

@ -0,0 +1,40 @@
"""FastAPI application entry point."""
from fastapi import FastAPI
from svrnty_vision import __version__
from svrnty_vision.routers import flux, palette, rembg, vlm
app = FastAPI(
title="svrnty-vision",
version=__version__,
description="Sovereign vision HTTP gateway — VLM, FLUX, palette, rembg.",
)
app.include_router(vlm.router)
app.include_router(flux.router)
app.include_router(palette.router)
app.include_router(rembg.router)
@app.get("/healthz")
async def healthz() -> dict[str, str]:
"""Liveness probe."""
return {"status": "ok", "version": __version__}
def main() -> None:
"""Run with `python -m svrnty_vision.server`."""
import uvicorn
from svrnty_vision.settings import settings
uvicorn.run(
"svrnty_vision.server:app",
host=settings.svrnty_vision_host,
port=settings.svrnty_vision_port,
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,30 @@
"""Settings loaded from environment / .env."""
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Runtime configuration for svrnty-vision."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
# Server
svrnty_vision_host: str = "0.0.0.0"
svrnty_vision_port: int = 8090
# Spark 1 — FLUX (ComfyUI)
spark1_flux_url: str = "http://spark1.lan:8188"
# Spark 2 — Qwen3-VL (vLLM, OpenAI-compatible)
spark2_vlm_url: str = "http://spark2.lan:8000"
spark2_vlm_model: str = "Qwen/Qwen3-VL-7B-Instruct"
# Common
vision_request_timeout_seconds: int = 120
settings = Settings()

35
tests/test_healthz.py Normal file
View File

@ -0,0 +1,35 @@
"""Smoke tests for the FastAPI scaffold."""
from fastapi.testclient import TestClient
from svrnty_vision.server import app
client = TestClient(app)
def test_healthz_returns_200() -> None:
response = client.get("/healthz")
assert response.status_code == 200
body = response.json()
assert body["status"] == "ok"
assert "version" in body
def test_vlm_analyze_returns_501() -> None:
response = client.post("/vlm/analyze")
assert response.status_code == 501
def test_flux_render_returns_501() -> None:
response = client.post("/flux/render")
assert response.status_code == 501
def test_palette_extract_returns_501() -> None:
response = client.post("/palette/extract")
assert response.status_code == 501
def test_rembg_cutout_returns_501() -> None:
response = client.post("/rembg/cutout")
assert response.status_code == 501