radames commited on
Commit
94890d6
1 Parent(s): 8b069db
Files changed (2) hide show
  1. app-txt2imglora.py +258 -0
  2. static/txt2imglora.html +310 -0
app-txt2imglora.py ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import traceback
5
+ from pydantic import BaseModel
6
+
7
+ from fastapi import FastAPI, WebSocket, HTTPException, WebSocketDisconnect
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import (
10
+ StreamingResponse,
11
+ JSONResponse,
12
+ HTMLResponse,
13
+ FileResponse,
14
+ )
15
+
16
+ from diffusers import DiffusionPipeline, LCMScheduler, AutoencoderTiny
17
+ from compel import Compel, ReturnedEmbeddingsType
18
+ import torch
19
+
20
+ try:
21
+ import intel_extension_for_pytorch as ipex
22
+ except:
23
+ pass
24
+ from PIL import Image
25
+ import numpy as np
26
+ import gradio as gr
27
+ import io
28
+ import uuid
29
+ import os
30
+ import time
31
+ import psutil
32
+
33
+
34
+ MAX_QUEUE_SIZE = int(os.environ.get("MAX_QUEUE_SIZE", 0))
35
+ TIMEOUT = float(os.environ.get("TIMEOUT", 0))
36
+ SAFETY_CHECKER = os.environ.get("SAFETY_CHECKER", None)
37
+ TORCH_COMPILE = os.environ.get("TORCH_COMPILE", None)
38
+
39
+ WIDTH = 768
40
+ HEIGHT = 768
41
+ # disable tiny autoencoder for better quality speed tradeoff
42
+ USE_TINY_AUTOENCODER = False
43
+
44
+ # check if MPS is available OSX only M1/M2/M3 chips
45
+ mps_available = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
46
+ xpu_available = hasattr(torch, "xpu") and torch.xpu.is_available()
47
+ device = torch.device(
48
+ "cuda" if torch.cuda.is_available() else "xpu" if xpu_available else "cpu"
49
+ )
50
+ torch_device = device
51
+ # change to torch.float16 to save GPU memory
52
+ torch_dtype = torch.float16
53
+
54
+ print(f"TIMEOUT: {TIMEOUT}")
55
+ print(f"SAFETY_CHECKER: {SAFETY_CHECKER}")
56
+ print(f"MAX_QUEUE_SIZE: {MAX_QUEUE_SIZE}")
57
+ print(f"device: {device}")
58
+
59
+ if mps_available:
60
+ device = torch.device("mps")
61
+ torch_device = "cpu"
62
+ torch_dtype = torch.float32
63
+
64
+ model_id = "stabilityai/stable-diffusion-xl-base-1.0"
65
+
66
+ if SAFETY_CHECKER == "True":
67
+ pipe = DiffusionPipeline.from_pretrained(model_id)
68
+ else:
69
+ pipe = DiffusionPipeline.from_pretrained(model_id, safety_checker=None)
70
+
71
+ if USE_TINY_AUTOENCODER:
72
+ pipe.vae = AutoencoderTiny.from_pretrained(
73
+ "madebyollin/taesd", torch_dtype=torch_dtype, use_safetensors=True
74
+ )
75
+ pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)
76
+ pipe.set_progress_bar_config(disable=True)
77
+ pipe.to(device=torch_device, dtype=torch_dtype).to(device)
78
+ pipe.unet.to(memory_format=torch.channels_last)
79
+
80
+ # check if computer has less than 64GB of RAM using sys or os
81
+ if psutil.virtual_memory().total < 64 * 1024**3:
82
+ pipe.enable_attention_slicing()
83
+
84
+ if TORCH_COMPILE:
85
+ pipe.unet = torch.compile(pipe.unet, mode="reduce-overhead", fullgraph=True)
86
+ pipe.vae = torch.compile(pipe.vae, mode="reduce-overhead", fullgraph=True)
87
+
88
+ pipe(prompt="warmup", num_inference_steps=1, guidance_scale=8.0)
89
+
90
+ # Load LCM LoRA
91
+ pipe.load_lora_weights("lcm-sd/lcm-sdxl-lora", weight_name="lcm_sdxl_lora.safetensors", adapter_name="lcm")
92
+
93
+ compel_proc = Compel(
94
+ tokenizer=[pipe.tokenizer, pipe.tokenizer_2],
95
+ text_encoder=[pipe.text_encoder, pipe.text_encoder_2],
96
+ returned_embeddings_type=ReturnedEmbeddingsType.PENULTIMATE_HIDDEN_STATES_NON_NORMALIZED,
97
+ requires_pooled=[False, True],
98
+ )
99
+ user_queue_map = {}
100
+
101
+
102
+ class InputParams(BaseModel):
103
+ seed: int = 2159232
104
+ prompt: str
105
+ guidance_scale: float = 0.5
106
+ strength: float = 0.5
107
+ steps: int = 4
108
+ lcm_steps: int = 50
109
+ width: int = WIDTH
110
+ height: int = HEIGHT
111
+
112
+
113
+ def predict(params: InputParams):
114
+ generator = torch.manual_seed(params.seed)
115
+ prompt_embeds, pooled_prompt_embeds = compel_proc(params.prompt)
116
+ results = pipe(
117
+ prompt_embeds=prompt_embeds,
118
+ pooled_prompt_embeds=pooled_prompt_embeds,
119
+ generator=generator,
120
+ num_inference_steps=params.steps,
121
+ guidance_scale=params.guidance_scale,
122
+ width=params.width,
123
+ height=params.height,
124
+ # original_inference_steps=params.lcm_steps,
125
+ output_type="pil",
126
+ )
127
+ nsfw_content_detected = (
128
+ results.nsfw_content_detected[0]
129
+ if "nsfw_content_detected" in results
130
+ else False
131
+ )
132
+ if nsfw_content_detected:
133
+ return None
134
+ return results.images[0]
135
+
136
+
137
+ app = FastAPI()
138
+ app.add_middleware(
139
+ CORSMiddleware,
140
+ allow_origins=["*"],
141
+ allow_credentials=True,
142
+ allow_methods=["*"],
143
+ allow_headers=["*"],
144
+ )
145
+
146
+
147
+ @app.websocket("/ws")
148
+ async def websocket_endpoint(websocket: WebSocket):
149
+ await websocket.accept()
150
+ if MAX_QUEUE_SIZE > 0 and len(user_queue_map) >= MAX_QUEUE_SIZE:
151
+ print("Server is full")
152
+ await websocket.send_json({"status": "error", "message": "Server is full"})
153
+ await websocket.close()
154
+ return
155
+
156
+ try:
157
+ uid = str(uuid.uuid4())
158
+ print(f"New user connected: {uid}")
159
+ await websocket.send_json(
160
+ {"status": "success", "message": "Connected", "userId": uid}
161
+ )
162
+ user_queue_map[uid] = {
163
+ "queue": asyncio.Queue(),
164
+ }
165
+ await websocket.send_json(
166
+ {"status": "start", "message": "Start Streaming", "userId": uid}
167
+ )
168
+ await handle_websocket_data(websocket, uid)
169
+ except WebSocketDisconnect as e:
170
+ logging.error(f"WebSocket Error: {e}, {uid}")
171
+ traceback.print_exc()
172
+ finally:
173
+ print(f"User disconnected: {uid}")
174
+ queue_value = user_queue_map.pop(uid, None)
175
+ queue = queue_value.get("queue", None)
176
+ if queue:
177
+ while not queue.empty():
178
+ try:
179
+ queue.get_nowait()
180
+ except asyncio.QueueEmpty:
181
+ continue
182
+
183
+
184
+ @app.get("/queue_size")
185
+ async def get_queue_size():
186
+ queue_size = len(user_queue_map)
187
+ return JSONResponse({"queue_size": queue_size})
188
+
189
+
190
+ @app.get("/stream/{user_id}")
191
+ async def stream(user_id: uuid.UUID):
192
+ uid = str(user_id)
193
+ try:
194
+ user_queue = user_queue_map[uid]
195
+ queue = user_queue["queue"]
196
+
197
+ async def generate():
198
+ while True:
199
+ params = await queue.get()
200
+ if params is None:
201
+ continue
202
+
203
+ image = predict(params)
204
+ if image is None:
205
+ continue
206
+ frame_data = io.BytesIO()
207
+ image.save(frame_data, format="JPEG")
208
+ frame_data = frame_data.getvalue()
209
+ if frame_data is not None and len(frame_data) > 0:
210
+ yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame_data + b"\r\n"
211
+
212
+ await asyncio.sleep(1.0 / 120.0)
213
+
214
+ return StreamingResponse(
215
+ generate(), media_type="multipart/x-mixed-replace;boundary=frame"
216
+ )
217
+ except Exception as e:
218
+ logging.error(f"Streaming Error: {e}, {user_queue_map}")
219
+ traceback.print_exc()
220
+ return HTTPException(status_code=404, detail="User not found")
221
+
222
+
223
+ async def handle_websocket_data(websocket: WebSocket, user_id: uuid.UUID):
224
+ uid = str(user_id)
225
+ user_queue = user_queue_map[uid]
226
+ queue = user_queue["queue"]
227
+ if not queue:
228
+ return HTTPException(status_code=404, detail="User not found")
229
+ last_time = time.time()
230
+ try:
231
+ while True:
232
+ params = await websocket.receive_json()
233
+ params = InputParams(**params)
234
+ while not queue.empty():
235
+ try:
236
+ queue.get_nowait()
237
+ except asyncio.QueueEmpty:
238
+ continue
239
+ await queue.put(params)
240
+ if TIMEOUT > 0 and time.time() - last_time > TIMEOUT:
241
+ await websocket.send_json(
242
+ {
243
+ "status": "timeout",
244
+ "message": "Your session has ended",
245
+ "userId": uid,
246
+ }
247
+ )
248
+ await websocket.close()
249
+ return
250
+
251
+ except Exception as e:
252
+ logging.error(f"Error: {e}")
253
+ traceback.print_exc()
254
+
255
+
256
+ @app.get("/", response_class=HTMLResponse)
257
+ async def root():
258
+ return FileResponse("./static/txt2imglora.html")
static/txt2imglora.html ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <title>Real-Time Latent Consistency Model</title>
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <script
9
+ src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.1/iframeResizer.contentWindow.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/piexif.min.js"></script>
11
+ <script src="https://cdn.tailwindcss.com"></script>
12
+ <style type="text/tailwindcss">
13
+ .button {
14
+ @apply bg-gray-700 hover:bg-gray-800 text-white font-normal p-2 rounded disabled:bg-gray-300 dark:disabled:bg-gray-700 disabled:cursor-not-allowed dark:disabled:text-black
15
+ }
16
+ </style>
17
+ <script type="module">
18
+ const getValue = (id) => {
19
+ const el = document.querySelector(`${id}`)
20
+ if (el.type === "checkbox")
21
+ return el.checked;
22
+ return el.value;
23
+ }
24
+ const startBtn = document.querySelector("#start");
25
+ const stopBtn = document.querySelector("#stop");
26
+ const videoEl = document.querySelector("#webcam");
27
+ const imageEl = document.querySelector("#player");
28
+ const queueSizeEl = document.querySelector("#queue_size");
29
+ const errorEl = document.querySelector("#error");
30
+ const snapBtn = document.querySelector("#snap");
31
+ const paramsEl = document.querySelector("#params");
32
+ const promptEl = document.querySelector("#prompt");
33
+ paramsEl.addEventListener("submit", (e) => e.preventDefault());
34
+ function LCMLive(promptEl, paramsEl, liveImage) {
35
+ let websocket;
36
+
37
+ async function start() {
38
+ return new Promise((resolve, reject) => {
39
+ const websocketURL = `${window.location.protocol === "https:" ? "wss" : "ws"
40
+ }:${window.location.host}/ws`;
41
+
42
+ const socket = new WebSocket(websocketURL);
43
+ socket.onopen = () => {
44
+ console.log("Connected to websocket");
45
+ };
46
+ socket.onclose = () => {
47
+ console.log("Disconnected from websocket");
48
+ stop();
49
+ resolve({ "status": "disconnected" });
50
+ };
51
+ socket.onerror = (err) => {
52
+ console.error(err);
53
+ reject(err);
54
+ };
55
+ socket.onmessage = (event) => {
56
+ const data = JSON.parse(event.data);
57
+ switch (data.status) {
58
+ case "success":
59
+ break;
60
+ case "start":
61
+ const userId = data.userId;
62
+ initPromptStream(userId);
63
+ break;
64
+ case "timeout":
65
+ stop();
66
+ resolve({ "status": "timeout" });
67
+ case "error":
68
+ stop();
69
+ reject(data.message);
70
+ }
71
+ };
72
+ websocket = socket;
73
+ })
74
+ }
75
+
76
+ async function promptUpdateStream(e) {
77
+ const dimension = getValue("input[name=dimension]:checked");
78
+ const [WIDTH, HEIGHT] = JSON.parse(dimension);
79
+ websocket.send(JSON.stringify({
80
+ "seed": getValue("#seed"),
81
+ "prompt": getValue("#prompt"),
82
+ "guidance_scale": getValue("#guidance-scale"),
83
+ "steps": getValue("#steps"),
84
+ "lcm_steps": getValue("#lcm_steps"),
85
+ "width": WIDTH,
86
+ "height": HEIGHT,
87
+ }));
88
+ }
89
+ function debouceInput(fn, delay) {
90
+ let timer;
91
+ return function (...args) {
92
+ clearTimeout(timer);
93
+ timer = setTimeout(() => {
94
+ fn(...args);
95
+ }, delay);
96
+ }
97
+ }
98
+ const debouncedInput = debouceInput(promptUpdateStream, 200);
99
+ function initPromptStream(userId) {
100
+ liveImage.src = `/stream/${userId}`;
101
+ paramsEl.addEventListener("change", debouncedInput);
102
+ promptEl.addEventListener("input", debouncedInput);
103
+ }
104
+
105
+ async function stop() {
106
+ websocket.close();
107
+ paramsEl.removeEventListener("change", debouncedInput);
108
+ promptEl.removeEventListener("input", debouncedInput);
109
+ }
110
+ return {
111
+ start,
112
+ stop
113
+ }
114
+ }
115
+ function toggleMessage(type) {
116
+ errorEl.hidden = false;
117
+ errorEl.scrollIntoView();
118
+ switch (type) {
119
+ case "error":
120
+ errorEl.innerText = "To many users are using the same GPU, please try again later.";
121
+ errorEl.classList.toggle("bg-red-300", "text-red-900");
122
+ break;
123
+ case "success":
124
+ errorEl.innerText = "Your session has ended, please start a new one.";
125
+ errorEl.classList.toggle("bg-green-300", "text-green-900");
126
+ break;
127
+ }
128
+ setTimeout(() => {
129
+ errorEl.hidden = true;
130
+ }, 2000);
131
+ }
132
+ function snapImage() {
133
+ try {
134
+ const zeroth = {};
135
+ const exif = {};
136
+ const gps = {};
137
+ zeroth[piexif.ImageIFD.Make] = "LCM Text-to-Image";
138
+ zeroth[piexif.ImageIFD.ImageDescription] = `prompt: ${getValue("#prompt")} | seed: ${getValue("#seed")} | guidance_scale: ${getValue("#guidance-scale")} | lcm_steps: ${getValue("#lcm_steps")} | steps: ${getValue("#steps")}`;
139
+ zeroth[piexif.ImageIFD.Software] = "https://github.com/radames/Real-Time-Latent-Consistency-Model";
140
+
141
+ exif[piexif.ExifIFD.DateTimeOriginal] = new Date().toISOString();
142
+
143
+ const exifObj = { "0th": zeroth, "Exif": exif, "GPS": gps };
144
+ const exifBytes = piexif.dump(exifObj);
145
+
146
+ const canvas = document.createElement("canvas");
147
+ canvas.width = imageEl.naturalWidth;
148
+ canvas.height = imageEl.naturalHeight;
149
+ const ctx = canvas.getContext("2d");
150
+ ctx.drawImage(imageEl, 0, 0);
151
+ const dataURL = canvas.toDataURL("image/jpeg");
152
+ const withExif = piexif.insert(exifBytes, dataURL);
153
+
154
+ const a = document.createElement("a");
155
+ a.href = withExif;
156
+ a.download = `lcm_txt_2_img${Date.now()}.png`;
157
+ a.click();
158
+ } catch (err) {
159
+ console.log(err);
160
+ }
161
+ }
162
+
163
+
164
+ const lcmLive = LCMLive(promptEl, paramsEl, imageEl);
165
+ startBtn.addEventListener("click", async () => {
166
+ try {
167
+ startBtn.disabled = true;
168
+ snapBtn.disabled = false;
169
+ const res = await lcmLive.start();
170
+ startBtn.disabled = false;
171
+ if (res.status === "timeout")
172
+ toggleMessage("success")
173
+ } catch (err) {
174
+ console.log(err);
175
+ toggleMessage("error")
176
+ startBtn.disabled = false;
177
+ }
178
+ });
179
+ stopBtn.addEventListener("click", () => {
180
+ lcmLive.stop();
181
+ });
182
+ window.addEventListener("beforeunload", () => {
183
+ lcmLive.stop();
184
+ });
185
+ snapBtn.addEventListener("click", snapImage);
186
+ setInterval(() =>
187
+ fetch("/queue_size")
188
+ .then((res) => res.json())
189
+ .then((data) => {
190
+ queueSizeEl.innerText = data.queue_size;
191
+ })
192
+ .catch((err) => {
193
+ console.log(err);
194
+ })
195
+ , 5000);
196
+ </script>
197
+ </head>
198
+
199
+ <body class="text-black dark:bg-gray-900 dark:text-white">
200
+ <div class="fixed right-2 top-2 p-4 font-bold text-sm rounded-lg max-w-xs text-center" id="error">
201
+ </div>  
202
+ <main class="container mx-auto px-4 py-4 max-w-4xl flex flex-col gap-4">
203
+ <article class="text-center max-w-xl mx-auto">
204
+ <h1 class="text-3xl font-bold">Real-Time Latent Consistency Model</h1>
205
+ <h2 class="text-2xl font-bold mb-4">Text to Image</h2>
206
+ <p class="text-sm">
207
+ This demo showcases
208
+ <a href="https://huggingface.co/SimianLuo/LCM_Dreamshaper_v7" target="_blank"
209
+ class="text-blue-500 underline hover:no-underline">LCM</a> Text to Image model
210
+ using
211
+ <a href="https://github.com/huggingface/diffusers/tree/main/examples/community#latent-consistency-pipeline"
212
+ target="_blank" class="text-blue-500 underline hover:no-underline">Diffusers</a> with a MJPEG
213
+ stream server.
214
+ </p>
215
+ <p class="text-sm">
216
+ There are <span id="queue_size" class="font-bold">0</span> user(s) sharing the same GPU, affecting
217
+ real-time performance. Maximum queue size is 10. <a
218
+ href="https://huggingface.co/spaces/radames/Real-Time-Latent-Consistency-Model?duplicate=true"
219
+ target="_blank" class="text-blue-500 underline hover:no-underline">Duplicate</a> and run it on your
220
+ own GPU.
221
+ </p>
222
+ </article>
223
+ <div>
224
+ <h2 class="font-medium">Prompt</h2>
225
+ <p class="text-sm text-gray-500 dark:text-gray-400">
226
+ Start your session and type your prompt here, accepts
227
+ <a href="https://github.com/damian0815/compel/blob/main/doc/syntax.md" target="_blank"
228
+ class="text-blue-500 underline hover:no-underline">Compel</a> syntax.
229
+ </p>
230
+ <div class="flex text-normal px-1 py-1 border border-gray-700 rounded-md items-center">
231
+ <textarea type="text" id="prompt" class="font-light w-full px-3 py-2 mx-1 outline-none dark:text-black"
232
+ title=" Start your session and type your prompt here, you can see the result in real-time."
233
+ placeholder="Add your prompt here...">Portrait of The Terminator with , glare pose, detailed, intricate, full of colour, cinematic lighting, trending on artstation, 8k, hyperrealistic, focused, extreme details, unreal engine 5, cinematic, masterpiece</textarea>
234
+ </div>
235
+
236
+ </div>
237
+ <div class="">
238
+ <details>
239
+ <summary class="font-medium cursor-pointer">Advanced Options</summary>
240
+ <form class="grid grid-cols-3 items-center gap-3 py-3" id="params" action="">
241
+ <label class="text-sm font-medium" for="dimension">Image Dimensions</label>
242
+ <div class="col-span-2 flex gap-2">
243
+ <div class="flex gap-1">
244
+ <input type="radio" id="dimension512" name="dimension" value="[512,512]"
245
+ class="cursor-pointer">
246
+ <label for="dimension512" class="text-sm cursor-pointer">512x512</label>
247
+ </div>
248
+ <div class="flex gap-1">
249
+ <input type="radio" id="dimension768" name="dimension" value="[768,768]"
250
+ lass="cursor-pointer">
251
+ <label for="dimension768" class="text-sm cursor-pointer">768x768</label>
252
+ </div>
253
+ <div class="flex gap-1">
254
+ <input type="radio" id="dimension1024" name="dimension" value="[1024,1024]" checked
255
+ class="cursor-pointer">
256
+ <label for="dimension1024" class="text-sm cursor-pointer">1024x1024</label>
257
+ </div>
258
+ </div>
259
+ <!-- -->
260
+ <label class="text-sm font-medium " for="steps">Inference Steps
261
+ </label>
262
+ <input type="range" id="steps" name="steps" min="1" max="20" value="4"
263
+ oninput="this.nextElementSibling.value = Number(this.value)">
264
+ <output class="text-xs w-[50px] text-center font-light px-1 py-1 border border-gray-700 rounded-md">
265
+ 4</output>
266
+ <!-- -->
267
+ <label class="text-sm font-medium" for="lcm_steps">LCM Inference Steps
268
+ </label>
269
+ <input type="range" id="lcm_steps" name="lcm_steps" min="2" max="60" value="50"
270
+ oninput="this.nextElementSibling.value = Number(this.value)">
271
+ <output class="text-xs w-[50px] text-center font-light px-1 py-1 border border-gray-700 rounded-md">
272
+ 50</output>
273
+ <!-- -->
274
+ <label class="text-sm font-medium" for="guidance-scale">Guidance Scale
275
+ </label>
276
+ <input type="range" id="guidance-scale" name="guidance-scale" min="0" max="5" step="0.0001"
277
+ value="0.8" oninput="this.nextElementSibling.value = Number(this.value).toFixed(2)">
278
+ <output class="text-xs w-[50px] text-center font-light px-1 py-1 border border-gray-700 rounded-md">
279
+ 8.0</output>
280
+ <!-- -->
281
+ <label class="text-sm font-medium" for="seed">Seed</label>
282
+ <input type="number" id="seed" name="seed" value="299792458"
283
+ class="font-light border border-gray-700 text-right rounded-md p-2 dark:text-black">
284
+ <button class="button"
285
+ onclick="document.querySelector('#seed').value = Math.floor(Math.random() * 1000000000); document.querySelector('#params').dispatchEvent(new Event('change'))">
286
+ Rand
287
+ </button>
288
+ <!-- -->
289
+ </form>
290
+ </details>
291
+ </div>
292
+ <div class="flex gap-3">
293
+ <button id="start" class="button">
294
+ Start
295
+ </button>
296
+ <button id="stop" class="button">
297
+ Stop
298
+ </button>
299
+ <button id="snap" disabled class="button ml-auto">
300
+ Snapshot
301
+ </button>
302
+ </div>
303
+ <div class="relative rounded-lg border border-slate-300 overflow-hidden">
304
+ <img id="player" class="w-full aspect-square rounded-lg"
305
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
306
+ </div>
307
+ </main>
308
+ </body>
309
+
310
+ </html>