aboutsummaryrefslogtreecommitdiffstats
path: root/server.py
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-07-08 16:06:20 +0200
committerDanilo M. <danix@danix.xyz>2026-07-08 16:06:20 +0200
commitf1c4df943554ad11d39dc62280d41de2a1ef925d (patch)
treef0092e63b4f5d50480e6a511286f907328b5dc56 /server.py
parentd34cc72ceedf964da446ea3749ba43aaaeac7b22 (diff)
downloadimggen-f1c4df943554ad11d39dc62280d41de2a1ef925d.tar.gz
imggen-f1c4df943554ad11d39dc62280d41de2a1ef925d.zip
Add persistent HTTP daemon holding one model resident in VRAM
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'server.py')
-rw-r--r--server.py131
1 files changed, 131 insertions, 0 deletions
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..f3be9e5
--- /dev/null
+++ b/server.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python3
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# Long-lived daemon that keeps ONE SDXL model resident in XPU VRAM so prompt
+# iteration skips the model-reload tax. Stdlib HTTP only. Serial by design:
+# one GPU, so concurrent generation would OOM. See README.md.
+
+import io
+import json
+import os
+import sys
+from http.server import BaseHTTPRequestHandler, HTTPServer
+
+import torch
+
+import generate
+
+PORT = int(os.environ.get("IMGGEN_PORT", "8765"))
+
+STATE = {"model": None, "pipe": None}
+
+
+def load(model_key):
+ if STATE["model"] == model_key and STATE["pipe"] is not None:
+ return
+ if STATE["pipe"] is not None:
+ del STATE["pipe"]
+ STATE["pipe"] = None
+ STATE["model"] = None
+ torch.xpu.empty_cache()
+ STATE["pipe"] = generate.build_pipe(model_key)
+ STATE["model"] = model_key
+
+
+class Handler(BaseHTTPRequestHandler):
+ def _json(self, code, obj):
+ body = json.dumps(obj).encode()
+ self.send_response(code)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def _read_json(self):
+ length = int(self.headers.get("Content-Length", "0"))
+ raw = self.rfile.read(length) if length else b"{}"
+ return json.loads(raw) # raises on bad JSON -> caught by caller
+
+ def do_GET(self):
+ if self.path == "/status":
+ self._json(200, {"model": STATE["model"], "ready": STATE["pipe"] is not None})
+ else:
+ self._json(404, {"error": "not found"})
+
+ def do_POST(self):
+ try:
+ data = self._read_json()
+ except (ValueError, json.JSONDecodeError):
+ self._json(400, {"error": "bad JSON"})
+ return
+
+ if self.path == "/model":
+ name = data.get("name")
+ try:
+ generate.resolve_model(name)
+ except SystemExit as e:
+ self._json(400, {"error": str(e)})
+ return
+ try:
+ load(name)
+ except Exception as e: # download / load failure
+ self._json(500, {"error": f"load failed: {e}"})
+ return
+ self._json(200, {"model": STATE["model"], "ready": True})
+ return
+
+ if self.path == "/generate":
+ try:
+ image, elapsed = generate.generate(
+ STATE["pipe"],
+ data["prompt"],
+ data.get("negative", ""),
+ int(data.get("steps", 25)),
+ float(data.get("guidance", 7.0)),
+ data.get("seed"),
+ int(data.get("width", 1024)),
+ int(data.get("height", 1024)),
+ )
+ except torch.OutOfMemoryError:
+ torch.xpu.empty_cache()
+ self._json(500, {"error": "OOM, try fewer steps or smaller size"})
+ return
+ except Exception as e:
+ self._json(500, {"error": str(e)})
+ return
+ buf = io.BytesIO()
+ image.save(buf, format="PNG")
+ body = buf.getvalue()
+ self.send_response(200)
+ self.send_header("Content-Type", "image/png")
+ self.send_header("X-Elapsed", f"{elapsed:.2f}")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+ return
+
+ self._json(404, {"error": "not found"})
+
+ def log_message(self, fmt, *args):
+ sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
+
+
+def main():
+ startup = os.environ.get("IMGGEN_MODEL", generate.DEFAULT_MODEL)
+ print(f"Loading {startup} on XPU ...", file=sys.stderr)
+ load(startup)
+ print(f"Ready on 127.0.0.1:{PORT} with model {startup}", file=sys.stderr)
+ HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
+
+
+if __name__ == "__main__":
+ main()