save code
This commit is contained in:
+67
-10
@@ -4,11 +4,27 @@ import io
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
import requests
|
import requests
|
||||||
from flask import Flask, request, jsonify, send_file
|
from flask import Flask, request, jsonify, send_file
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image, ImageFilter
|
from PIL import Image, ImageFilter
|
||||||
|
|
||||||
|
# 让 PIL 支持 iPhone 的 HEIC/HEIF 照片(浏览器 accept="image/*" 会允许选中它们)。
|
||||||
|
try:
|
||||||
|
from pillow_heif import register_heif_opener
|
||||||
|
register_heif_opener()
|
||||||
|
_HEIF_OK = True
|
||||||
|
except Exception:
|
||||||
|
_HEIF_OK = False
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
)
|
||||||
|
log = logging.getLogger("hair")
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
COMFYUI_URL = "http://127.0.0.1:8188"
|
COMFYUI_URL = "http://127.0.0.1:8188"
|
||||||
|
|
||||||
@@ -18,6 +34,7 @@ _CORS_HEADERS = {
|
|||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||||
"Access-Control-Allow-Headers": "Content-Type",
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
|
"Access-Control-Expose-Headers": "X-Generate-Time",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +60,7 @@ def build_workflow(image_filename, prompt_text, seed=None):
|
|||||||
# Loaders
|
# Loaders
|
||||||
"16": {"class_type": "UNETLoader", "inputs": {
|
"16": {"class_type": "UNETLoader", "inputs": {
|
||||||
"unet_name": "flux2.0/flux-2-klein-9b-fp8.safetensors",
|
"unet_name": "flux2.0/flux-2-klein-9b-fp8.safetensors",
|
||||||
"weight_dtype": "fp8_e4m3fn"}},
|
"weight_dtype": "fp8_e4m3fn_fast"}},
|
||||||
"3": {"class_type": "VAELoader", "inputs": {
|
"3": {"class_type": "VAELoader", "inputs": {
|
||||||
"vae_name": "flux2-vae.safetensors"}},
|
"vae_name": "flux2-vae.safetensors"}},
|
||||||
"61": {"class_type": "CLIPLoader", "inputs": {
|
"61": {"class_type": "CLIPLoader", "inputs": {
|
||||||
@@ -136,7 +153,7 @@ def build_workflow(image_filename, prompt_text, seed=None):
|
|||||||
"1": {"class_type": "BasicScheduler", "inputs": {
|
"1": {"class_type": "BasicScheduler", "inputs": {
|
||||||
"model": ["2", 0],
|
"model": ["2", 0],
|
||||||
"scheduler": "simple",
|
"scheduler": "simple",
|
||||||
"steps": 6,
|
"steps": 4,
|
||||||
"denoise": 1}},
|
"denoise": 1}},
|
||||||
"20": {"class_type": "BasicGuider", "inputs": {
|
"20": {"class_type": "BasicGuider", "inputs": {
|
||||||
"model": ["2", 0],
|
"model": ["2", 0],
|
||||||
@@ -181,20 +198,42 @@ def index():
|
|||||||
|
|
||||||
@app.route("/api/generate", methods=["POST"])
|
@app.route("/api/generate", methods=["POST"])
|
||||||
def generate():
|
def generate():
|
||||||
|
t_start = time.time()
|
||||||
try:
|
try:
|
||||||
|
if "image" not in request.files or "mask" not in request.files:
|
||||||
|
msg = f"缺少上传文件: files={list(request.files.keys())}"
|
||||||
|
log.warning(msg)
|
||||||
|
return jsonify({"error": msg}), 400
|
||||||
image_file = request.files["image"]
|
image_file = request.files["image"]
|
||||||
mask_file = request.files["mask"]
|
mask_file = request.files["mask"]
|
||||||
prompt_text = request.form.get("prompt", "填充遮罩区域的头发,皮肤加一点磨皮")
|
prompt_text = request.form.get("prompt", "填充遮罩区域的头发,皮肤加一点磨皮")
|
||||||
|
log.info(
|
||||||
|
"收到请求: image=%s mask=%s prompt=%r",
|
||||||
|
image_file.filename, mask_file.filename, prompt_text,
|
||||||
|
)
|
||||||
|
|
||||||
# Load original image as RGB
|
# Load original image as RGB
|
||||||
image = Image.open(image_file).convert("RGB")
|
try:
|
||||||
|
image = Image.open(image_file).convert("RGB")
|
||||||
|
except Exception as e:
|
||||||
|
log.error("无法解码人物图片 %s: %s", image_file.filename, e)
|
||||||
|
hint = "" if _HEIF_OK else "(当前不支持 HEIC)"
|
||||||
|
return jsonify({
|
||||||
|
"error": f"无法识别人物图片格式{hint},请改用 JPG/PNG: {e}"
|
||||||
|
}), 400
|
||||||
|
|
||||||
# Load mask and extract mask data from ALL channels (R, G, B, A)
|
# Load mask and extract mask data from ALL channels (R, G, B, A)
|
||||||
# This handles different mask formats:
|
# This handles different mask formats:
|
||||||
# - Red mask (R=255 where drawn): user-provided PNG
|
# - Red mask (R=255 where drawn): user-provided PNG
|
||||||
# - White mask (R=G=B=255 where drawn): frontend canvas
|
# - White mask (R=G=B=255 where drawn): frontend canvas
|
||||||
# - Alpha mask (A=255 where drawn): transparent brush
|
# - Alpha mask (A=255 where drawn): transparent brush
|
||||||
mask_img = Image.open(mask_file).convert("RGBA")
|
try:
|
||||||
|
mask_img = Image.open(mask_file).convert("RGBA")
|
||||||
|
except Exception as e:
|
||||||
|
log.error("无法解码遮罩图片 %s: %s", mask_file.filename, e)
|
||||||
|
return jsonify({
|
||||||
|
"error": f"无法识别遮罩图片格式,请改用 JPG/PNG: {e}"
|
||||||
|
}), 400
|
||||||
mask_arr = np.array(mask_img)
|
mask_arr = np.array(mask_img)
|
||||||
# Use max of all channels: 255 where any color/alpha is drawn, 0 where empty
|
# Use max of all channels: 255 where any color/alpha is drawn, 0 where empty
|
||||||
mask_data = np.max(mask_arr, axis=2) # (H, W) uint8
|
mask_data = np.max(mask_arr, axis=2) # (H, W) uint8
|
||||||
@@ -229,6 +268,7 @@ def generate():
|
|||||||
)
|
)
|
||||||
upload_data = upload_resp.json()
|
upload_data = upload_resp.json()
|
||||||
if "name" not in upload_data:
|
if "name" not in upload_data:
|
||||||
|
log.error("ComfyUI 上传图片失败: %s", upload_data)
|
||||||
return jsonify({"error": f"Upload failed: {upload_data}"}), 500
|
return jsonify({"error": f"Upload failed: {upload_data}"}), 500
|
||||||
image_filename = upload_data["name"]
|
image_filename = upload_data["name"]
|
||||||
|
|
||||||
@@ -241,12 +281,16 @@ def generate():
|
|||||||
)
|
)
|
||||||
prompt_data = prompt_resp.json()
|
prompt_data = prompt_resp.json()
|
||||||
if "error" in prompt_data:
|
if "error" in prompt_data:
|
||||||
|
log.error(
|
||||||
|
"ComfyUI /prompt 校验失败: error=%s node_errors=%s",
|
||||||
|
prompt_data.get("error"), prompt_data.get("node_errors"),
|
||||||
|
)
|
||||||
return jsonify({"error": json.dumps(prompt_data["error"], ensure_ascii=False)}), 500
|
return jsonify({"error": json.dumps(prompt_data["error"], ensure_ascii=False)}), 500
|
||||||
prompt_id = prompt_data["prompt_id"]
|
prompt_id = prompt_data["prompt_id"]
|
||||||
|
|
||||||
# Poll for completion (5 min timeout)
|
# Poll for completion (5 min timeout, 0.1s interval)
|
||||||
for _ in range(150):
|
for _ in range(3000):
|
||||||
time.sleep(2)
|
time.sleep(0.1)
|
||||||
history_resp = requests.get(
|
history_resp = requests.get(
|
||||||
f"{COMFYUI_URL}/history/{prompt_id}", timeout=10
|
f"{COMFYUI_URL}/history/{prompt_id}", timeout=10
|
||||||
)
|
)
|
||||||
@@ -254,7 +298,14 @@ def generate():
|
|||||||
if prompt_id in history_data:
|
if prompt_id in history_data:
|
||||||
status = history_data[prompt_id].get("status", {})
|
status = history_data[prompt_id].get("status", {})
|
||||||
if status.get("status_str") == "error":
|
if status.get("status_str") == "error":
|
||||||
return jsonify({"error": "Workflow execution failed"}), 500
|
log.error(
|
||||||
|
"ComfyUI 工作流执行失败: %s",
|
||||||
|
json.dumps(status, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
return jsonify({
|
||||||
|
"error": "Workflow execution failed",
|
||||||
|
"detail": status.get("messages", status),
|
||||||
|
}), 500
|
||||||
outputs = history_data[prompt_id].get("outputs", {})
|
outputs = history_data[prompt_id].get("outputs", {})
|
||||||
if "17" in outputs: # SaveImage node
|
if "17" in outputs: # SaveImage node
|
||||||
image_info = outputs["17"]["images"][0]
|
image_info = outputs["17"]["images"][0]
|
||||||
@@ -266,16 +317,22 @@ def generate():
|
|||||||
params={"filename": filename, "subfolder": subfolder, "type": img_type},
|
params={"filename": filename, "subfolder": subfolder, "type": img_type},
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
return send_file(
|
elapsed = time.time() - t_start
|
||||||
|
log.info("重绘完成,服务端耗时 %.2fs", elapsed)
|
||||||
|
resp = send_file(
|
||||||
io.BytesIO(view_resp.content), mimetype="image/png"
|
io.BytesIO(view_resp.content), mimetype="image/png"
|
||||||
)
|
)
|
||||||
|
resp.headers["X-Generate-Time"] = f"{elapsed:.2f}"
|
||||||
|
return resp
|
||||||
|
|
||||||
return jsonify({"error": "Timeout: workflow did not complete in 5 minutes"}), 500
|
return jsonify({"error": "Timeout: workflow did not complete in 5 minutes"}), 500
|
||||||
|
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
|
log.error("无法连接 ComfyUI @ %s", COMFYUI_URL)
|
||||||
return jsonify({"error": "Cannot connect to ComfyUI at " + COMFYUI_URL + ". Is it running?"}), 503
|
return jsonify({"error": "Cannot connect to ComfyUI at " + COMFYUI_URL + ". Is it running?"}), 503
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
log.error("生成失败,未捕获异常:\n%s", traceback.format_exc())
|
||||||
|
return jsonify({"error": f"{type(e).__name__}: {e}"}), 500
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Benchmark ComfyUI hair-inpaint workflow across model / dtype / steps."""
|
||||||
|
import io, time, sys
|
||||||
|
import requests
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageFilter
|
||||||
|
import app as A
|
||||||
|
|
||||||
|
COMFY = "http://127.0.0.1:8188"
|
||||||
|
|
||||||
|
|
||||||
|
def prep_and_upload():
|
||||||
|
image = Image.open("用来重绘.jpg").convert("RGB")
|
||||||
|
mask_img = Image.open("用来重绘.png").convert("RGBA")
|
||||||
|
mask_data = np.max(np.array(mask_img), axis=2)
|
||||||
|
m = Image.fromarray(mask_data, mode="L")
|
||||||
|
if m.size != image.size:
|
||||||
|
m = m.resize(image.size, Image.LANCZOS)
|
||||||
|
m = m.filter(ImageFilter.GaussianBlur(radius=4))
|
||||||
|
alpha = Image.eval(m, lambda x: 255 - x)
|
||||||
|
r, g, b = image.split()
|
||||||
|
rgba = Image.merge("RGBA", (r, g, b, alpha))
|
||||||
|
buf = io.BytesIO(); rgba.save(buf, format="PNG"); buf.seek(0)
|
||||||
|
up = requests.post(f"{COMFY}/upload/image",
|
||||||
|
files={"image": ("hair_input.png", buf, "image/png")}).json()
|
||||||
|
return up["name"]
|
||||||
|
|
||||||
|
|
||||||
|
def run_once(fname, model, dtype, steps):
|
||||||
|
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮")
|
||||||
|
wf["16"]["inputs"]["unet_name"] = model
|
||||||
|
wf["16"]["inputs"]["weight_dtype"] = dtype
|
||||||
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
r = requests.post(f"{COMFY}/prompt", json={"prompt": wf}).json()
|
||||||
|
if "prompt_id" not in r:
|
||||||
|
raise RuntimeError(f"submit failed: {str(r)[:300]}")
|
||||||
|
pid = r["prompt_id"]
|
||||||
|
deadline = time.time() + 180
|
||||||
|
while time.time() < deadline:
|
||||||
|
time.sleep(0.1)
|
||||||
|
h = requests.get(f"{COMFY}/history/{pid}").json()
|
||||||
|
if pid not in h:
|
||||||
|
continue
|
||||||
|
st = h[pid].get("status", {})
|
||||||
|
if st.get("status_str") == "error":
|
||||||
|
for m in st.get("messages", []):
|
||||||
|
if m[0] == "execution_error":
|
||||||
|
raise RuntimeError(str(m[1])[:300])
|
||||||
|
raise RuntimeError("execution error")
|
||||||
|
if "17" in h[pid].get("outputs", {}):
|
||||||
|
ts = {mm[0]: mm[1].get("timestamp") for mm in st["messages"]}
|
||||||
|
return (ts["execution_success"] - ts["execution_start"]) / 1000.0
|
||||||
|
raise TimeoutError("run exceeded 180s")
|
||||||
|
|
||||||
|
|
||||||
|
CONFIGS = [
|
||||||
|
("flux2.0/flux-2-klein-9b-fp8.safetensors", "fp8_e4m3fn", 6, "9B fp8 (当前)"),
|
||||||
|
("flux2.0/flux-2-klein-9b-fp8.safetensors", "fp8_e4m3fn_fast", 6, "9B fp8-fast"),
|
||||||
|
("flux2.0/flux-2-klein-9b-fp8.safetensors", "fp8_e4m3fn_fast", 4, "9B fp8-fast s4"),
|
||||||
|
("flux-2-klein-4b-fp8.safetensors", "fp8_e4m3fn", 6, "4B fp8"),
|
||||||
|
("flux-2-klein-4b-fp8.safetensors", "fp8_e4m3fn_fast", 6, "4B fp8-fast"),
|
||||||
|
("flux-2-klein-4b-fp8.safetensors", "fp8_e4m3fn_fast", 4, "4B fp8-fast s4"),
|
||||||
|
]
|
||||||
|
|
||||||
|
fname = prep_and_upload()
|
||||||
|
print("input uploaded:", fname)
|
||||||
|
print(f"{'配置':<22}{'warmup':>10}{'run1':>10}{'run2':>10}{'best':>10}")
|
||||||
|
for model, dtype, steps, label in CONFIGS:
|
||||||
|
times = []
|
||||||
|
for i in range(3): # 1 warmup + 2 measured
|
||||||
|
try:
|
||||||
|
t = run_once(fname, model, dtype, steps)
|
||||||
|
except Exception as e:
|
||||||
|
t = float('nan'); print("ERR", label, e)
|
||||||
|
times.append(t)
|
||||||
|
best = min(times[1:])
|
||||||
|
print(f"{label:<22}{times[0]:>9.2f}s{times[1]:>9.2f}s{times[2]:>9.2f}s{best:>9.2f}s")
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Steps sweep + resolution test on the working 9B fp8-fast config."""
|
||||||
|
import io, time
|
||||||
|
import requests
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageFilter
|
||||||
|
import app as A
|
||||||
|
|
||||||
|
COMFY = "http://127.0.0.1:8188"
|
||||||
|
MODEL = "flux2.0/flux-2-klein-9b-fp8.safetensors"
|
||||||
|
DTYPE = "fp8_e4m3fn_fast"
|
||||||
|
|
||||||
|
|
||||||
|
def upload(scale=1.0):
|
||||||
|
image = Image.open("用来重绘.jpg").convert("RGB")
|
||||||
|
mask_img = Image.open("用来重绘.png").convert("RGBA")
|
||||||
|
if scale != 1.0:
|
||||||
|
w, h = image.size
|
||||||
|
w, h = int(w * scale) // 8 * 8, int(h * scale) // 8 * 8
|
||||||
|
image = image.resize((w, h), Image.LANCZOS)
|
||||||
|
mask_data = np.max(np.array(mask_img), axis=2)
|
||||||
|
m = Image.fromarray(mask_data, mode="L")
|
||||||
|
if m.size != image.size:
|
||||||
|
m = m.resize(image.size, Image.LANCZOS)
|
||||||
|
m = m.filter(ImageFilter.GaussianBlur(radius=4))
|
||||||
|
alpha = Image.eval(m, lambda x: 255 - x)
|
||||||
|
r, g, b = image.split()
|
||||||
|
rgba = Image.merge("RGBA", (r, g, b, alpha))
|
||||||
|
buf = io.BytesIO(); rgba.save(buf, format="PNG"); buf.seek(0)
|
||||||
|
up = requests.post(f"{COMFY}/upload/image",
|
||||||
|
files={"image": ("hair_input.png", buf, "image/png")}).json()
|
||||||
|
return up["name"], image.size
|
||||||
|
|
||||||
|
|
||||||
|
def run(fname, steps):
|
||||||
|
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮")
|
||||||
|
wf["16"]["inputs"]["unet_name"] = MODEL
|
||||||
|
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
||||||
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
pid = requests.post(f"{COMFY}/prompt", json={"prompt": wf}).json()["prompt_id"]
|
||||||
|
dl = time.time() + 120
|
||||||
|
while time.time() < dl:
|
||||||
|
time.sleep(0.1)
|
||||||
|
h = requests.get(f"{COMFY}/history/{pid}").json()
|
||||||
|
if pid in h and "17" in h[pid].get("outputs", {}):
|
||||||
|
ts = {m[0]: m[1].get("timestamp") for m in h[pid]["status"]["messages"]}
|
||||||
|
return (ts["execution_success"] - ts["execution_start"]) / 1000.0
|
||||||
|
return float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
print("=== 步数扫描 (9B fp8-fast, 原分辨率 1024x775) ===", flush=True)
|
||||||
|
fname, sz = upload(1.0)
|
||||||
|
run(fname, 6) # warmup
|
||||||
|
res = {}
|
||||||
|
for s in [2, 3, 4, 6, 8]:
|
||||||
|
t = min(run(fname, s), run(fname, s))
|
||||||
|
res[s] = t
|
||||||
|
print(f" steps={s}: {t:.2f}s", flush=True)
|
||||||
|
# derive per-step cost & fixed overhead via two points
|
||||||
|
per = (res[8] - res[2]) / 6
|
||||||
|
fixed = res[2] - per * 2
|
||||||
|
print(f" -> 每步 ~{per:.3f}s, 固定开销(VAE/编码/colormatch/加载) ~{fixed:.2f}s", flush=True)
|
||||||
|
|
||||||
|
print("\n=== 分辨率影响 (steps=4) ===", flush=True)
|
||||||
|
for scale in [1.0, 0.75, 0.6]:
|
||||||
|
fn, s2 = upload(scale)
|
||||||
|
run(fn, 4) # warmup
|
||||||
|
t = min(run(fn, 4), run(fn, 4))
|
||||||
|
print(f" {s2[0]}x{s2[1]} ({s2[0]*s2[1]/1e6:.2f}MP): {t:.2f}s", flush=True)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate result images at different step counts for quality comparison."""
|
||||||
|
import io, time
|
||||||
|
import requests
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageFilter
|
||||||
|
import app as A
|
||||||
|
|
||||||
|
COMFY = "http://127.0.0.1:8188"
|
||||||
|
MODEL = "flux2.0/flux-2-klein-9b-fp8.safetensors"
|
||||||
|
DTYPE = "fp8_e4m3fn_fast"
|
||||||
|
OUT = "output"
|
||||||
|
|
||||||
|
image = Image.open("用来重绘.jpg").convert("RGB")
|
||||||
|
mask_img = Image.open("用来重绘.png").convert("RGBA")
|
||||||
|
mask_data = np.max(np.array(mask_img), axis=2)
|
||||||
|
m = Image.fromarray(mask_data, mode="L")
|
||||||
|
if m.size != image.size:
|
||||||
|
m = m.resize(image.size, Image.LANCZOS)
|
||||||
|
m = m.filter(ImageFilter.GaussianBlur(radius=4))
|
||||||
|
alpha = Image.eval(m, lambda x: 255 - x)
|
||||||
|
r, g, b = image.split()
|
||||||
|
rgba = Image.merge("RGBA", (r, g, b, alpha))
|
||||||
|
buf = io.BytesIO(); rgba.save(buf, format="PNG"); buf.seek(0)
|
||||||
|
fname = requests.post(f"{COMFY}/upload/image",
|
||||||
|
files={"image": ("hair_input.png", buf, "image/png")}).json()["name"]
|
||||||
|
|
||||||
|
# fixed seed for fair comparison
|
||||||
|
SEED = 123456789
|
||||||
|
imgs = []
|
||||||
|
labels = []
|
||||||
|
for steps in [2, 3, 4, 6]:
|
||||||
|
wf = A.build_workflow(fname, "填充遮罩区域的头发,皮肤加一点磨皮", seed=SEED)
|
||||||
|
wf["16"]["inputs"]["unet_name"] = MODEL
|
||||||
|
wf["16"]["inputs"]["weight_dtype"] = DTYPE
|
||||||
|
wf["1"]["inputs"]["steps"] = steps
|
||||||
|
pid = requests.post(f"{COMFY}/prompt", json={"prompt": wf}).json()["prompt_id"]
|
||||||
|
t0 = time.time()
|
||||||
|
while True:
|
||||||
|
time.sleep(0.1)
|
||||||
|
h = requests.get(f"{COMFY}/history/{pid}").json()
|
||||||
|
if pid in h and "17" in h[pid].get("outputs", {}):
|
||||||
|
ts = {mm[0]: mm[1].get("timestamp") for mm in h[pid]["status"]["messages"]}
|
||||||
|
dur = (ts["execution_success"] - ts["execution_start"]) / 1000.0
|
||||||
|
info = h[pid]["outputs"]["17"]["images"][0]
|
||||||
|
data = requests.get(f"{COMFY}/view", params={
|
||||||
|
"filename": info["filename"], "subfolder": info.get("subfolder", ""),
|
||||||
|
"type": info.get("type", "output")}).content
|
||||||
|
im = Image.open(io.BytesIO(data)).convert("RGB")
|
||||||
|
imgs.append(im)
|
||||||
|
labels.append(f"steps={steps} {dur:.2f}s")
|
||||||
|
print(f"steps={steps}: {dur:.2f}s", flush=True)
|
||||||
|
break
|
||||||
|
|
||||||
|
# build side-by-side contact sheet
|
||||||
|
from PIL import ImageDraw
|
||||||
|
h0 = imgs[0].height
|
||||||
|
w0 = imgs[0].width
|
||||||
|
pad = 10
|
||||||
|
bar = 28
|
||||||
|
sheet = Image.new("RGB", (w0 * len(imgs) + pad * (len(imgs) + 1),
|
||||||
|
h0 + bar + pad * 2), (26, 26, 46))
|
||||||
|
d = ImageDraw.Draw(sheet)
|
||||||
|
for i, (im, lb) in enumerate(zip(imgs, labels)):
|
||||||
|
x = pad + i * (w0 + pad)
|
||||||
|
sheet.paste(im, (x, bar + pad))
|
||||||
|
d.text((x + 4, 6), lb, fill=(233, 69, 96))
|
||||||
|
sheet.save(f"{OUT}/compare_steps.png")
|
||||||
|
print("saved:", f"{OUT}/compare_steps.png", flush=True)
|
||||||
+17
-2
@@ -136,7 +136,13 @@ generateBtn.addEventListener('click', async () => {
|
|||||||
|
|
||||||
generateBtn.disabled = true;
|
generateBtn.disabled = true;
|
||||||
generateBtn.textContent = '⏳ 生成中...';
|
generateBtn.textContent = '⏳ 生成中...';
|
||||||
resultArea.innerHTML = '<div class="loading"><div class="spinner"></div><br>正在调用 ComfyUI 生成,请耐心等待...</div>';
|
|
||||||
|
const startTime = performance.now();
|
||||||
|
resultArea.innerHTML = '<div class="loading"><div class="spinner"></div><br>正在调用 ComfyUI 生成,请耐心等待...<div id="liveTimer" style="margin-top:8px;font-size:15px;color:#aaa">已用时 0.0s</div></div>';
|
||||||
|
const liveTimer = document.getElementById('liveTimer');
|
||||||
|
const timerId = setInterval(() => {
|
||||||
|
if (liveTimer) liveTimer.textContent = '已用时 ' + ((performance.now() - startTime) / 1000).toFixed(1) + 's';
|
||||||
|
}, 100);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -152,8 +158,15 @@ generateBtn.addEventListener('click', async () => {
|
|||||||
|
|
||||||
const resultBlob = await resp.blob();
|
const resultBlob = await resp.blob();
|
||||||
const resultUrl = URL.createObjectURL(resultBlob);
|
const resultUrl = URL.createObjectURL(resultBlob);
|
||||||
|
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
|
||||||
|
// 服务端纯推理耗时(若返回该响应头)
|
||||||
|
const serverTime = resp.headers.get('X-Generate-Time');
|
||||||
|
const serverInfo = serverTime ? `,服务端推理 ${parseFloat(serverTime).toFixed(1)}s` : '';
|
||||||
|
|
||||||
resultArea.innerHTML = `
|
resultArea.innerHTML = `
|
||||||
|
<div style="text-align:center;margin-bottom:12px;color:#4ade80;font-size:16px;font-weight:600">
|
||||||
|
⏱️ 本次重绘耗时 ${elapsed}s${serverInfo}
|
||||||
|
</div>
|
||||||
<div class="result-area">
|
<div class="result-area">
|
||||||
<div class="result-item">
|
<div class="result-item">
|
||||||
<h3>原图</h3>
|
<h3>原图</h3>
|
||||||
@@ -166,8 +179,10 @@ generateBtn.addEventListener('click', async () => {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resultArea.innerHTML = `<div class="error">❌ ${err.message}</div>`;
|
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
|
||||||
|
resultArea.innerHTML = `<div class="error">❌ ${err.message}<br><span style="color:#aaa;font-size:13px">(耗时 ${elapsed}s)</span></div>`;
|
||||||
} finally {
|
} finally {
|
||||||
|
clearInterval(timerId);
|
||||||
generateBtn.disabled = false;
|
generateBtn.disabled = false;
|
||||||
generateBtn.textContent = '🚀 生成';
|
generateBtn.textContent = '🚀 生成';
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 910 KiB After Width: | Height: | Size: 899 KiB |
Reference in New Issue
Block a user