mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
272 lines
9.5 KiB
Python
272 lines
9.5 KiB
Python
import asyncio
|
|
import base64
|
|
import json
|
|
import logging
|
|
import pathlib
|
|
import time
|
|
from io import BytesIO
|
|
from typing import Dict, Any, Optional, Tuple, Callable, List
|
|
from dataclasses import dataclass
|
|
from dataclasses_json import dataclass_json
|
|
from enum import Enum, auto
|
|
import base64
|
|
from PIL import Image
|
|
import io
|
|
import torch
|
|
from diffusers import AutoPipelineForImage2Image, LCMScheduler
|
|
from diffusers.utils import make_image_grid, load_image
|
|
from sd_embed.embedding_funcs import get_weighted_text_embeddings_sdxl
|
|
import torch
|
|
import websockets
|
|
from PIL import Image
|
|
from diffusers import (
|
|
StableDiffusionXLPipeline,
|
|
StableDiffusionXLImg2ImgPipeline,
|
|
AutoPipelineForImage2Image,
|
|
LCMScheduler,
|
|
UNet2DConditionModel,
|
|
)
|
|
import torch
|
|
import torchvision.transforms as transforms
|
|
import gc
|
|
|
|
transform = transforms.Compose([
|
|
transforms.ToTensor(), # This converts PIL Image to tensor and scales to [0, 1]
|
|
])
|
|
|
|
@dataclass_json
|
|
@dataclass
|
|
class ProgressMessage:
|
|
message: str
|
|
progress: float
|
|
type: str = "progress"
|
|
error: Optional[str] = None
|
|
|
|
@dataclass_json
|
|
@dataclass
|
|
class LoadModelRequest:
|
|
model_path: str
|
|
lora_path: Optional[str] = None
|
|
command: str = "load_model"
|
|
|
|
@dataclass_json
|
|
@dataclass
|
|
class GenerateRequest:
|
|
image: str
|
|
prompt: str
|
|
command: str = "generate"
|
|
strength: float = 0.6
|
|
guidance_scale: float = 2.0
|
|
num_inference_steps: int = 4
|
|
height: int = 1024
|
|
width: int = 1024
|
|
lora_strength: float = 1.0
|
|
|
|
@dataclass_json
|
|
@dataclass
|
|
class GenerateResponse:
|
|
image: str
|
|
type: str = "result"
|
|
|
|
def save_image(image: Image.Image, base_path: str = "outputs") -> str:
|
|
"""Save a PIL Image to disk with a timestamp-based filename."""
|
|
try:
|
|
# Create outputs directory if it doesn't exist
|
|
output_dir = pathlib.Path(base_path)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Generate filename with timestamp
|
|
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
|
filename = f"generated-{timestamp}.png"
|
|
filepath = output_dir / filename
|
|
|
|
# Save the image
|
|
print(f"Saving image to: {filepath}")
|
|
image.save(str(filepath), format="PNG")
|
|
print(f"Successfully saved image to: {filepath}")
|
|
return str(filepath)
|
|
except Exception as e:
|
|
print(f"Error saving image: {e}")
|
|
return ""
|
|
|
|
|
|
async def send_progress(websocket, message: str, progress: float = 0.0, error: str = None):
|
|
"""Send progress updates to the client."""
|
|
progress_msg = ProgressMessage(
|
|
message=message,
|
|
progress=progress,
|
|
error=error
|
|
)
|
|
await websocket.send(progress_msg.to_json())
|
|
|
|
async def load_model(model_path: str, lora_path: str = None) -> StableDiffusionXLImg2ImgPipeline:
|
|
"""Load the model with progress reporting."""
|
|
try:
|
|
# Load base SDXL pipeline
|
|
pipe = StableDiffusionXLPipeline.from_single_file(
|
|
model_path,
|
|
torch_dtype=torch.float16,
|
|
use_safetensors=True
|
|
).to("cuda")
|
|
|
|
# Convert to img2img pipeline
|
|
pipe = StableDiffusionXLImg2ImgPipeline(**pipe.components)
|
|
|
|
# Set scheduler
|
|
pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)
|
|
|
|
# Load LCM-LoRA weights if provided
|
|
if lora_path:
|
|
pipe.load_lora_weights(
|
|
"F:/ComfyUI_windows_portable_nvidia/ComfyUI_windows_portable/ComfyUI/models/loras/LCM_LoRA_Weights_SDXL.safetensors",
|
|
adapter_name="lcm"
|
|
)
|
|
pipe.load_lora_weights(lora_path, adapter_name="lora")
|
|
pipe.set_adapters(["lcm", "lora"], adapter_weights=[1.0, 1.0])
|
|
#pipe.set_adapters(["lcm"], adapter_weights=[1.0])
|
|
#pipe.unet = torch.compile(pipe.unet, mode="reduce-overhead", fullgraph=True)
|
|
return pipe
|
|
except Exception as e:
|
|
raise Exception(f"Error loading model: {str(e)}")
|
|
|
|
#https://huggingface.co/blog/simple_sdxl_optimizations
|
|
async def generate_image(
|
|
pipe: StableDiffusionXLImg2ImgPipeline,
|
|
request: GenerateRequest
|
|
) -> str:
|
|
"""Generate image and return base64 string."""
|
|
try:
|
|
# Clear CUDA cache before processing
|
|
torch.cuda.empty_cache()
|
|
|
|
image_stream = io.BytesIO(base64.b64decode(request.image))
|
|
|
|
init_image = Image.open(image_stream).convert('RGB')
|
|
init_image = init_image.resize((request.width, request.height)) # width, height
|
|
init_image = transform(init_image).to('cuda')
|
|
init_image = init_image.unsqueeze(0)
|
|
|
|
(
|
|
prompt_embeds
|
|
, prompt_neg_embeds
|
|
, pooled_prompt_embeds
|
|
, negative_pooled_prompt_embeds
|
|
) = get_weighted_text_embeddings_sdxl(
|
|
pipe,
|
|
prompt = request.prompt,
|
|
neg_prompt = "bad quality, poorly Rendered face, poorly drawn face, poorly facial details, poorly drawn hands, poorly rendered hands, low resolution, Images cut out at the top, left, right, bottom, bad composition, mutated body parts, blurry image, disfigured, oversaturated, bad anatomy, deformed body features"
|
|
)
|
|
|
|
# Generate image
|
|
with torch.inference_mode():
|
|
output_image = pipe(
|
|
prompt_embeds = prompt_embeds,
|
|
negative_prompt_embeds = prompt_neg_embeds,
|
|
pooled_prompt_embeds = pooled_prompt_embeds,
|
|
negative_pooled_prompt_embeds = negative_pooled_prompt_embeds,
|
|
image=init_image,
|
|
num_inference_steps=request.num_inference_steps,
|
|
guidance_scale=request.guidance_scale,
|
|
strength=request.strength,
|
|
generator=torch.manual_seed(0),
|
|
height=request.height, width=request.width,
|
|
).images[0]
|
|
|
|
# Clean up CUDA tensors
|
|
del init_image, prompt_embeds, prompt_neg_embeds,pooled_prompt_embeds, negative_pooled_prompt_embeds
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
# Convert to base64
|
|
buffered = BytesIO()
|
|
output_image.save(buffered, format="PNG")
|
|
buffered.seek(0)
|
|
img_str = base64.b64encode(buffered.getvalue()).decode()
|
|
|
|
return f"data:image/png;base64,{img_str}"
|
|
except Exception as e:
|
|
raise Exception(f"Error generating image: {str(e)} at line {e.__traceback__.tb_lineno}")
|
|
|
|
async def handle_client(websocket):
|
|
"""Handle WebSocket client connection."""
|
|
pipe = None
|
|
try:
|
|
async for message in websocket:
|
|
try:
|
|
# Clear memory before processing new request
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
data = json.loads(message)
|
|
command = data.get("command")
|
|
|
|
if command == "load_model":
|
|
await send_progress(websocket, "Loading model...", 0.0)
|
|
request = LoadModelRequest.from_json(message)
|
|
|
|
if pipe is not None:
|
|
# Clean up existing model
|
|
del pipe
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
pipe = await load_model(request.model_path, request.lora_path)
|
|
await send_progress(websocket, "Model loaded successfully", 1.0)
|
|
|
|
elif command == "generate":
|
|
if not pipe:
|
|
raise Exception("Model not loaded. Please load model first.")
|
|
|
|
await send_progress(websocket, "Generating image...", 0.0)
|
|
request = GenerateRequest.from_json(message)
|
|
|
|
# Use a timeout for generation to prevent hanging
|
|
try:
|
|
result = await asyncio.wait_for(
|
|
generate_image(pipe=pipe, request=request),
|
|
timeout=10.0 # 30 second timeout
|
|
)
|
|
response = GenerateResponse(image=result)
|
|
await websocket.send(response.to_json())
|
|
except asyncio.TimeoutError:
|
|
# Force cleanup if generation times out
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
raise Exception("Generation timed out")
|
|
|
|
else:
|
|
raise Exception(f"Unknown command: {command}")
|
|
|
|
except Exception as e:
|
|
await send_progress(websocket, "Error processing request", 0.0, str(e))
|
|
# Clean up after error
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
except websockets.exceptions.ConnectionClosed:
|
|
print("Client connection closed")
|
|
except Exception as e:
|
|
await send_progress(websocket, "Fatal error", 0.0, str(e))
|
|
finally:
|
|
# Clean up resources when client disconnects
|
|
if pipe is not None:
|
|
del pipe
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
async def server():
|
|
"""Start WebSocket server."""
|
|
async with websockets.serve(
|
|
handle_client,
|
|
"localhost",
|
|
8765,
|
|
max_size=None,
|
|
ping_interval=None, # Disable ping/pong to prevent timeouts
|
|
max_queue=10 # Limit pending connections
|
|
):
|
|
print("Server started on ws://localhost:8765")
|
|
await asyncio.Future() # run forever
|
|
|
|
if __name__ == "__main__":
|
|
# test() # Comment out test function
|
|
asyncio.run(server()) |