#!/usr/bin/env python3 """ mflux-service — Standalone FastAPI wrapper for mflux image generation. Runs NATIVELY on Mac Mini (requires Metal GPU, not Docker). Generates images using Flux Schnell via the mflux library. Setup: python3 -m venv ~/mflux-env source ~/mflux-env/bin/activate pip install mflux fastapi uvicorn Run: source ~/mflux-env/bin/activate python scripts/mflux-service.py Or as a background service: nohup ~/mflux-env/bin/python scripts/mflux-service.py > /tmp/mflux-service.log 2>&1 & License: Apache-2.0 """ import base64 import io import logging import os import time from typing import Optional import uvicorn from fastapi import FastAPI from pydantic import BaseModel logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger("mflux-service") app = FastAPI(title="mflux Image Generation Service", version="1.0.0") # Lazy-loaded generator _flux = None def _get_flux(): """Lazy-load the Flux model on first use.""" global _flux if _flux is None: logger.info("Loading Flux Schnell model (first call, may download ~12 GB)...") from mflux import Flux1 _flux = Flux1( model_name="schnell", quantize=8, ) logger.info("Flux Schnell model loaded.") return _flux class GenerateRequest(BaseModel): prompt: str width: int = 512 height: int = 512 steps: int = 4 seed: Optional[int] = None class GenerateResponse(BaseModel): image_b64: Optional[str] = None success: bool = True error: Optional[str] = None duration_ms: int = 0 @app.get("/health") async def health(): return {"status": "ok", "model": "flux-schnell", "gpu": "metal"} @app.post("/generate", response_model=GenerateResponse) async def generate_image(req: GenerateRequest): """Generate an image from a text prompt using Flux Schnell.""" t0 = time.time() # Validate dimensions (must be multiples of 64 for Flux) width = max(256, min(1024, (req.width // 64) * 64)) height = max(256, min(1024, (req.height // 64) * 64)) try: from mflux import Config flux = _get_flux() image = flux.generate_image( seed=req.seed or int(time.time()) % 2**31, prompt=req.prompt, config=Config( num_inference_steps=req.steps, height=height, width=width, ), ) # Convert PIL image to base64 buf = io.BytesIO() image.save(buf, format="PNG") buf.seek(0) img_b64 = "data:image/png;base64," + base64.b64encode(buf.read()).decode("utf-8") duration_ms = int((time.time() - t0) * 1000) logger.info(f"Generated {width}x{height} image in {duration_ms}ms: {req.prompt[:60]}...") return GenerateResponse(image_b64=img_b64, success=True, duration_ms=duration_ms) except Exception as e: duration_ms = int((time.time() - t0) * 1000) logger.error(f"Generation failed: {e}") return GenerateResponse(image_b64=None, success=False, error=str(e), duration_ms=duration_ms) if __name__ == "__main__": port = int(os.getenv("MFLUX_PORT", "8095")) logger.info(f"Starting mflux-service on port {port}") uvicorn.run(app, host="0.0.0.0", port=port)