💬 Commit message: Update 2026-02-15 08:03:38, 10 files, 372 lines

📁 Files changed: 10
📝 Lines changed: 372

  • deploy.md
  • .coverage
  • deploy.sh
  • __init__.py
  • comfy_routes.py
  • index-BEOoAFTp.css
  • index-Wi7i1Rks.js
  • index.html
  • GenerateView.vue
  • app.ts
This commit is contained in:
Adam Ladachowski
2026-02-15 08:03:38 +01:00
parent 356d8fd156
commit 3638bd7d81
10 changed files with 299 additions and 73 deletions
+2
View File
@@ -12,6 +12,7 @@ from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from tensors.server.civitai_routes import create_civitai_router
from tensors.server.comfy_routes import create_comfy_router
from tensors.server.db_routes import create_db_router
from tensors.server.download_routes import create_download_router
from tensors.server.gallery_routes import create_gallery_router
@@ -53,6 +54,7 @@ def create_app() -> FastAPI:
return {"status": "ok"}
app.include_router(create_civitai_router())
app.include_router(create_comfy_router())
app.include_router(create_db_router())
app.include_router(create_gallery_router())
app.include_router(create_download_router())
+252
View File
@@ -0,0 +1,252 @@
"""ComfyUI integration routes for model management and generation."""
from __future__ import annotations
import base64
import logging
import random
from typing import Any
import httpx
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from tensors.comfy import ComfyClient, get_last_checkpoint, save_last_checkpoint
logger = logging.getLogger(__name__)
COMFY_URL = "http://junkpile:8188"
def get_comfy_client() -> ComfyClient:
"""Get a ComfyUI client instance."""
return ComfyClient(base_url=COMFY_URL)
class ModelInfo(BaseModel):
"""Model information."""
name: str
path: str
filename: str
size_mb: float = 0
modified: int = 0
category: str = "sd15"
class LoRAInfo(BaseModel):
"""LoRA information."""
name: str
path: str
filename: str
size_mb: float = 0
modified: int = 0
category: str = "sd15"
class SwitchModelRequest(BaseModel):
"""Request to switch models."""
model: str
class LoraConfig(BaseModel):
"""LoRA configuration for generation."""
path: str
multiplier: float = 0.8
class GenerateRequest(BaseModel):
"""Image generation request."""
prompt: str
negative_prompt: str = ""
width: int = 512
height: int = 512
steps: int = 20
cfg_scale: float = 7.0
seed: int = -1
sampler: str = "euler_ancestral"
save_to_gallery: bool = True
lora: LoraConfig | None = None
class GeneratedImage(BaseModel):
"""Generated image info."""
id: str
data: str # base64 encoded
seed: int
def create_comfy_router() -> APIRouter: # noqa: PLR0915
"""Create the ComfyUI integration router."""
router = APIRouter(prefix="/api", tags=["comfy"])
@router.get("/models")
async def list_models() -> dict[str, Any]:
"""List available checkpoint models."""
try:
client = get_comfy_client()
checkpoints = client.get_checkpoints()
models = []
for ckpt in checkpoints:
# Determine category based on filename
lower = ckpt.lower()
category = "large" if "xl" in lower or "pony" in lower else "sd15"
models.append(
ModelInfo(
name=ckpt.replace(".safetensors", "").replace(".ckpt", ""),
path=ckpt,
filename=ckpt,
category=category,
)
)
return {"models": [m.model_dump() for m in models], "total": len(models)}
except httpx.HTTPError as e:
logger.exception("Failed to connect to ComfyUI")
raise HTTPException(status_code=503, detail=f"ComfyUI not available: {e}") from e
@router.get("/models/active")
async def get_active_model() -> dict[str, Any]:
"""Get the currently active/selected model."""
last = get_last_checkpoint()
return {"loaded": last is not None, "model": last}
@router.post("/models/switch")
async def switch_model(request: SwitchModelRequest) -> dict[str, Any]:
"""Switch to a different model (saves preference, actual load happens on generation)."""
old_model = get_last_checkpoint()
save_last_checkpoint(request.model)
return {"ok": True, "old_model": old_model, "new_model": request.model}
@router.get("/models/status")
async def get_status() -> dict[str, Any]:
"""Get ComfyUI server status."""
try:
resp = httpx.get(f"{COMFY_URL}/system_stats", timeout=5)
resp.raise_for_status()
stats = resp.json()
device = stats.get("devices", [{}])[0]
return {
"service": "comfyui",
"active": True,
"status": "running",
"current_model": get_last_checkpoint(),
"host": "junkpile",
"port": "8188",
"version": stats.get("system", {}).get("comfyui_version"),
"gpu": device.get("name"),
"vram_total": device.get("vram_total"),
"vram_free": device.get("vram_free"),
}
except httpx.HTTPError:
return {
"service": "comfyui",
"active": False,
"status": "offline",
"current_model": None,
"host": "junkpile",
"port": "8188",
}
@router.get("/models/loras")
async def list_loras() -> dict[str, Any]:
"""List available LoRAs."""
try:
client = get_comfy_client()
loras = client.get_loras()
lora_list = []
for lora in loras:
lower = lora.lower()
category = "large" if "xl" in lower or "pony" in lower else "sd15"
lora_list.append(
LoRAInfo(
name=lora.replace(".safetensors", ""),
path=lora,
filename=lora,
category=category,
)
)
return {"loras": [lo.model_dump() for lo in lora_list], "total": len(lora_list)}
except httpx.HTTPError as e:
logger.exception("Failed to connect to ComfyUI")
raise HTTPException(status_code=503, detail=f"ComfyUI not available: {e}") from e
@router.post("/generate")
async def generate_image(request: GenerateRequest) -> dict[str, Any]:
"""Generate an image using ComfyUI."""
try:
client = get_comfy_client()
# Use last checkpoint or first available
checkpoint = get_last_checkpoint()
if not checkpoint:
checkpoints = client.get_checkpoints()
if not checkpoints:
raise HTTPException(status_code=400, detail="No checkpoints available")
checkpoint = checkpoints[0]
save_last_checkpoint(checkpoint)
# Generate random seed if not specified
seed = request.seed if request.seed >= 0 else random.randint(0, 2**32 - 1)
# Build generation params
gen_kwargs: dict[str, Any] = {
"prompt": request.prompt,
"negative_prompt": request.negative_prompt,
"checkpoint": checkpoint,
"width": request.width,
"height": request.height,
"steps": request.steps,
"cfg": request.cfg_scale,
"seed": seed,
"sampler": request.sampler,
"auto_restart": False, # Don't restart container from web UI
}
# Add LoRA if specified
if request.lora:
gen_kwargs["lora"] = request.lora.path
gen_kwargs["lora_strength"] = request.lora.multiplier
result = client.generate(**gen_kwargs)
# Get image data
images = []
for img_info in result.get("images", []):
img_data = client.get_image(
img_info["filename"],
img_info.get("subfolder", ""),
img_info.get("type", "output"),
)
images.append(
GeneratedImage(
id=img_info["filename"],
data=base64.b64encode(img_data).decode(),
seed=result.get("seed", seed),
)
)
return {"images": [img.model_dump() for img in images]}
except httpx.HTTPError as e:
logger.exception("Failed to connect to ComfyUI")
raise HTTPException(status_code=503, detail=f"ComfyUI not available: {e}") from e
except TimeoutError as e:
logger.exception("Generation timed out")
raise HTTPException(status_code=504, detail=str(e)) from e
except Exception as e:
logger.exception("Generation failed")
raise HTTPException(status_code=500, detail=str(e)) from e
return router
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tensors</title>
<script type="module" crossorigin src="/assets/index-DEHUU-Zz.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ljwp9hgM.css">
<script type="module" crossorigin src="/assets/index-Wi7i1Rks.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BEOoAFTp.css">
</head>
<body>
<div id="app"></div>
@@ -74,7 +74,7 @@ async function generate() {
// Build final prompt with quality tags
const finalPrompt = `${store.defaultQualityTags}, ${currentPrompt}`
// Get LoRA config if selected (sd-server expects LoRA as separate param with filename, not in prompt)
// Get LoRA config if selected
const selectedLoraModel = store.selectedLora ? store.loras.find(l => l.path === store.selectedLora) : null
const loraConfig = selectedLoraModel ? { path: selectedLoraModel.filename, multiplier: store.loraWeight } : undefined
@@ -124,7 +124,7 @@ async function generate() {
<v-card class="pa-6 text-center" min-width="300">
<v-progress-circular indeterminate color="primary" size="48" class="mb-4" />
<div class="text-h6">{{ store.switchMessage || 'Switching model...' }}</div>
<div class="text-caption text-grey mt-2">sd-server is restarting</div>
<div class="text-caption text-grey mt-2">Model will load on next generation</div>
</v-card>
</v-overlay>
+5 -23
View File
@@ -163,29 +163,11 @@ export const useAppStore = defineStore('app', () => {
try {
const result = await api.switchModel(modelPath)
switchMessage.value = 'Restarting sd-server...'
// Poll for server to come back online (up to 60 seconds)
let attempts = 0
const maxAttempts = 30
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 2000))
try {
const status = await api.getServerStatus()
if (status.active && status.current_model?.includes(modelPath.split('/').pop() || '')) {
activeModel.value = result.new_model
selectedModel.value = result.new_model
switchMessage.value = 'Model switched successfully'
setTimeout(() => { switchMessage.value = null }, 3000)
return
}
} catch {
// Server still restarting, continue polling
}
attempts++
switchMessage.value = `Waiting for sd-server... (${attempts}/${maxAttempts})`
}
throw new Error('Timeout waiting for sd-server to restart')
// ComfyUI loads models on-demand, no restart needed
activeModel.value = result.new_model
selectedModel.value = modelPath
switchMessage.value = 'Model selected'
setTimeout(() => { switchMessage.value = null }, 2000)
} catch (error: any) {
console.error('Failed to switch model:', error)
switchError.value = error.message || 'Failed to switch model'