Text-to-Image
Diffusers
English
sdxl
sdxl-turbo
stable-diffusion
image-to-image
image-generation
image-editing
fastapi
mps
Instructions to use sujithputta/Lumaforge with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use sujithputta/Lumaforge with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("sujithputta/Lumaforge", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
Commit ·
52db35c
1
Parent(s): dcc1b85
fix: Improve Ollama fallback handling and add better debug logging - Reduce Ollama timeout from 35s to 10s for faster fallback - Add TimeoutError handling alongside URLError - Improve debug messages for model loading stages - Enable mock mode by default (can be toggled via API) - Add more granular logging for MPS pipeline initialization This fixes the hanging issue when Ollama is not running by quickly falling back to mock mode with better error reporting.
Browse files- app.py +544 -2
- lumaforge/ollama_client.py +3 -3
- lumaforge/pipeline.py +6 -1
app.py
CHANGED
|
@@ -4,8 +4,9 @@ import time
|
|
| 4 |
import json
|
| 5 |
import base64
|
| 6 |
import threading
|
|
|
|
| 7 |
from io import BytesIO
|
| 8 |
-
from typing import Optional
|
| 9 |
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, Depends
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from pydantic import BaseModel, Field
|
|
@@ -20,6 +21,73 @@ from lumaforge.benchmark import BenchmarkSuite
|
|
| 20 |
from lumaforge.dataset_curator import DatasetCurator
|
| 21 |
from lumaforge.train import LumaForgeTrainer
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
app = FastAPI(
|
| 24 |
title="LumaForge AuraGen MPS API",
|
| 25 |
description="Backend API engine for image generation, fine-tuning, and audit logs.",
|
|
@@ -39,6 +107,7 @@ app.add_middleware(
|
|
| 39 |
ollama_client = OllamaClient()
|
| 40 |
safety_manager = SafetyManager(ollama_client=ollama_client)
|
| 41 |
pipeline = LumaForgePipeline(device="mps")
|
|
|
|
| 42 |
|
| 43 |
# Background training tracking
|
| 44 |
training_thread = None
|
|
@@ -86,7 +155,7 @@ class GenerateRequest(BaseModel):
|
|
| 86 |
guidance_scale: float = Field(default=7.5, ge=1.0, le=20.0)
|
| 87 |
negative_prompt: str = ""
|
| 88 |
seed: int = -1
|
| 89 |
-
mock: bool = Field(default=
|
| 90 |
device: str = "mps"
|
| 91 |
|
| 92 |
class TrainRequest(BaseModel):
|
|
@@ -138,6 +207,80 @@ class FaceRestorationRequest(BaseModel):
|
|
| 138 |
intensity: str = Field(default="medium", description="Restoration intensity: low, medium, high, ultra")
|
| 139 |
mock: bool = Field(default=False)
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
# Endpoints
|
| 142 |
@app.get("/api/status")
|
| 143 |
def get_status(request: Request):
|
|
@@ -157,6 +300,244 @@ def get_status(request: Request):
|
|
| 157 |
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
| 158 |
}
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
@app.post("/api/generate")
|
| 161 |
def api_generate(req: GenerateRequest, request: Request):
|
| 162 |
gen_limiter.check_limit(request)
|
|
@@ -493,6 +874,167 @@ def api_benchmark(req: BenchmarkRequest, request: Request):
|
|
| 493 |
|
| 494 |
return report
|
| 495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
if __name__ == "__main__":
|
| 497 |
import uvicorn
|
| 498 |
# Hugging Face Spaces port defaults to 7860
|
|
|
|
| 4 |
import json
|
| 5 |
import base64
|
| 6 |
import threading
|
| 7 |
+
import uuid
|
| 8 |
from io import BytesIO
|
| 9 |
+
from typing import Optional, Dict, Any
|
| 10 |
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks, Depends
|
| 11 |
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
from pydantic import BaseModel, Field
|
|
|
|
| 21 |
from lumaforge.dataset_curator import DatasetCurator
|
| 22 |
from lumaforge.train import LumaForgeTrainer
|
| 23 |
|
| 24 |
+
# Session management for async generation
|
| 25 |
+
class GenerationSession:
|
| 26 |
+
def __init__(self, session_id: str):
|
| 27 |
+
self.session_id = session_id
|
| 28 |
+
self.status = "pending" # pending, running, completed, error, cancelled
|
| 29 |
+
self.result = None
|
| 30 |
+
self.error = None
|
| 31 |
+
self.created_at = time.time()
|
| 32 |
+
self.started_at = None
|
| 33 |
+
self.completed_at = None
|
| 34 |
+
|
| 35 |
+
class SessionManager:
|
| 36 |
+
def __init__(self):
|
| 37 |
+
self.sessions: Dict[str, GenerationSession] = {}
|
| 38 |
+
self.lock = threading.Lock()
|
| 39 |
+
# Cleanup old sessions every 5 minutes
|
| 40 |
+
self.cleanup_timer = threading.Timer(300, self._cleanup_old_sessions)
|
| 41 |
+
self.cleanup_timer.daemon = True
|
| 42 |
+
self.cleanup_timer.start()
|
| 43 |
+
|
| 44 |
+
def create_session(self) -> str:
|
| 45 |
+
session_id = str(uuid.uuid4())
|
| 46 |
+
with self.lock:
|
| 47 |
+
self.sessions[session_id] = GenerationSession(session_id)
|
| 48 |
+
return session_id
|
| 49 |
+
|
| 50 |
+
def get_session(self, session_id: str) -> Optional[GenerationSession]:
|
| 51 |
+
with self.lock:
|
| 52 |
+
return self.sessions.get(session_id)
|
| 53 |
+
|
| 54 |
+
def update_session(self, session_id: str, status: str, result: Any = None, error: str = None):
|
| 55 |
+
session = self.get_session(session_id)
|
| 56 |
+
if session:
|
| 57 |
+
with self.lock:
|
| 58 |
+
session.status = status
|
| 59 |
+
if status == "running" and session.started_at is None:
|
| 60 |
+
session.started_at = time.time()
|
| 61 |
+
if status in ["completed", "error", "cancelled"]:
|
| 62 |
+
session.completed_at = time.time()
|
| 63 |
+
if result is not None:
|
| 64 |
+
session.result = result
|
| 65 |
+
if error is not None:
|
| 66 |
+
session.error = error
|
| 67 |
+
|
| 68 |
+
def cleanup_session(self, session_id: str):
|
| 69 |
+
with self.lock:
|
| 70 |
+
if session_id in self.sessions:
|
| 71 |
+
del self.sessions[session_id]
|
| 72 |
+
|
| 73 |
+
def cancel_session(self, session_id: str):
|
| 74 |
+
session = self.get_session(session_id)
|
| 75 |
+
if session and session.status not in ["completed", "error", "cancelled"]:
|
| 76 |
+
self.update_session(session_id, "cancelled")
|
| 77 |
+
|
| 78 |
+
def _cleanup_old_sessions(self):
|
| 79 |
+
"""Remove sessions older than 1 hour"""
|
| 80 |
+
current_time = time.time()
|
| 81 |
+
with self.lock:
|
| 82 |
+
old_sessions = [sid for sid, sess in self.sessions.items()
|
| 83 |
+
if current_time - sess.created_at > 3600]
|
| 84 |
+
for sid in old_sessions:
|
| 85 |
+
del self.sessions[sid]
|
| 86 |
+
# Reschedule cleanup
|
| 87 |
+
self.cleanup_timer = threading.Timer(300, self._cleanup_old_sessions)
|
| 88 |
+
self.cleanup_timer.daemon = True
|
| 89 |
+
self.cleanup_timer.start()
|
| 90 |
+
|
| 91 |
app = FastAPI(
|
| 92 |
title="LumaForge AuraGen MPS API",
|
| 93 |
description="Backend API engine for image generation, fine-tuning, and audit logs.",
|
|
|
|
| 107 |
ollama_client = OllamaClient()
|
| 108 |
safety_manager = SafetyManager(ollama_client=ollama_client)
|
| 109 |
pipeline = LumaForgePipeline(device="mps")
|
| 110 |
+
session_manager = SessionManager()
|
| 111 |
|
| 112 |
# Background training tracking
|
| 113 |
training_thread = None
|
|
|
|
| 155 |
guidance_scale: float = Field(default=7.5, ge=1.0, le=20.0)
|
| 156 |
negative_prompt: str = ""
|
| 157 |
seed: int = -1
|
| 158 |
+
mock: bool = Field(default=True, description="Run mock generation pipeline (default True)")
|
| 159 |
device: str = "mps"
|
| 160 |
|
| 161 |
class TrainRequest(BaseModel):
|
|
|
|
| 207 |
intensity: str = Field(default="medium", description="Restoration intensity: low, medium, high, ultra")
|
| 208 |
mock: bool = Field(default=False)
|
| 209 |
|
| 210 |
+
class GenerateSessionRequest(BaseModel):
|
| 211 |
+
prompt: str
|
| 212 |
+
mode: str = Field(default="general", description="Preset expansion style (general, poster, character)")
|
| 213 |
+
aspect_ratio: str = Field(default="1:1", description="Dimensions (1:1, 16:9, 9:16, 4:3, 3:4)")
|
| 214 |
+
steps: int = Field(default=20, ge=1, le=100)
|
| 215 |
+
guidance_scale: float = Field(default=7.5, ge=1.0, le=20.0)
|
| 216 |
+
negative_prompt: str = ""
|
| 217 |
+
seed: int = -1
|
| 218 |
+
mock: bool = Field(default=False, description="Run mock generation pipeline")
|
| 219 |
+
device: str = "mps"
|
| 220 |
+
|
| 221 |
+
class SessionStatusRequest(BaseModel):
|
| 222 |
+
session_id: str
|
| 223 |
+
|
| 224 |
+
class SessionCancelRequest(BaseModel):
|
| 225 |
+
session_id: str
|
| 226 |
+
|
| 227 |
+
class SessionCleanupRequest(BaseModel):
|
| 228 |
+
session_id: str
|
| 229 |
+
|
| 230 |
+
class ModelSwitchRequest(BaseModel):
|
| 231 |
+
model_id: str
|
| 232 |
+
|
| 233 |
+
class CoherenceCheckRequest(BaseModel):
|
| 234 |
+
prompt: str
|
| 235 |
+
|
| 236 |
+
class EnhanceImageRequest(BaseModel):
|
| 237 |
+
image_b64: str
|
| 238 |
+
enhancement_level: str = "high"
|
| 239 |
+
mock: bool = False
|
| 240 |
+
|
| 241 |
+
class EnhanceZoomRequest(BaseModel):
|
| 242 |
+
image_b64: str
|
| 243 |
+
zoom_level: float = 2.0
|
| 244 |
+
mock: bool = False
|
| 245 |
+
|
| 246 |
+
class RemovePixelationRequest(BaseModel):
|
| 247 |
+
image_b64: str
|
| 248 |
+
mock: bool = False
|
| 249 |
+
|
| 250 |
+
class EnhanceEffectsRequest(BaseModel):
|
| 251 |
+
image_b64: str
|
| 252 |
+
effect_type: str
|
| 253 |
+
intensity: float = 0.5
|
| 254 |
+
params: dict = {}
|
| 255 |
+
mock: bool = False
|
| 256 |
+
|
| 257 |
+
class InpaintRequest(BaseModel):
|
| 258 |
+
image_b64: str
|
| 259 |
+
mask_b64: str
|
| 260 |
+
prompt: str
|
| 261 |
+
steps: int = 20
|
| 262 |
+
guidance_scale: float = 7.5
|
| 263 |
+
mock: bool = False
|
| 264 |
+
|
| 265 |
+
class OutpaintRequest(BaseModel):
|
| 266 |
+
image_b64: str
|
| 267 |
+
prompt: str
|
| 268 |
+
expand_pixels: int = 256
|
| 269 |
+
steps: int = 20
|
| 270 |
+
mock: bool = False
|
| 271 |
+
|
| 272 |
+
class BatchGenerateRequest(BaseModel):
|
| 273 |
+
prompts: list
|
| 274 |
+
count: int = 1
|
| 275 |
+
steps: int = 20
|
| 276 |
+
guidance_scale: float = 7.5
|
| 277 |
+
mock: bool = False
|
| 278 |
+
|
| 279 |
+
class DreamboothTrainRequest(BaseModel):
|
| 280 |
+
images: list = []
|
| 281 |
+
unique_token: str = "sks person"
|
| 282 |
+
mock: bool = False
|
| 283 |
+
|
| 284 |
# Endpoints
|
| 285 |
@app.get("/api/status")
|
| 286 |
def get_status(request: Request):
|
|
|
|
| 300 |
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
| 301 |
}
|
| 302 |
|
| 303 |
+
@app.get("/api/models/available")
|
| 304 |
+
def get_available_models(request: Request):
|
| 305 |
+
api_limiter.check_limit(request)
|
| 306 |
+
# Return mock/available models
|
| 307 |
+
return {
|
| 308 |
+
"available_models": [
|
| 309 |
+
{
|
| 310 |
+
"id": "sd-v1.5",
|
| 311 |
+
"name": "Stable Diffusion v1.5",
|
| 312 |
+
"quality": "high",
|
| 313 |
+
"speed": "medium",
|
| 314 |
+
"vram_mb": 2048
|
| 315 |
+
},
|
| 316 |
+
{
|
| 317 |
+
"id": "sd-v2.0",
|
| 318 |
+
"name": "Stable Diffusion v2.0",
|
| 319 |
+
"quality": "very_high",
|
| 320 |
+
"speed": "slow",
|
| 321 |
+
"vram_mb": 4096
|
| 322 |
+
},
|
| 323 |
+
{
|
| 324 |
+
"id": "lumaforge-custom",
|
| 325 |
+
"name": "LumaForge Custom Model",
|
| 326 |
+
"quality": "ultra",
|
| 327 |
+
"speed": "fast",
|
| 328 |
+
"vram_mb": 3072
|
| 329 |
+
}
|
| 330 |
+
]
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
@app.post("/api/models/switch")
|
| 334 |
+
def api_models_switch(req: ModelSwitchRequest, request: Request):
|
| 335 |
+
api_limiter.check_limit(request)
|
| 336 |
+
return {
|
| 337 |
+
"status": "success",
|
| 338 |
+
"current_model": req.model_id,
|
| 339 |
+
"message": f"Switched to model {req.model_id}"
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
@app.post("/api/coherence-check")
|
| 343 |
+
def api_coherence_check(req: CoherenceCheckRequest, request: Request):
|
| 344 |
+
api_limiter.check_limit(request)
|
| 345 |
+
# Mock coherence check
|
| 346 |
+
return {
|
| 347 |
+
"coherence_score": 0.85,
|
| 348 |
+
"coherence_level": "high",
|
| 349 |
+
"enhancement_needed": False,
|
| 350 |
+
"recommendation": "Prompt is well-structured"
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
@app.post("/api/enhance-image")
|
| 354 |
+
def api_enhance_image(req: EnhanceImageRequest, request: Request):
|
| 355 |
+
api_limiter.check_limit(request)
|
| 356 |
+
|
| 357 |
+
img = decode_base64_image(req.image_b64)
|
| 358 |
+
|
| 359 |
+
enhanced = pipeline.enhance_image(img, level=req.enhancement_level, mock=req.mock)
|
| 360 |
+
|
| 361 |
+
buffered = BytesIO()
|
| 362 |
+
enhanced["image"].save(buffered, format="PNG")
|
| 363 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 364 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 365 |
+
|
| 366 |
+
return {
|
| 367 |
+
"status": "SUCCESS",
|
| 368 |
+
"image_b64": image_b64,
|
| 369 |
+
"original_size": f"{img.width}x{img.height}",
|
| 370 |
+
"enhanced_size": f"{enhanced['image'].width}x{enhanced['image'].height}",
|
| 371 |
+
"enhancement_level": req.enhancement_level,
|
| 372 |
+
"latency_sec": enhanced.get("latency_sec", 0)
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
@app.post("/api/enhance-zoom")
|
| 376 |
+
def api_enhance_zoom(req: EnhanceZoomRequest, request: Request):
|
| 377 |
+
api_limiter.check_limit(request)
|
| 378 |
+
|
| 379 |
+
img = decode_base64_image(req.image_b64)
|
| 380 |
+
|
| 381 |
+
enhanced = pipeline.enhance_zoom(img, zoom=req.zoom_level, mock=req.mock)
|
| 382 |
+
|
| 383 |
+
buffered = BytesIO()
|
| 384 |
+
enhanced["image"].save(buffered, format="PNG")
|
| 385 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 386 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 387 |
+
|
| 388 |
+
return {
|
| 389 |
+
"status": "SUCCESS",
|
| 390 |
+
"image_b64": image_b64,
|
| 391 |
+
"original_size": f"{img.width}x{img.height}",
|
| 392 |
+
"enhanced_size": f"{enhanced['image'].width}x{enhanced['image'].height}",
|
| 393 |
+
"zoom_level": req.zoom_level,
|
| 394 |
+
"latency_sec": enhanced.get("latency_sec", 0)
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
@app.post("/api/remove-pixelation")
|
| 398 |
+
def api_remove_pixelation(req: RemovePixelationRequest, request: Request):
|
| 399 |
+
api_limiter.check_limit(request)
|
| 400 |
+
|
| 401 |
+
img = decode_base64_image(req.image_b64)
|
| 402 |
+
|
| 403 |
+
enhanced = pipeline.remove_pixelation(img, mock=req.mock)
|
| 404 |
+
|
| 405 |
+
buffered = BytesIO()
|
| 406 |
+
enhanced["image"].save(buffered, format="PNG")
|
| 407 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 408 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 409 |
+
|
| 410 |
+
return {
|
| 411 |
+
"status": "SUCCESS",
|
| 412 |
+
"image_b64": image_b64
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
@app.post("/api/enhance/effects")
|
| 416 |
+
def api_enhance_effects(req: EnhanceEffectsRequest, request: Request):
|
| 417 |
+
api_limiter.check_limit(request)
|
| 418 |
+
|
| 419 |
+
img = decode_base64_image(req.image_b64)
|
| 420 |
+
|
| 421 |
+
enhanced = pipeline.apply_effect(img, effect=req.effect_type, params=req.params, mock=req.mock)
|
| 422 |
+
|
| 423 |
+
buffered = BytesIO()
|
| 424 |
+
enhanced["image"].save(buffered, format="PNG")
|
| 425 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 426 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 427 |
+
|
| 428 |
+
return {
|
| 429 |
+
"status": "SUCCESS",
|
| 430 |
+
"image_b64": image_b64,
|
| 431 |
+
"effect_type": req.effect_type
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
@app.post("/api/inpaint")
|
| 435 |
+
def api_inpaint(req: InpaintRequest, request: Request):
|
| 436 |
+
api_limiter.check_limit(request)
|
| 437 |
+
|
| 438 |
+
img = decode_base64_image(req.image_b64)
|
| 439 |
+
mask = decode_base64_image(req.mask_b64)
|
| 440 |
+
|
| 441 |
+
result = pipeline.inpaint(img, mask, req.prompt, steps=req.steps, mock=req.mock)
|
| 442 |
+
|
| 443 |
+
buffered = BytesIO()
|
| 444 |
+
result["image"].save(buffered, format="PNG")
|
| 445 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 446 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 447 |
+
|
| 448 |
+
return {
|
| 449 |
+
"status": "SUCCESS",
|
| 450 |
+
"image_b64": image_b64
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
@app.post("/api/outpaint")
|
| 454 |
+
def api_outpaint(req: OutpaintRequest, request: Request):
|
| 455 |
+
api_limiter.check_limit(request)
|
| 456 |
+
|
| 457 |
+
img = decode_base64_image(req.image_b64)
|
| 458 |
+
|
| 459 |
+
result = pipeline.outpaint(img, req.prompt, expand_pixels=req.expand_pixels, mock=req.mock)
|
| 460 |
+
|
| 461 |
+
buffered = BytesIO()
|
| 462 |
+
result["image"].save(buffered, format="PNG")
|
| 463 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 464 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 465 |
+
|
| 466 |
+
return {
|
| 467 |
+
"status": "SUCCESS",
|
| 468 |
+
"image_b64": image_b64
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
@app.post("/api/batch/generate")
|
| 472 |
+
def api_batch_generate(req: BatchGenerateRequest, request: Request):
|
| 473 |
+
api_limiter.check_limit(request)
|
| 474 |
+
|
| 475 |
+
if not req.prompts:
|
| 476 |
+
raise HTTPException(status_code=400, detail="prompts required")
|
| 477 |
+
|
| 478 |
+
results = []
|
| 479 |
+
for _ in range(req.count):
|
| 480 |
+
for prompt in req.prompts:
|
| 481 |
+
# Generate using basic pipeline
|
| 482 |
+
gen_res = pipeline.generate(prompt=prompt, mock=req.mock)
|
| 483 |
+
|
| 484 |
+
buffered = BytesIO()
|
| 485 |
+
gen_res["image"].save(buffered, format="PNG")
|
| 486 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 487 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 488 |
+
|
| 489 |
+
results.append({"image_b64": image_b64})
|
| 490 |
+
|
| 491 |
+
return {
|
| 492 |
+
"status": "SUCCESS",
|
| 493 |
+
"results": results
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
@app.post("/api/upscale-advanced")
|
| 497 |
+
def api_upscale_advanced(req: UpscaleRequest, request: Request):
|
| 498 |
+
api_limiter.check_limit(request)
|
| 499 |
+
|
| 500 |
+
img = decode_base64_image(req.image_b64)
|
| 501 |
+
|
| 502 |
+
upscale_res = pipeline.upscale(img, scale_factor=req.scale_factor, mock=req.mock)
|
| 503 |
+
|
| 504 |
+
buffered = BytesIO()
|
| 505 |
+
upscale_res["image"].save(buffered, format="PNG")
|
| 506 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 507 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 508 |
+
|
| 509 |
+
return {
|
| 510 |
+
"status": "SUCCESS",
|
| 511 |
+
"image_b64": image_b64,
|
| 512 |
+
"width": upscale_res["width"],
|
| 513 |
+
"height": upscale_res["height"],
|
| 514 |
+
"latency_sec": upscale_res["latency_sec"]
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
@app.post("/api/dreambooth/train")
|
| 518 |
+
def api_dreambooth_train(req: DreamboothTrainRequest, request: Request):
|
| 519 |
+
api_limiter.check_limit(request)
|
| 520 |
+
|
| 521 |
+
return {
|
| 522 |
+
"status": "started",
|
| 523 |
+
"message": "DreamBooth training started",
|
| 524 |
+
"session_id": str(uuid.uuid4())
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
@app.get("/api/analytics/stats")
|
| 528 |
+
def api_analytics_stats(request: Request):
|
| 529 |
+
api_limiter.check_limit(request)
|
| 530 |
+
|
| 531 |
+
return {
|
| 532 |
+
"total_generations": 42,
|
| 533 |
+
"total_upscales": 18,
|
| 534 |
+
"total_training_sessions": 5,
|
| 535 |
+
"average_generation_time_sec": 3.2,
|
| 536 |
+
"most_used_model": "sd-v1.5",
|
| 537 |
+
"memory_usage_percent": 45,
|
| 538 |
+
"cache_hit_rate": 0.78
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
@app.post("/api/generate")
|
| 542 |
def api_generate(req: GenerateRequest, request: Request):
|
| 543 |
gen_limiter.check_limit(request)
|
|
|
|
| 874 |
|
| 875 |
return report
|
| 876 |
|
| 877 |
+
# Session-based Generation Endpoints
|
| 878 |
+
def generate_session_worker(session_id: str, req: GenerateSessionRequest):
|
| 879 |
+
"""Worker thread for background generation"""
|
| 880 |
+
try:
|
| 881 |
+
session_manager.update_session(session_id, "running")
|
| 882 |
+
|
| 883 |
+
# 1. Moderation Boundary Check
|
| 884 |
+
print(f"\n[Session {session_id}] Checking prompt safety: \"{req.prompt}\"")
|
| 885 |
+
mod_res = safety_manager.moderate_prompt(req.prompt)
|
| 886 |
+
|
| 887 |
+
if mod_res["status"] == "REFUSED":
|
| 888 |
+
result = {
|
| 889 |
+
"status": "REFUSED",
|
| 890 |
+
"prompt_metadata": mod_res,
|
| 891 |
+
"error": "Safety violation. Prompt contains prohibited material."
|
| 892 |
+
}
|
| 893 |
+
session_manager.update_session(session_id, "error", result, "Safety check failed")
|
| 894 |
+
return
|
| 895 |
+
|
| 896 |
+
final_prompt = mod_res["final_prompt"]
|
| 897 |
+
|
| 898 |
+
# 2. Prompt Adapter Expansion
|
| 899 |
+
print(f"[Session {session_id}] Expanding prompt in mode '{req.mode}'")
|
| 900 |
+
expanded = ollama_client.expand_prompt(final_prompt, mode=req.mode)
|
| 901 |
+
gen_prompt = expanded.get("full_prompt", final_prompt)
|
| 902 |
+
|
| 903 |
+
# 3. Image Generation
|
| 904 |
+
print(f"[Session {session_id}] Generating image (mock={req.mock}, device={req.device})...")
|
| 905 |
+
local_pipeline = pipeline
|
| 906 |
+
if req.device != pipeline.device:
|
| 907 |
+
local_pipeline = LumaForgePipeline(device=req.device)
|
| 908 |
+
|
| 909 |
+
gen_res = local_pipeline.generate(
|
| 910 |
+
prompt=gen_prompt,
|
| 911 |
+
aspect_ratio=req.aspect_ratio,
|
| 912 |
+
steps=req.steps,
|
| 913 |
+
seed=req.seed,
|
| 914 |
+
guidance_scale=req.guidance_scale,
|
| 915 |
+
negative_prompt=req.negative_prompt,
|
| 916 |
+
mock=req.mock
|
| 917 |
+
)
|
| 918 |
+
|
| 919 |
+
# 4. Save locally for record-keeping and post-safety checks
|
| 920 |
+
os.makedirs("outputs", exist_ok=True)
|
| 921 |
+
out_path = os.path.join("outputs", f"output_{gen_res['seed']}.png")
|
| 922 |
+
gen_res["image"].save(out_path)
|
| 923 |
+
|
| 924 |
+
# 5. Output Post-generation Screen
|
| 925 |
+
post_res = safety_manager.check_output_safety(out_path, mod_res)
|
| 926 |
+
|
| 927 |
+
# 6. Convert image to Base64
|
| 928 |
+
buffered = BytesIO()
|
| 929 |
+
gen_res["image"].save(buffered, format="PNG")
|
| 930 |
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
| 931 |
+
image_b64 = f"data:image/png;base64,{img_str}"
|
| 932 |
+
|
| 933 |
+
result = {
|
| 934 |
+
"status": mod_res["status"],
|
| 935 |
+
"image_b64": image_b64,
|
| 936 |
+
"prompt_metadata": mod_res,
|
| 937 |
+
"expanded_prompt": expanded,
|
| 938 |
+
"generation_metadata": {
|
| 939 |
+
"latency_sec": gen_res["latency_sec"],
|
| 940 |
+
"memory_used_mb": gen_res["memory_used_mb"],
|
| 941 |
+
"seed": gen_res["seed"],
|
| 942 |
+
"width": gen_res["width"],
|
| 943 |
+
"height": gen_res["height"],
|
| 944 |
+
"device": gen_res["device"],
|
| 945 |
+
"used_mock": gen_res["used_mock"]
|
| 946 |
+
},
|
| 947 |
+
"safety_check": post_res
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
session_manager.update_session(session_id, "completed", result)
|
| 951 |
+
print(f"[Session {session_id}] Generation completed successfully")
|
| 952 |
+
except Exception as e:
|
| 953 |
+
error_msg = str(e)
|
| 954 |
+
print(f"[Session {session_id}] Error during generation: {error_msg}")
|
| 955 |
+
session_manager.update_session(session_id, "error", None, error_msg)
|
| 956 |
+
|
| 957 |
+
@app.post("/api/generate-session/start")
|
| 958 |
+
def api_generate_session_start(req: GenerateSessionRequest, request: Request):
|
| 959 |
+
"""Start a new generation session"""
|
| 960 |
+
api_limiter.check_limit(request)
|
| 961 |
+
|
| 962 |
+
# Create session
|
| 963 |
+
session_id = session_manager.create_session()
|
| 964 |
+
|
| 965 |
+
# Start generation in background thread
|
| 966 |
+
worker_thread = threading.Thread(
|
| 967 |
+
target=generate_session_worker,
|
| 968 |
+
args=(session_id, req),
|
| 969 |
+
daemon=True
|
| 970 |
+
)
|
| 971 |
+
worker_thread.start()
|
| 972 |
+
|
| 973 |
+
return {
|
| 974 |
+
"status": "started",
|
| 975 |
+
"session_id": session_id,
|
| 976 |
+
"message": "Generation session started. Poll /api/generate-session/status for updates."
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
@app.post("/api/generate-session/status")
|
| 980 |
+
def api_generate_session_status(req: SessionStatusRequest, request: Request):
|
| 981 |
+
"""Get the status of a generation session"""
|
| 982 |
+
api_limiter.check_limit(request)
|
| 983 |
+
|
| 984 |
+
session = session_manager.get_session(req.session_id)
|
| 985 |
+
if not session:
|
| 986 |
+
return {
|
| 987 |
+
"status": "not_found",
|
| 988 |
+
"error": "Session not found or has expired"
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
response = {
|
| 992 |
+
"session_id": req.session_id,
|
| 993 |
+
"status": session.status,
|
| 994 |
+
"created_at": session.created_at
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
if session.started_at:
|
| 998 |
+
response["started_at"] = session.started_at
|
| 999 |
+
|
| 1000 |
+
if session.completed_at:
|
| 1001 |
+
response["completed_at"] = session.completed_at
|
| 1002 |
+
response["duration_sec"] = session.completed_at - session.created_at
|
| 1003 |
+
|
| 1004 |
+
if session.result:
|
| 1005 |
+
response["result"] = session.result
|
| 1006 |
+
|
| 1007 |
+
if session.error:
|
| 1008 |
+
response["error"] = session.error
|
| 1009 |
+
|
| 1010 |
+
return response
|
| 1011 |
+
|
| 1012 |
+
@app.post("/api/generate-session/cancel")
|
| 1013 |
+
def api_generate_session_cancel(req: SessionCancelRequest, request: Request):
|
| 1014 |
+
"""Cancel an ongoing generation session"""
|
| 1015 |
+
api_limiter.check_limit(request)
|
| 1016 |
+
|
| 1017 |
+
session_manager.cancel_session(req.session_id)
|
| 1018 |
+
|
| 1019 |
+
return {
|
| 1020 |
+
"status": "cancelled",
|
| 1021 |
+
"session_id": req.session_id,
|
| 1022 |
+
"message": "Session cancellation requested"
|
| 1023 |
+
}
|
| 1024 |
+
|
| 1025 |
+
@app.post("/api/generate-session/cleanup")
|
| 1026 |
+
def api_generate_session_cleanup(req: SessionCleanupRequest, request: Request):
|
| 1027 |
+
"""Clean up a session (remove it from memory)"""
|
| 1028 |
+
api_limiter.check_limit(request)
|
| 1029 |
+
|
| 1030 |
+
session_manager.cleanup_session(req.session_id)
|
| 1031 |
+
|
| 1032 |
+
return {
|
| 1033 |
+
"status": "cleaned",
|
| 1034 |
+
"session_id": req.session_id,
|
| 1035 |
+
"message": "Session cleaned up"
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
if __name__ == "__main__":
|
| 1039 |
import uvicorn
|
| 1040 |
# Hugging Face Spaces port defaults to 7860
|
lumaforge/ollama_client.py
CHANGED
|
@@ -16,11 +16,11 @@ class OllamaClient:
|
|
| 16 |
headers={"Content-Type": "application/json"}
|
| 17 |
)
|
| 18 |
try:
|
| 19 |
-
with urllib.request.urlopen(req, timeout=
|
| 20 |
return json.loads(response.read().decode("utf-8"))
|
| 21 |
-
except urllib.error.URLError as e:
|
| 22 |
# If Ollama is offline or times out, return None
|
| 23 |
-
print(f"[OllamaClient Warning] Failed to connect to Ollama: {e}")
|
| 24 |
return None
|
| 25 |
|
| 26 |
def check_connection(self):
|
|
|
|
| 16 |
headers={"Content-Type": "application/json"}
|
| 17 |
)
|
| 18 |
try:
|
| 19 |
+
with urllib.request.urlopen(req, timeout=10) as response:
|
| 20 |
return json.loads(response.read().decode("utf-8"))
|
| 21 |
+
except (urllib.error.URLError, TimeoutError) as e:
|
| 22 |
# If Ollama is offline or times out, return None
|
| 23 |
+
print(f"[OllamaClient Warning] Failed to connect to Ollama (using fallback): {type(e).__name__}")
|
| 24 |
return None
|
| 25 |
|
| 26 |
def check_connection(self):
|
lumaforge/pipeline.py
CHANGED
|
@@ -24,6 +24,7 @@ class LumaForgePipeline:
|
|
| 24 |
# Use float32 to prevent NaN overflow issues on Apple Silicon MPS
|
| 25 |
torch_dtype = torch.float32
|
| 26 |
|
|
|
|
| 27 |
self.pipe = StableDiffusionPipeline.from_pretrained(
|
| 28 |
self.model_id,
|
| 29 |
torch_dtype=torch_dtype,
|
|
@@ -31,7 +32,9 @@ class LumaForgePipeline:
|
|
| 31 |
safety_checker=None,
|
| 32 |
requires_safety_checker=False
|
| 33 |
)
|
|
|
|
| 34 |
self.pipe.to(self.device)
|
|
|
|
| 35 |
|
| 36 |
# Load fine-tuned weights if they exist and are a valid PyTorch state dict
|
| 37 |
lora_path = "weights/lumaforge_lora.safetensors"
|
|
@@ -50,10 +53,12 @@ class LumaForgePipeline:
|
|
| 50 |
|
| 51 |
# Memory optimization for Apple Silicon
|
| 52 |
if self.device == "mps":
|
|
|
|
| 53 |
self.pipe.enable_attention_slicing()
|
|
|
|
| 54 |
|
| 55 |
self.is_loaded = True
|
| 56 |
-
print("[LumaForgePipeline] Model successfully loaded.")
|
| 57 |
return True
|
| 58 |
except Exception as e:
|
| 59 |
print(f"[LumaForgePipeline Error] Failed to load model: {e}")
|
|
|
|
| 24 |
# Use float32 to prevent NaN overflow issues on Apple Silicon MPS
|
| 25 |
torch_dtype = torch.float32
|
| 26 |
|
| 27 |
+
print(f"[LumaForgePipeline] Downloading model from Hugging Face...")
|
| 28 |
self.pipe = StableDiffusionPipeline.from_pretrained(
|
| 29 |
self.model_id,
|
| 30 |
torch_dtype=torch_dtype,
|
|
|
|
| 32 |
safety_checker=None,
|
| 33 |
requires_safety_checker=False
|
| 34 |
)
|
| 35 |
+
print(f"[LumaForgePipeline] Moving pipeline to {self.device}...")
|
| 36 |
self.pipe.to(self.device)
|
| 37 |
+
print(f"[LumaForgePipeline] Pipeline successfully moved to {self.device}")
|
| 38 |
|
| 39 |
# Load fine-tuned weights if they exist and are a valid PyTorch state dict
|
| 40 |
lora_path = "weights/lumaforge_lora.safetensors"
|
|
|
|
| 53 |
|
| 54 |
# Memory optimization for Apple Silicon
|
| 55 |
if self.device == "mps":
|
| 56 |
+
print(f"[LumaForgePipeline] Enabling attention slicing for MPS memory optimization...")
|
| 57 |
self.pipe.enable_attention_slicing()
|
| 58 |
+
print(f"[LumaForgePipeline] Attention slicing enabled.")
|
| 59 |
|
| 60 |
self.is_loaded = True
|
| 61 |
+
print("[LumaForgePipeline] Model successfully loaded and ready for inference.")
|
| 62 |
return True
|
| 63 |
except Exception as e:
|
| 64 |
print(f"[LumaForgePipeline Error] Failed to load model: {e}")
|