feat: port img2img watercolour develop to ZeroGPU (staged LoRA)
#1
by reversely - opened
- app.py +240 -0
- packages.txt +1 -0
- requirements.txt +4 -1
app.py
CHANGED
|
@@ -1274,6 +1274,12 @@ def client_config():
|
|
| 1274 |
"asrProfiles": list(ASR_CHUNK_PROFILES.keys()),
|
| 1275 |
"asrDefaultProfile": ASR_DEFAULT_PROFILE,
|
| 1276 |
"captionModel": AUTOCAPTION_MODEL_ID,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1277 |
"phoenix": _get_tracer() is not None,
|
| 1278 |
}
|
| 1279 |
|
|
@@ -1411,6 +1417,240 @@ async def nsfw_rest(request: Request):
|
|
| 1411 |
return {"score": 0.0, "blocked": False, "threshold": NSFW_THRESHOLD, "available": available, "error": str(exc)}
|
| 1412 |
|
| 1413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1414 |
# --------------------------------------------------------------------------- #
|
| 1415 |
# Voice → text via NVIDIA NeMo ASR (item 5). Streaming Nemotron model; needs a
|
| 1416 |
# GPU to be practical. Loaded lazily; if NeMo/torch/GPU are missing the endpoint
|
|
|
|
| 1274 |
"asrProfiles": list(ASR_CHUNK_PROFILES.keys()),
|
| 1275 |
"asrDefaultProfile": ASR_DEFAULT_PROFILE,
|
| 1276 |
"captionModel": AUTOCAPTION_MODEL_ID,
|
| 1277 |
+
# "Proceed to Development" watercolour: the endpoint always exists; this
|
| 1278 |
+
# only reports whether torch/diffusers are present (a CUDA-free check —
|
| 1279 |
+
# the LoRA is optional, stage 1 runs base img2img). The ~14 GB pipeline
|
| 1280 |
+
# still builds lazily on first use inside the GPU worker.
|
| 1281 |
+
"img2imgEnabled": _img2img_prep(),
|
| 1282 |
+
"img2imgModel": IMG2IMG_MODEL_ID,
|
| 1283 |
"phoenix": _get_tracer() is not None,
|
| 1284 |
}
|
| 1285 |
|
|
|
|
| 1417 |
return {"score": 0.0, "blocked": False, "threshold": NSFW_THRESHOLD, "available": available, "error": str(exc)}
|
| 1418 |
|
| 1419 |
|
| 1420 |
+
# --------------------------------------------------------------------------- #
|
| 1421 |
+
# Develop → watercolour via Flux.2-klein + a scene LoRA (in-process img2img).
|
| 1422 |
+
# This is what "Proceed to Development" triggers: each photo is sent here and
|
| 1423 |
+
# painted into a watercolour by Flux2KleinPipeline (the source is VAE-encoded
|
| 1424 |
+
# and used as reference conditioning while the LoRA steers the watercolour
|
| 1425 |
+
# style).
|
| 1426 |
+
#
|
| 1427 |
+
# ZeroGPU port notes (mirrors the caption/NSFW/ASR helpers in this file):
|
| 1428 |
+
# * The pipeline is ~14 GB and lives ONLY on the GPU, so it must be built
|
| 1429 |
+
# INSIDE a @spaces.GPU context — never in the main process, where CUDA must
|
| 1430 |
+
# stay un-initialized. `_img2img_prep()` does the CUDA-free part (import +
|
| 1431 |
+
# LoRA file check) so /config and the endpoint can report availability
|
| 1432 |
+
# cheaply; `_ensure_img2img_pipe()` does the actual GPU build, lazily, the
|
| 1433 |
+
# first time `_img2img_infer` runs in a worker.
|
| 1434 |
+
# * @spaces.GPU pickles arguments to/from the worker. The Flux pipe is NOT
|
| 1435 |
+
# picklable, so it is fetched from module state inside the worker and never
|
| 1436 |
+
# crosses the boundary. Only the PIL image in and the PIL image out cross
|
| 1437 |
+
# it (both picklable), exactly like `_nsfw_infer`.
|
| 1438 |
+
# * This file is self-contained on the Space (no backend/ package), so the
|
| 1439 |
+
# squaring + load + stylize logic from backend/hf/3_infer_img2img.py is
|
| 1440 |
+
# inlined here rather than imported.
|
| 1441 |
+
#
|
| 1442 |
+
# STAGED ROLLOUT: the scene LoRA is OPTIONAL. Stage 1 (now) is "pure img2img" —
|
| 1443 |
+
# the base Flux2Klein model develops the photo with a generic watercolour prompt,
|
| 1444 |
+
# no .safetensors required. Stage 2 is dropping the LoRA file in (or setting
|
| 1445 |
+
# SOZAI_IMG2IMG_LORA_REPO): it's detected automatically and the trained
|
| 1446 |
+
# watercolour style + trigger prompt switch on. So a missing LoRA degrades to
|
| 1447 |
+
# base img2img rather than disabling the feature.
|
| 1448 |
+
#
|
| 1449 |
+
# Fails CLOSED (available=false) only when torch/diffusers are absent, so a CPU
|
| 1450 |
+
# Space or local Mac keeps the original photo instead of erroring/OOMing.
|
| 1451 |
+
# --------------------------------------------------------------------------- #
|
| 1452 |
+
IMG2IMG_MODEL_ID = os.environ.get("SOZAI_IMG2IMG_MODEL", "black-forest-labs/FLUX.2-klein-4B")
|
| 1453 |
+
# Path to the watercolour scene LoRA .safetensors. OPTIONAL: ship the file in the
|
| 1454 |
+
# repo at this path, or point SOZAI_IMG2IMG_LORA_REPO/_FILE at a Hub repo to have
|
| 1455 |
+
# it downloaded (CPU/network only) at prep time. Absent = base img2img (stage 1).
|
| 1456 |
+
IMG2IMG_LORA_PATH = os.environ.get(
|
| 1457 |
+
"SOZAI_IMG2IMG_LORA",
|
| 1458 |
+
os.path.join(BASE, "data/places/checkpoints/hf-spaces/scene_lora.safetensors"),
|
| 1459 |
+
)
|
| 1460 |
+
IMG2IMG_LORA_REPO = os.environ.get("SOZAI_IMG2IMG_LORA_REPO", "")
|
| 1461 |
+
IMG2IMG_LORA_FILE = os.environ.get("SOZAI_IMG2IMG_LORA_FILE", "scene_lora.safetensors")
|
| 1462 |
+
IMG2IMG_LORA_SCALE = float(os.environ.get("SOZAI_IMG2IMG_LORA_SCALE", "1.0"))
|
| 1463 |
+
# Generation defaults (from backend/hf/3_infer_img2img.py).
|
| 1464 |
+
IMG2IMG_TRIGGER = "wtrclr8"
|
| 1465 |
+
# Prompt used WITH the LoRA (its trigger token), vs. the base-model fallback used
|
| 1466 |
+
# in stage 1 when no LoRA is loaded — the trigger token means nothing without it.
|
| 1467 |
+
IMG2IMG_PROMPT = os.environ.get("SOZAI_IMG2IMG_PROMPT", "WTRCLR8 watercolour painting")
|
| 1468 |
+
IMG2IMG_BASE_PROMPT = os.environ.get(
|
| 1469 |
+
"SOZAI_IMG2IMG_BASE_PROMPT",
|
| 1470 |
+
"a soft watercolour painting of this scene, painterly, textured paper, gentle washes",
|
| 1471 |
+
)
|
| 1472 |
+
IMG2IMG_SIZE = int(os.environ.get("SOZAI_IMG2IMG_SIZE", "1024"))
|
| 1473 |
+
IMG2IMG_STEPS = int(os.environ.get("SOZAI_IMG2IMG_STEPS", "8"))
|
| 1474 |
+
IMG2IMG_GUIDANCE = float(os.environ.get("SOZAI_IMG2IMG_GUIDANCE", "3.5"))
|
| 1475 |
+
IMG2IMG_SEED = int(os.environ.get("SOZAI_IMG2IMG_SEED", "42"))
|
| 1476 |
+
_img2img_lock = threading.Lock()
|
| 1477 |
+
_img2img_state = {"loaded": False, "available": False, "pipe": None,
|
| 1478 |
+
"lora_path": None, "lora_loaded": False}
|
| 1479 |
+
|
| 1480 |
+
|
| 1481 |
+
def _img2img_prep() -> bool:
|
| 1482 |
+
"""CUDA-free availability check, run in the main process. Verifies diffusers
|
| 1483 |
+
(with Flux2KleinPipeline) imports; the LoRA is OPTIONAL — if a file is present
|
| 1484 |
+
(or downloadable via SOZAI_IMG2IMG_LORA_REPO) its path is cached for stage 2,
|
| 1485 |
+
otherwise we fall back to base img2img. Does NOT touch CUDA or build the
|
| 1486 |
+
pipeline. Returns True whenever the base feature can run in a GPU worker."""
|
| 1487 |
+
if _img2img_state["loaded"]:
|
| 1488 |
+
return _img2img_state["available"]
|
| 1489 |
+
with _img2img_lock:
|
| 1490 |
+
if _img2img_state["loaded"]:
|
| 1491 |
+
return _img2img_state["available"]
|
| 1492 |
+
try:
|
| 1493 |
+
import torch # noqa: F401 (import check only — no CUDA here)
|
| 1494 |
+
from diffusers import Flux2KleinPipeline # noqa: F401
|
| 1495 |
+
# Resolve the LoRA if we can, but its absence is NOT fatal.
|
| 1496 |
+
lora_path = IMG2IMG_LORA_PATH if (IMG2IMG_LORA_PATH and
|
| 1497 |
+
os.path.isfile(IMG2IMG_LORA_PATH)) else None
|
| 1498 |
+
if lora_path is None and IMG2IMG_LORA_REPO:
|
| 1499 |
+
try:
|
| 1500 |
+
from huggingface_hub import hf_hub_download
|
| 1501 |
+
lora_path = hf_hub_download(repo_id=IMG2IMG_LORA_REPO,
|
| 1502 |
+
filename=IMG2IMG_LORA_FILE)
|
| 1503 |
+
except Exception as lexc: # noqa: BLE001
|
| 1504 |
+
print(f"[img2img] LoRA download skipped ({lexc}); using base model")
|
| 1505 |
+
lora_path = None
|
| 1506 |
+
_img2img_state["lora_path"] = lora_path
|
| 1507 |
+
_img2img_state["available"] = True
|
| 1508 |
+
print(f"[img2img] available ({IMG2IMG_MODEL_ID}; "
|
| 1509 |
+
f"LoRA={lora_path or 'none — base img2img (stage 1)'}); "
|
| 1510 |
+
f"pipeline will build on first use inside the GPU worker")
|
| 1511 |
+
except Exception as exc: # noqa: BLE001
|
| 1512 |
+
print(f"[img2img] unavailable, endpoint will report disabled: {exc}")
|
| 1513 |
+
_img2img_state["available"] = False
|
| 1514 |
+
finally:
|
| 1515 |
+
_img2img_state["loaded"] = True
|
| 1516 |
+
return _img2img_state["available"]
|
| 1517 |
+
|
| 1518 |
+
|
| 1519 |
+
_img2img_build_lock = threading.Lock()
|
| 1520 |
+
|
| 1521 |
+
|
| 1522 |
+
def _ensure_img2img_pipe():
|
| 1523 |
+
"""Build (once) and return the Flux2KleinPipeline on the GPU, applying the
|
| 1524 |
+
watercolour LoRA only if one was resolved (stage 2); otherwise it's plain
|
| 1525 |
+
base img2img (stage 1). MUST be called from inside a @spaces.GPU context on
|
| 1526 |
+
ZeroGPU so the CUDA context is created with a GPU attached. Idempotent +
|
| 1527 |
+
thread-safe; caches into _img2img_state. Returns the pipe, or None on failure
|
| 1528 |
+
(caller falls back)."""
|
| 1529 |
+
pipe = _img2img_state.get("pipe")
|
| 1530 |
+
if pipe is not None:
|
| 1531 |
+
return pipe
|
| 1532 |
+
with _img2img_build_lock:
|
| 1533 |
+
pipe = _img2img_state.get("pipe")
|
| 1534 |
+
if pipe is not None:
|
| 1535 |
+
return pipe
|
| 1536 |
+
try:
|
| 1537 |
+
import torch
|
| 1538 |
+
from diffusers import Flux2KleinPipeline
|
| 1539 |
+
try:
|
| 1540 |
+
from pillow_heif import register_heif_opener
|
| 1541 |
+
register_heif_opener() # so HEIC/HEIF phone photos open
|
| 1542 |
+
except Exception: # noqa: BLE001
|
| 1543 |
+
pass
|
| 1544 |
+
lora_path = _img2img_state.get("lora_path")
|
| 1545 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 1546 |
+
print(f"[img2img] building {IMG2IMG_MODEL_ID} on {device} "
|
| 1547 |
+
f"({'with LoRA' if lora_path else 'base, no LoRA'}) …")
|
| 1548 |
+
pipe = Flux2KleinPipeline.from_pretrained(
|
| 1549 |
+
IMG2IMG_MODEL_ID, torch_dtype=torch.bfloat16
|
| 1550 |
+
).to(device)
|
| 1551 |
+
if lora_path and os.path.isfile(lora_path):
|
| 1552 |
+
from pathlib import Path as _Path
|
| 1553 |
+
lf = _Path(lora_path)
|
| 1554 |
+
pipe.load_lora_weights(str(lf.parent), weight_name=lf.name,
|
| 1555 |
+
adapter_name=IMG2IMG_TRIGGER)
|
| 1556 |
+
pipe.set_adapters([IMG2IMG_TRIGGER], adapter_weights=[IMG2IMG_LORA_SCALE])
|
| 1557 |
+
_img2img_state["lora_loaded"] = True
|
| 1558 |
+
print("[img2img] pipeline ready (watercolour LoRA applied)")
|
| 1559 |
+
else:
|
| 1560 |
+
_img2img_state["lora_loaded"] = False
|
| 1561 |
+
print("[img2img] pipeline ready (base model — stage 1, no LoRA)")
|
| 1562 |
+
_img2img_state["pipe"] = pipe
|
| 1563 |
+
return pipe
|
| 1564 |
+
except Exception as exc: # noqa: BLE001
|
| 1565 |
+
print(f"[img2img] in-worker build failed: {exc}")
|
| 1566 |
+
return None
|
| 1567 |
+
|
| 1568 |
+
|
| 1569 |
+
def _img2img_square(img, size: int = IMG2IMG_SIZE):
|
| 1570 |
+
"""EXIF-normalise, flatten to RGB, and centre-crop a PIL image to size×size —
|
| 1571 |
+
the shape Klein expects (inlined from backend/hf/3_infer_img2img.py)."""
|
| 1572 |
+
from PIL import Image as PILImage, ImageOps
|
| 1573 |
+
src = ImageOps.exif_transpose(img).convert("RGB")
|
| 1574 |
+
w, h = src.size
|
| 1575 |
+
s = min(w, h)
|
| 1576 |
+
src = src.crop(((w - s) // 2, (h - s) // 2, (w + s) // 2, (h + s) // 2))
|
| 1577 |
+
return src.resize((size, size), PILImage.LANCZOS)
|
| 1578 |
+
|
| 1579 |
+
|
| 1580 |
+
@spaces.GPU(duration=120)
|
| 1581 |
+
def _img2img_infer(src):
|
| 1582 |
+
"""Develop one PIL photo into a watercolour PIL image, inside a @spaces.GPU
|
| 1583 |
+
context. Builds/reuses the pipeline here (never in the main process, never
|
| 1584 |
+
pickled across the boundary). `src` and the returned image are PIL (both
|
| 1585 |
+
picklable), the only things that cross the GPU boundary. Generous duration
|
| 1586 |
+
because the FIRST call also pays the ~14 GB cold load; later calls reuse the
|
| 1587 |
+
cached pipe and are fast."""
|
| 1588 |
+
import torch
|
| 1589 |
+
pipe = _ensure_img2img_pipe()
|
| 1590 |
+
if pipe is None:
|
| 1591 |
+
raise RuntimeError("img2img pipeline could not be initialized")
|
| 1592 |
+
sq = _img2img_square(src, size=IMG2IMG_SIZE)
|
| 1593 |
+
# Trigger-token prompt only makes sense once the LoRA is loaded; otherwise
|
| 1594 |
+
# use the generic base-model watercolour prompt (stage 1).
|
| 1595 |
+
prompt = IMG2IMG_PROMPT if _img2img_state.get("lora_loaded") else IMG2IMG_BASE_PROMPT
|
| 1596 |
+
device = getattr(pipe, "device", None)
|
| 1597 |
+
generator = torch.Generator(
|
| 1598 |
+
str(device) if device is not None else "cuda"
|
| 1599 |
+
).manual_seed(IMG2IMG_SEED)
|
| 1600 |
+
result = pipe(
|
| 1601 |
+
prompt=prompt,
|
| 1602 |
+
image=sq,
|
| 1603 |
+
height=IMG2IMG_SIZE,
|
| 1604 |
+
width=IMG2IMG_SIZE,
|
| 1605 |
+
num_inference_steps=IMG2IMG_STEPS,
|
| 1606 |
+
guidance_scale=IMG2IMG_GUIDANCE,
|
| 1607 |
+
generator=generator,
|
| 1608 |
+
)
|
| 1609 |
+
return result.images[0]
|
| 1610 |
+
|
| 1611 |
+
|
| 1612 |
+
def _pil_to_data_url(img, fmt: str = "PNG") -> str:
|
| 1613 |
+
import base64
|
| 1614 |
+
buf = io.BytesIO()
|
| 1615 |
+
img.save(buf, format=fmt)
|
| 1616 |
+
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
|
| 1617 |
+
return f"data:image/{fmt.lower()};base64,{b64}"
|
| 1618 |
+
|
| 1619 |
+
|
| 1620 |
+
@app.post("/api/img2img")
|
| 1621 |
+
async def img2img_rest(request: Request):
|
| 1622 |
+
"""Develop one uploaded photo into a watercolour painting.
|
| 1623 |
+
Body: {data_url: "data:image/...;base64,...", name?: str}.
|
| 1624 |
+
Returns {available, image: <png data url>, model}. When the pipeline can't
|
| 1625 |
+
run on this host it returns {available: false, image: null} so the caller can
|
| 1626 |
+
keep the original photo rather than treating it as an error."""
|
| 1627 |
+
import base64
|
| 1628 |
+
if not _img2img_prep():
|
| 1629 |
+
return {"available": False, "image": None, "model": IMG2IMG_MODEL_ID}
|
| 1630 |
+
try:
|
| 1631 |
+
body = await request.json()
|
| 1632 |
+
except Exception: # noqa: BLE001
|
| 1633 |
+
raise HTTPException(status_code=400, detail="invalid JSON body")
|
| 1634 |
+
data_url = (body or {}).get("data_url", "")
|
| 1635 |
+
name = str((body or {}).get("name", "upload"))[:60]
|
| 1636 |
+
if not data_url:
|
| 1637 |
+
raise HTTPException(status_code=400, detail="missing data_url")
|
| 1638 |
+
try:
|
| 1639 |
+
b64 = data_url.split(",", 1)[1] if "," in data_url else data_url
|
| 1640 |
+
raw = base64.b64decode(b64)
|
| 1641 |
+
from PIL import Image
|
| 1642 |
+
src = Image.open(io.BytesIO(raw))
|
| 1643 |
+
with traced("img2img", model=IMG2IMG_MODEL_ID, file=name) as t:
|
| 1644 |
+
out = _img2img_infer(src)
|
| 1645 |
+
t.set_output({"size": list(out.size)})
|
| 1646 |
+
return {"available": True, "image": _pil_to_data_url(out), "model": IMG2IMG_MODEL_ID}
|
| 1647 |
+
except HTTPException:
|
| 1648 |
+
raise
|
| 1649 |
+
except Exception as exc: # noqa: BLE001
|
| 1650 |
+
print(f"[img2img] generation failed: {exc}")
|
| 1651 |
+
raise HTTPException(status_code=500, detail=str(exc))
|
| 1652 |
+
|
| 1653 |
+
|
| 1654 |
# --------------------------------------------------------------------------- #
|
| 1655 |
# Voice → text via NVIDIA NeMo ASR (item 5). Streaming Nemotron model; needs a
|
| 1656 |
# GPU to be practical. Loaded lazily; if NeMo/torch/GPU are missing the endpoint
|
packages.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
libheif-dev
|
requirements.txt
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
gradio
|
| 2 |
uvicorn[standard]
|
| 3 |
transformers
|
| 4 |
-
diffusers
|
|
|
|
|
|
|
| 5 |
accelerate
|
|
|
|
| 6 |
hf_xet
|
| 7 |
huggingface_hub
|
| 8 |
soundfile>=0.12
|
|
|
|
| 1 |
gradio
|
| 2 |
uvicorn[standard]
|
| 3 |
transformers
|
| 4 |
+
# Flux2KleinPipeline (img2img watercolour develop) is not in a released diffusers
|
| 5 |
+
# yet — it must come from git main. See backend/hf/3_infer_img2img.py.
|
| 6 |
+
diffusers @ git+https://github.com/huggingface/diffusers.git
|
| 7 |
accelerate
|
| 8 |
+
pillow-heif # HEIC/HEIF source photos for the img2img develop step
|
| 9 |
hf_xet
|
| 10 |
huggingface_hub
|
| 11 |
soundfile>=0.12
|