flashimgs / app.py
opsiclear-admin's picture
FlashImgs ZeroGPU Space: Gradio SDK, AGPL-3.0 wrapper, vendored sm_120 wheels
28026e3 verified
Raw
History Blame Contribute Delete
24.8 kB
# FlashImgs Hugging Face Space
# Copyright (C) 2025 OpsiClear
#
# This program (the Gradio wrapper) is free software: you can redistribute it
# and/or modify it under the terms of the GNU Affero General Public License v3.0
# as published by the Free Software Foundation. See the LICENSE file for the full
# text. It is distributed WITHOUT ANY WARRANTY.
#
# The bundled FlashImgs engine wheels (wheels/) are a separate work under the
# OpsiClear Restrictive License and are NOT covered by the AGPL; see NOTICE.
from __future__ import annotations
import gc
import math
import os
import shutil
import struct
import time
import uuid
from pathlib import Path
import gradio as gr
import numpy as np
import torch
from PIL import Image, ImageOps
try:
import spaces # Hugging Face ZeroGPU runtime
except Exception: # effect-free fallback when not running on ZeroGPU
class _SpacesStub:
@staticmethod
def GPU(*args, **kwargs):
def _decorator(func):
return func
if args and callable(args[0]):
return args[0]
return _decorator
spaces = _SpacesStub()
os.environ.setdefault("FLASHIMGS_GAUSSIFIER_AOTI", "0")
OUTPUT_ROOT = Path(os.environ.get("FLASHIMGS_OUTPUT_DIR", "/tmp/flashimgs_hf_outputs"))
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
APP_CSS = """
#workspace {
max-width: 1440px;
margin: 0 auto;
gap: 16px;
align-items: flex-start;
}
.settings-panel {
flex: 0 0 330px !important;
max-width: 350px !important;
min-width: 300px !important;
}
.results-panel {
flex: 1 1 0 !important;
min-width: min(760px, 100%) !important;
}
.preview-tabs, .preview-tabs > div {
min-width: 0;
}
.download-row {
gap: 12px;
}
@media (max-width: 920px) {
#workspace {
display: block;
}
.settings-panel, .results-panel {
max-width: none !important;
min-width: 0 !important;
flex-basis: auto !important;
}
}
"""
def _load_flashimgs():
import flashimgs
return flashimgs
def _device_name() -> str:
if not torch.cuda.is_available():
return "CPU"
return torch.cuda.get_device_name(torch.cuda.current_device())
def _prepare_image(image: Image.Image, max_side: int) -> tuple[torch.Tensor, Image.Image, torch.Tensor | None, str]:
if image is None:
raise gr.Error("Upload an image first.")
image = ImageOps.exif_transpose(image)
original_w, original_h = image.size
has_alpha = image.mode in {"RGBA", "LA"} or (image.mode == "P" and "transparency" in image.info)
image_rgba = image.convert("RGBA")
max_side = int(max_side)
if max_side > 0 and max(original_w, original_h) > max_side:
scale = max_side / float(max(original_w, original_h))
new_size = (max(1, round(original_w * scale)), max(1, round(original_h * scale)))
image_rgba = image_rgba.resize(new_size, Image.Resampling.LANCZOS)
rgba = np.asarray(image_rgba, dtype=np.uint8)
rgb = rgba[..., :3].astype(np.float32) / 255.0
image_rgb = Image.fromarray(rgba[..., :3], mode="RGB")
image_tensor = torch.from_numpy(rgb).permute(2, 0, 1).contiguous()
mask_tensor = None
if has_alpha:
alpha = rgba[..., 3]
if alpha.min() < 255:
mask_array = (alpha > 127).astype(np.float32)
if np.any(mask_array):
mask_tensor = torch.from_numpy(mask_array).unsqueeze(0).contiguous()
size_note = f"{original_w}x{original_h}"
if image_rgba.size != (original_w, original_h):
size_note += f" -> {image_rgba.size[0]}x{image_rgba.size[1]}"
return image_tensor, image_rgb, mask_tensor, size_note
def _tensor_to_pil(tensor: torch.Tensor) -> Image.Image:
array = (
tensor.detach()
.clamp(0.0, 1.0)
.mul(255.0)
.byte()
.permute(1, 2, 0)
.cpu()
.numpy()
)
return Image.fromarray(array, mode="RGB")
def _prune_old_outputs(keep: int = 20) -> None:
runs = [p for p in OUTPUT_ROOT.iterdir() if p.is_dir()]
runs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
for path in runs[keep:]:
shutil.rmtree(path, ignore_errors=True)
def _load_splat2d(path: Path):
with path.open("rb") as f:
magic = f.read(4)
if magic != b"GS2D":
raise gr.Error("The cached Splat2D file is not a FlashImgs GS2D file.")
header = f.read(8)
if len(header) != 8:
raise gr.Error("The cached Splat2D file is truncated.")
n_gaussians, height, width = struct.unpack("<IHH", header)
floats = np.fromfile(f, dtype="<f4")
if n_gaussians <= 0:
raise gr.Error("The cached Splat2D file has no Gaussians.")
if floats.size % n_gaussians != 0:
raise gr.Error("The cached Splat2D file has an invalid payload length.")
floats_per_gaussian = floats.size // n_gaussians
feature_dim = floats_per_gaussian - 5
if feature_dim <= 0:
raise gr.Error("The cached Splat2D file does not contain color features.")
offset = 0
xy = floats[offset : offset + n_gaussians * 2].reshape(n_gaussians, 2)
offset += n_gaussians * 2
raw_scale = floats[offset : offset + n_gaussians * 2].reshape(n_gaussians, 2)
offset += n_gaussians * 2
rot = floats[offset : offset + n_gaussians]
offset += n_gaussians
feat = floats[offset:].reshape(n_gaussians, feature_dim)
# The Gradio training path uses FlashImgs' normal inverse-scale parameterization.
scale_px = 1.0 / np.maximum(raw_scale, 0.01)
return xy, scale_px.astype(np.float32), rot, feat, int(height), int(width)
def _mask_edge_distance(mask_array: np.ndarray) -> np.ndarray:
import cv2
mask_u8 = (mask_array > 0).astype(np.uint8)
padded = np.pad(mask_u8, 1, mode="constant", constant_values=0)
dist = cv2.distanceTransform(padded, cv2.DIST_L2, cv2.DIST_MASK_PRECISE)
return dist[1:-1, 1:-1].astype(np.float32, copy=False)
def _write_ply_from_splat_arrays(
path: Path,
xy: np.ndarray,
scale_px: np.ndarray,
rot: np.ndarray,
feat: np.ndarray,
height: int,
width: int,
*,
mask_array: np.ndarray | None = None,
mask_edge_clamp: float = 0.35,
opacity_scale: float,
min_opacity: float,
max_opacity: float,
scale_multiplier: float,
thickness: float,
y_axis: str,
) -> None:
n_gaussians = int(xy.shape[0])
height_f = max(float(height), 1.0)
width_f = max(float(width), 1.0)
aspect = width_f / height_f
world_height = 1.0
thickness = max(float(thickness), 1e-8)
opacity_mode = "factor_rgb"
opacity_scale = max(float(opacity_scale), 1e-6)
min_opacity = min(1.0 - 1e-6, max(1e-6, float(min_opacity)))
max_opacity = min(1.0 - 1e-6, max(min_opacity, float(max_opacity)))
scale_multiplier = max(float(scale_multiplier), 1e-6)
y_axis = str(y_axis).lower().strip()
if y_axis not in {"down", "up"}:
y_axis = "down"
if mask_array is not None and float(mask_edge_clamp) > 0:
dist = _mask_edge_distance(mask_array)
x_px = np.clip(np.floor(xy[:, 0] * float(width)).astype(np.int64), 0, int(width) - 1)
y_px = np.clip(np.floor(xy[:, 1] * float(height)).astype(np.int64), 0, int(height) - 1)
max_scale = np.maximum(0.25, dist[y_px, x_px] * float(mask_edge_clamp)).astype(np.float32)
scale_px = np.minimum(scale_px, max_scale[:, None]).astype(np.float32, copy=False)
if feat.shape[1] == 1:
rgb = np.repeat(feat[:, :1], 3, axis=1)
elif feat.shape[1] < 3:
rgb = np.pad(feat, ((0, 0), (0, 3 - feat.shape[1])), mode="constant")
else:
rgb = feat[:, :3]
rgb = np.clip(rgb, 0.0, 1.0).astype(np.float32, copy=False)
amp = np.max(rgb, axis=1).astype(np.float32, copy=False)
alpha = np.clip(opacity_scale * amp, min_opacity, max_opacity).astype(np.float32)
ply_rgb = np.clip(rgb / alpha[:, None], 0.0, 1.0).astype(np.float32, copy=False)
alpha = np.clip(alpha, 1e-6, 1.0 - 1e-6).astype(np.float32, copy=False)
opacity_logit = np.log(alpha / (1.0 - alpha)).astype(np.float32, copy=False)
f_dc = (ply_rgb - 0.5) / 0.28209479177387814
names = ["x", "y", "z", "nx", "ny", "nz", "f_dc_0", "f_dc_1", "f_dc_2"]
names += [f"f_rest_{i}" for i in range(45)]
names += ["opacity", "scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3"]
names += ["fi_u", "fi_v", "fi_scale_x_px", "fi_scale_y_px", "fi_rot", "fi_r", "fi_g", "fi_b"]
vertices = np.zeros(n_gaussians, dtype=np.dtype([(name, "<f4") for name in names]))
vertices["x"] = (xy[:, 0] - 0.5) * aspect * world_height
if y_axis == "down":
vertices["y"] = (xy[:, 1] - 0.5) * world_height
theta = -rot
else:
vertices["y"] = (0.5 - xy[:, 1]) * world_height
theta = rot
vertices["z"] = 0.0
vertices["f_dc_0"] = f_dc[:, 0]
vertices["f_dc_1"] = f_dc[:, 1]
vertices["f_dc_2"] = f_dc[:, 2]
vertices["opacity"] = opacity_logit
scale_world = np.maximum(scale_px * scale_multiplier * (world_height / height_f), 1e-8)
vertices["scale_0"] = np.log(scale_world[:, 0])
vertices["scale_1"] = np.log(scale_world[:, 1])
vertices["scale_2"] = math.log(thickness)
vertices["rot_0"] = np.cos(0.5 * theta)
vertices["rot_1"] = 0.0
vertices["rot_2"] = 0.0
vertices["rot_3"] = np.sin(0.5 * theta)
vertices["fi_u"] = xy[:, 0]
vertices["fi_v"] = xy[:, 1]
vertices["fi_scale_x_px"] = scale_px[:, 0]
vertices["fi_scale_y_px"] = scale_px[:, 1]
vertices["fi_rot"] = rot
vertices["fi_r"] = rgb[:, 0]
vertices["fi_g"] = rgb[:, 1]
vertices["fi_b"] = rgb[:, 2]
path.parent.mkdir(parents=True, exist_ok=True)
header = [
"ply",
"format binary_little_endian 1.0",
"comment FlashImgs planar 2DGS export for standard 3DGS viewers",
f"comment fi_image_width {int(width)}",
f"comment fi_image_height {int(height)}",
f"comment fi_world_height {world_height:.9g}",
f"comment fi_world_width {aspect * world_height:.9g}",
f"comment fi_y_axis {y_axis}",
f"comment fi_opacity_mode {opacity_mode}",
f"comment fi_opacity_scale {opacity_scale:.9g}",
f"comment fi_min_opacity {min_opacity:.9g}",
f"comment fi_max_opacity {max_opacity:.9g}",
f"comment fi_scale_multiplier {scale_multiplier:.9g}",
f"comment fi_mask_edge_clamp {float(mask_edge_clamp):.9g}",
f"element vertex {n_gaussians}",
]
header.extend(f"property float {name}" for name in names)
header.append("end_header")
with path.open("wb") as f:
f.write(("\n".join(header) + "\n").encode("ascii"))
vertices.tofile(f)
def update_ply_preview(
splat_path_value,
current_ply_path_value,
mask_path_value,
mask_edge_clamp: float,
ply_opacity_scale: float,
ply_min_opacity: float,
ply_max_opacity: float,
ply_scale_multiplier: float,
ply_thickness: float,
ply_y_axis: str,
):
if not splat_path_value:
return gr.update(), gr.update(), current_ply_path_value
splat_path = Path(str(splat_path_value))
if not splat_path.exists():
return gr.update(), gr.update(), current_ply_path_value
xy, scale_px, rot, feat, height, width = _load_splat2d(splat_path)
mask_array = None
if mask_path_value:
mask_path = Path(str(mask_path_value))
if mask_path.exists():
mask_array = np.load(mask_path).astype(bool)
old_path = Path(str(current_ply_path_value)) if current_ply_path_value else None
ply_path = splat_path.parent / f"flashimgs_fit_edited_{uuid.uuid4().hex[:8]}.ply"
_write_ply_from_splat_arrays(
ply_path,
xy,
scale_px,
rot,
feat,
height,
width,
mask_array=mask_array,
mask_edge_clamp=float(mask_edge_clamp),
opacity_scale=float(ply_opacity_scale),
min_opacity=float(ply_min_opacity),
max_opacity=float(ply_max_opacity),
scale_multiplier=float(ply_scale_multiplier),
thickness=float(ply_thickness),
y_axis=ply_y_axis,
)
if old_path and old_path.name.startswith("flashimgs_fit_edited_") and old_path.exists():
old_path.unlink(missing_ok=True)
return str(ply_path), str(ply_path), str(ply_path)
def _fit_with_progress(
flashimgs,
image_tensor: torch.Tensor,
*,
gaussians: int,
steps: int,
lr: float,
scale: float,
loss: str,
seed: int,
mask_tensor: torch.Tensor | None,
mask_border_padding: bool,
progress: gr.Progress,
):
import main as _main
total_steps = max(1, int(steps))
session = flashimgs.Session.from_tensor(
image_tensor,
gaussians=max(0, int(gaussians)),
scale=float(scale),
loss=loss,
lr_mult=float(lr),
device="cuda",
seed=int(seed),
mask_tensor=mask_tensor,
mask_border_padding=bool(mask_border_padding),
)
last_update = 0
update_every = max(1, total_steps // 100)
def on_step(completed_step: int, _gs) -> None:
nonlocal last_update
if completed_step < total_steps and completed_step - last_update < update_every:
return
last_update = int(completed_step)
frac = min(1.0, max(0.0, completed_step / total_steps))
progress(
0.10 + 0.78 * frac,
desc=f"Fitting splats: {completed_step:,}/{total_steps:,} steps",
)
elapsed_s, _, _ = _main.train_loop(
session.model,
total_steps,
verbose=False,
snapshot_hook=on_step,
)
on_step(total_steps, session.model)
return session, 0.0, 0.0, float(elapsed_s)
def _fit_duration(*args, **kwargs) -> float:
"""Seconds of ZeroGPU time to reserve for a fit, estimated from its arguments.
Mirrors ``fit_image``'s positional signature ``(image, max_side, gaussians,
steps, ...)``. Fits are fast, but reserve headroom for large images, high
step counts, and first-call CUDA + extension warmup.
"""
def _arg(idx: int, default: float) -> float:
try:
return float(args[idx])
except (IndexError, TypeError, ValueError):
return default
max_side = _arg(1, 1536.0)
gaussians = _arg(2, 0.0)
steps = _arg(3, 900.0)
est = 30.0 + 0.06 * steps + (gaussians / 100000.0) * 40.0
if max_side == 0.0 or max_side > 2048.0:
est += 30.0
return float(min(180.0, max(45.0, est)))
@spaces.GPU(duration=_fit_duration)
def fit_image(
image: Image.Image,
max_side: int,
gaussians: int,
steps: int,
lr: float,
scale: float,
loss: str,
render_height: int,
seed: int,
mask_border_padding: bool,
mask_edge_clamp: float,
ply_opacity_scale: float,
ply_min_opacity: float,
ply_max_opacity: float,
ply_scale_multiplier: float,
ply_thickness: float,
ply_y_axis: str,
progress: gr.Progress = gr.Progress(track_tqdm=False),
):
if not torch.cuda.is_available():
raise gr.Error(
"FlashImgs fitting needs CUDA. In Space settings, select a GPU hardware tier."
)
progress(0.02, desc="Preparing image")
image_tensor, resized_image, mask_tensor, size_note = _prepare_image(image, max_side)
fit_w, fit_h = resized_image.size
mask_source = "alpha" if mask_tensor is not None else "none"
if mask_tensor is None and bool(mask_border_padding):
mask_tensor = torch.ones((1, fit_h, fit_w), dtype=torch.float32).contiguous()
mask_source = "image border"
run_dir = OUTPUT_ROOT / uuid.uuid4().hex
run_dir.mkdir(parents=True, exist_ok=True)
splat_path = run_dir / "flashimgs_fit.splat2d"
ply_path = run_dir / "flashimgs_fit.ply"
mask_path = run_dir / "mask.npy"
flashimgs = _load_flashimgs()
session = None
psnr = 0.0
ssim = 0.0
train_elapsed_s = 0.0
start = time.time()
try:
progress(0.1, desc=f"Fitting splats: 0/{max(1, int(steps)):,} steps")
session, psnr, ssim, train_elapsed_s = _fit_with_progress(
flashimgs,
image_tensor,
gaussians=gaussians,
steps=steps,
lr=lr,
scale=scale,
loss=loss,
seed=seed,
mask_tensor=mask_tensor,
mask_border_padding=mask_border_padding,
progress=progress,
)
if mask_tensor is not None:
progress(0.86, desc="Pruning mask boundary")
session.mask_prune()
session.materialize_gate()
psnr, ssim = session.evaluate()
progress(0.88, desc="Rendering")
render_at = None if int(render_height) <= 0 else int(render_height)
reconstruction = _tensor_to_pil(session.render(height=render_at))
progress(0.94, desc="Exporting")
session.export_splat2d(str(splat_path))
mask_state_path = None
mask_array = None
if mask_tensor is not None:
mask_array = mask_tensor.squeeze(0).cpu().numpy().astype(bool)
np.save(mask_path, mask_array.astype(np.uint8))
mask_state_path = str(mask_path)
xy, scale_px, rot, feat, height, width = _load_splat2d(splat_path)
_write_ply_from_splat_arrays(
ply_path,
xy,
scale_px,
rot,
feat,
height,
width,
mask_array=mask_array,
mask_edge_clamp=float(mask_edge_clamp),
opacity_scale=float(ply_opacity_scale),
min_opacity=float(ply_min_opacity),
max_opacity=float(ply_max_opacity),
scale_multiplier=float(ply_scale_multiplier),
thickness=float(ply_thickness),
y_axis=ply_y_axis,
)
total_s = time.time() - start
n_gaussians = int(session.n_gaussians)
metrics = "\n".join(
[
f"Device: {_device_name()}",
f"Input: {size_note}",
f"Fit resolution: {fit_w}x{fit_h}",
f"Mask: {mask_source}",
f"Mask border padding: {'on' if mask_tensor is not None and mask_border_padding else 'off'}",
f"Mask edge clamp: {float(mask_edge_clamp):.2f}x" if mask_tensor is not None else "Mask edge clamp: off",
f"Gaussians: {n_gaussians:,}",
f"Steps: {int(steps):,}",
f"PSNR: {psnr:.2f} dB",
f"SSIM: {ssim:.5f}",
f"Training time: {train_elapsed_s:.2f} s",
f"Total request time: {total_s:.2f} s",
]
)
_prune_old_outputs()
progress(1.0, desc="Done")
return reconstruction, metrics, str(splat_path), str(ply_path), str(ply_path), str(splat_path), str(ply_path), mask_state_path
finally:
del session
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
with gr.Blocks(title="FlashImgs") as demo:
gr.Markdown("# FlashImgs")
splat_state = gr.State(value=None)
ply_state = gr.State(value=None)
mask_state = gr.State(value=None)
with gr.Row(elem_id="workspace"):
with gr.Column(scale=1, min_width=300, elem_classes=["settings-panel"]):
image = gr.Image(
label="Image",
type="pil",
sources=["upload", "clipboard"],
image_mode="RGBA",
height=240,
)
run = gr.Button("Fit", variant="primary")
with gr.Accordion("Fit", open=True):
max_side = gr.Slider(0, 4096, value=1536, step=64, label="Max side (0 = original)")
gaussians = gr.Slider(0, 100000, value=0, step=1000, label="Gaussians")
steps = gr.Slider(100, 3200, value=900, step=100, label="Steps")
loss = gr.Dropdown(["l2", "l1", "l2+ssim", "l1+ssim"], value="l2", label="Loss")
mask_border_padding = gr.Checkbox(value=True, label="Treat image border as mask border")
mask_edge_clamp = gr.Slider(0.0, 1.0, value=0.35, step=0.05, label="Mask edge scale clamp")
with gr.Accordion("PLY", open=True):
ply_opacity_scale = gr.Slider(0.1, 4.0, value=1.0, step=0.1, label="Opacity scale")
ply_min_opacity = gr.Slider(0.001, 0.25, value=0.01, step=0.001, label="Min opacity")
ply_max_opacity = gr.Slider(0.05, 0.99, value=0.95, step=0.01, label="Max opacity")
ply_scale_multiplier = gr.Slider(0.25, 3.0, value=1.0, step=0.05, label="XY scale multiplier")
ply_thickness = gr.Slider(0.00001, 0.002, value=0.0001, step=0.00001, label="Z thickness")
ply_y_axis = gr.Dropdown(["down", "up"], value="down", label="PLY Y axis")
update_ply = gr.Button("Update PLY Preview")
with gr.Accordion("Advanced", open=False):
lr = gr.Slider(1.0, 16.0, value=11.0, step=0.5, label="LR")
scale = gr.Slider(0.5, 4.0, value=1.5, step=0.1, label="Scale")
render_height = gr.Slider(0, 2048, value=0, step=128, label="Render height")
seed = gr.Number(value=42, precision=0, label="Seed")
with gr.Column(scale=3, min_width=520, elem_classes=["results-panel"]):
with gr.Tabs(elem_classes=["preview-tabs"]):
with gr.Tab("3DGS"):
ply_preview = gr.Model3D(
label="3DGS Preview",
display_mode="point_cloud",
clear_color=(0.03, 0.03, 0.03, 1.0),
height=620,
)
with gr.Tab("Image"):
reconstruction = gr.Image(label="Reconstruction", type="pil", height=620)
with gr.Accordion("Details and downloads", open=False):
metrics = gr.Textbox(label="Metrics", lines=9)
with gr.Row(elem_classes=["download-row"]):
splat = gr.File(label="Splat2D")
ply = gr.File(label="Edited 3DGS PLY")
gr.Markdown(
"FlashImgs Space · © 2025 OpsiClear · wrapper licensed "
"[AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.html) — source available "
"in this Space's **Files** tab. The bundled FlashImgs engine is separately "
"licensed (non-commercial); see NOTICE. No warranty.",
elem_id="app-footer",
)
run.click(
fn=fit_image,
inputs=[
image,
max_side,
gaussians,
steps,
lr,
scale,
loss,
render_height,
seed,
mask_border_padding,
mask_edge_clamp,
ply_opacity_scale,
ply_min_opacity,
ply_max_opacity,
ply_scale_multiplier,
ply_thickness,
ply_y_axis,
],
outputs=[reconstruction, metrics, splat, ply, ply_preview, splat_state, ply_state, mask_state],
show_progress_on=[ply_preview],
)
ply_update_inputs = [
splat_state,
ply_state,
mask_state,
mask_edge_clamp,
ply_opacity_scale,
ply_min_opacity,
ply_max_opacity,
ply_scale_multiplier,
ply_thickness,
ply_y_axis,
]
ply_update_outputs = [ply, ply_preview, ply_state]
update_ply.click(
fn=update_ply_preview,
inputs=ply_update_inputs,
outputs=ply_update_outputs,
show_progress="minimal",
show_progress_on=[ply_preview],
trigger_mode="always_last",
concurrency_limit=1,
concurrency_id="ply_update",
)
for control in [mask_edge_clamp, ply_opacity_scale, ply_min_opacity, ply_max_opacity, ply_scale_multiplier, ply_thickness]:
control.input(
fn=update_ply_preview,
inputs=ply_update_inputs,
outputs=ply_update_outputs,
show_progress="hidden",
trigger_mode="always_last",
concurrency_limit=1,
concurrency_id="ply_update",
)
for control in [ply_y_axis]:
control.change(
fn=update_ply_preview,
inputs=ply_update_inputs,
outputs=ply_update_outputs,
show_progress="hidden",
trigger_mode="always_last",
concurrency_limit=1,
concurrency_id="ply_update",
)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=1, max_size=8).launch(
server_name="0.0.0.0",
server_port=int(os.environ.get("PORT", "7860")),
css=APP_CSS,
)