From 2a90c3f884faa2236fe40716659481ffa05ae728 Mon Sep 17 00:00:00 2001 From: Svrnty Date: Sun, 24 May 2026 13:25:57 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20initial=20scaffold=20=E2=80=94=20FastAP?= =?UTF-8?q?I=20shell=20+=20stub=20vision=20routers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .env.example | 18 +++++++ .gitignore | 35 ++++++++++++ CLAUDE.md | 78 +++++++++++++++++++++++++++ README.md | 67 +++++++++++++++++++++++ pyproject.toml | 33 ++++++++++++ requirements.txt | 8 +++ src/svrnty_vision/__init__.py | 3 ++ src/svrnty_vision/routers/__init__.py | 1 + src/svrnty_vision/routers/flux.py | 17 ++++++ src/svrnty_vision/routers/palette.py | 17 ++++++ src/svrnty_vision/routers/rembg.py | 17 ++++++ src/svrnty_vision/routers/vlm.py | 17 ++++++ src/svrnty_vision/server.py | 40 ++++++++++++++ src/svrnty_vision/settings.py | 30 +++++++++++ tests/test_healthz.py | 35 ++++++++++++ 15 files changed, 416 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/svrnty_vision/__init__.py create mode 100644 src/svrnty_vision/routers/__init__.py create mode 100644 src/svrnty_vision/routers/flux.py create mode 100644 src/svrnty_vision/routers/palette.py create mode 100644 src/svrnty_vision/routers/rembg.py create mode 100644 src/svrnty_vision/routers/vlm.py create mode 100644 src/svrnty_vision/server.py create mode 100644 src/svrnty_vision/settings.py create mode 100644 tests/test_healthz.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fe42f94 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..531e6a1 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b329bd2 --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9dfb82 --- /dev/null +++ b/README.md @@ -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). diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..051f123 --- /dev/null +++ b/pyproject.toml @@ -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"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e1b2466 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/src/svrnty_vision/__init__.py b/src/svrnty_vision/__init__.py new file mode 100644 index 0000000..085d9d5 --- /dev/null +++ b/src/svrnty_vision/__init__.py @@ -0,0 +1,3 @@ +"""svrnty-vision — sovereign vision HTTP gateway.""" + +__version__ = "0.1.0" diff --git a/src/svrnty_vision/routers/__init__.py b/src/svrnty_vision/routers/__init__.py new file mode 100644 index 0000000..2af54f8 --- /dev/null +++ b/src/svrnty_vision/routers/__init__.py @@ -0,0 +1 @@ +"""Vision endpoint routers — all 501-stubs in Phase 4a.""" diff --git a/src/svrnty_vision/routers/flux.py b/src/svrnty_vision/routers/flux.py new file mode 100644 index 0000000..770731f --- /dev/null +++ b/src/svrnty_vision/routers/flux.py @@ -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", + ) diff --git a/src/svrnty_vision/routers/palette.py b/src/svrnty_vision/routers/palette.py new file mode 100644 index 0000000..463a1a5 --- /dev/null +++ b/src/svrnty_vision/routers/palette.py @@ -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", + ) diff --git a/src/svrnty_vision/routers/rembg.py b/src/svrnty_vision/routers/rembg.py new file mode 100644 index 0000000..4089fd7 --- /dev/null +++ b/src/svrnty_vision/routers/rembg.py @@ -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", + ) diff --git a/src/svrnty_vision/routers/vlm.py b/src/svrnty_vision/routers/vlm.py new file mode 100644 index 0000000..e32a658 --- /dev/null +++ b/src/svrnty_vision/routers/vlm.py @@ -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", + ) diff --git a/src/svrnty_vision/server.py b/src/svrnty_vision/server.py new file mode 100644 index 0000000..80ca606 --- /dev/null +++ b/src/svrnty_vision/server.py @@ -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() diff --git a/src/svrnty_vision/settings.py b/src/svrnty_vision/settings.py new file mode 100644 index 0000000..6644cf1 --- /dev/null +++ b/src/svrnty_vision/settings.py @@ -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() diff --git a/tests/test_healthz.py b/tests/test_healthz.py new file mode 100644 index 0000000..14747dd --- /dev/null +++ b/tests/test_healthz.py @@ -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