|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
import math |
|
|
import re |
|
|
import os |
|
|
import io |
|
|
from math import gcd, lcm |
|
|
from typing import List, Tuple, Literal, Dict, Optional |
|
|
|
|
|
import gradio as gr |
|
|
from PIL import Image |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RoundingMode = Literal["round", "floor", "ceil"] |
|
|
ModelFamily = Literal["SDXL (native~1024)", "SD 1.x (native~512)", "SD 2.x (native~768)"] |
|
|
|
|
|
DEFAULT_MODEL_FAMILY: ModelFamily = "SDXL (native~1024)" |
|
|
|
|
|
def ensure_multiple(value: int, multiple: int, mode: RoundingMode = "round") -> int: |
|
|
if multiple <= 1: |
|
|
return max(1, value) |
|
|
q, r = divmod(value, multiple) |
|
|
if r == 0: |
|
|
return max(multiple, value) |
|
|
if mode == "floor": |
|
|
return max(multiple, q * multiple) |
|
|
if mode == "ceil": |
|
|
return (q + 1) * multiple |
|
|
lower = q * multiple |
|
|
upper = (q + 1) * multiple |
|
|
return upper if (value - lower) >= (upper - value) else lower |
|
|
|
|
|
def simplified_ratio(width: int, height: int) -> str: |
|
|
if width <= 0 or height <= 0: |
|
|
return "—" |
|
|
g = gcd(width, height) |
|
|
return f"{width // g}:{height // g}" |
|
|
|
|
|
def megapixels(width: int, height: int) -> float: |
|
|
return round((width * height) / 1_000_000.0, 3) |
|
|
|
|
|
def parse_dims(text: str) -> List[Tuple[int, int]]: |
|
|
"""Вытягивает все пары 'WxH' из произвольного текста.""" |
|
|
dims: List[Tuple[int, int]] = [] |
|
|
for w, h in re.findall(r"(\d{3,4})x(\d{3,4})", text): |
|
|
dims.append((int(w), int(h))) |
|
|
|
|
|
seen = set() |
|
|
uniq: List[Tuple[int, int]] = [] |
|
|
for d in dims: |
|
|
if d not in seen: |
|
|
seen.add(d) |
|
|
uniq.append(d) |
|
|
return uniq |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pick_base_multiple_by_model(model_family: ModelFamily, target_max_side: Optional[int] = None) -> int: |
|
|
""" |
|
|
Эвристика: |
|
|
- SDXL: кратность 64 (родной 1024, датасеты шагом 64). |
|
|
- SD 2.x: минимум 16..32, чаще 32 в HD. |
|
|
- SD 1.x: минимум 8..16, чаще 32 в HD. |
|
|
""" |
|
|
if model_family == "SDXL (native~1024)": |
|
|
base = 64 |
|
|
elif model_family == "SD 2.x (native~768)": |
|
|
base = 32 |
|
|
else: |
|
|
base = 32 |
|
|
if target_max_side and target_max_side >= 1536: |
|
|
base = max(base, 64) |
|
|
return base |
|
|
|
|
|
def combine_multiples(base_multiple: int, diffusion_tile: int = 0, vae_tile: int = 0) -> int: |
|
|
""" |
|
|
Объединяем ограничения: НОК(base_multiple, diffusion_tile, vae_tile). |
|
|
Нули игнорируются. Жёстко ограничим верх до 256. |
|
|
""" |
|
|
m = base_multiple |
|
|
for x in (diffusion_tile, vae_tile): |
|
|
if isinstance(x, int) and x >= 8: |
|
|
m = lcm(m, x) |
|
|
return max(8, min(m, 256)) |
|
|
|
|
|
def explain_auto_multiple(model_family: ModelFamily, max_side: Optional[int], m_base: int, m_final: int, |
|
|
diffusion_tile: int, vae_tile: int) -> str: |
|
|
reasons = [f"модель: {model_family} -> base={m_base}"] |
|
|
if max_side: |
|
|
reasons.append(f"размер цели: ~{max_side}px") |
|
|
if diffusion_tile: |
|
|
reasons.append(f"diffusion tile={diffusion_tile}") |
|
|
if vae_tile: |
|
|
reasons.append(f"VAE tile={vae_tile}") |
|
|
if m_final != m_base: |
|
|
reasons.append(f"НОК -> {m_final}") |
|
|
return " | ".join(reasons) |
|
|
|
|
|
def autopick_multiple(model_family: ModelFamily, |
|
|
diffusion_tile: int, |
|
|
vae_tile: int, |
|
|
width_hint: Optional[int], |
|
|
height_hint: Optional[int]) -> Tuple[int, str]: |
|
|
max_side = None |
|
|
if width_hint and height_hint: |
|
|
max_side = max(width_hint, height_hint) |
|
|
m_base = pick_base_multiple_by_model(model_family, max_side) |
|
|
m_final = combine_multiples(m_base, diffusion_tile, vae_tile) |
|
|
return m_final, explain_auto_multiple(model_family, max_side, m_base, m_final, diffusion_tile, vae_tile) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resize_by_shorter_side(image: Image.Image, target_shorter: int, multiple: int, mode: RoundingMode) -> Tuple[int, int]: |
|
|
w, h = image.size |
|
|
if w <= 0 or h <= 0 or target_shorter <= 0: |
|
|
return w, h |
|
|
scale = target_shorter / min(w, h) |
|
|
new_w = ensure_multiple(int(round(w * scale)), multiple, mode) |
|
|
new_h = ensure_multiple(int(round(h * scale)), multiple, mode) |
|
|
return new_w, new_h |
|
|
|
|
|
def resize_to_pixel_count(image: Image.Image, target_pixels: int, multiple: int, mode: RoundingMode) -> Tuple[int, int]: |
|
|
w, h = image.size |
|
|
if w <= 0 or h <= 0 or target_pixels <= 0: |
|
|
return w, h |
|
|
scale = math.sqrt(target_pixels / (w * h)) |
|
|
new_w = ensure_multiple(int(round(w * scale)), multiple, mode) |
|
|
new_h = ensure_multiple(int(round(h * scale)), multiple, mode) |
|
|
return new_w, new_h |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_catalog_from_file(path: str) -> List[Tuple[int, int]]: |
|
|
if not os.path.isfile(path): |
|
|
return [] |
|
|
try: |
|
|
with open(path, "r", encoding="utf-8", errors="ignore") as f: |
|
|
text = f.read() |
|
|
return parse_dims(text) |
|
|
except Exception: |
|
|
return [] |
|
|
|
|
|
_GRID_CACHE: Dict[Tuple[int, int, int], List[Tuple[int, int]]] = {} |
|
|
|
|
|
def generate_grid_catalog(min_size: int, max_size: int, step: int) -> List[Tuple[int, int]]: |
|
|
"""Генерация полной решётки кратных 'step' размеров в диапазоне [min_size, max_size], с кэшем.""" |
|
|
key = (min_size, max_size, step) |
|
|
cached = _GRID_CACHE.get(key) |
|
|
if cached is not None: |
|
|
return cached |
|
|
sizes = list(range(min_size, max_size + 1, step)) |
|
|
out: List[Tuple[int, int]] = [] |
|
|
for w in sizes: |
|
|
for h in sizes: |
|
|
out.append((w, h)) |
|
|
_GRID_CACHE[key] = out |
|
|
return out |
|
|
|
|
|
def orientation_of(w: int, h: int) -> str: |
|
|
if w == h: |
|
|
return "square" |
|
|
return "landscape" if w > h else "portrait" |
|
|
|
|
|
def aspect_tuple(w: int, h: int) -> Tuple[int, int]: |
|
|
g = gcd(w, h) |
|
|
return (w // g, h // g) |
|
|
|
|
|
def _parse_aspect_tokens(aspect_str: str) -> List[Tuple[int, int, bool, float]]: |
|
|
""" |
|
|
Поддерживаем токены: |
|
|
- 'A:B' → точное совпадение |
|
|
- '~A:B' → приблизительное (±0.02 по умолчанию) |
|
|
- 'A:B@0.03' → точный A:B, но с допуском 0.03 (редкий кейс) |
|
|
- '~A:B@0.015' → приблизительный матч с явным допуском |
|
|
Возвращает список (ax, ay, approx, tol). |
|
|
""" |
|
|
out: List[Tuple[int, int, bool, float]] = [] |
|
|
if not aspect_str.strip(): |
|
|
return out |
|
|
tokens = [t.strip() for t in re.split(r"[;,]+", aspect_str) if t.strip()] |
|
|
for t in tokens: |
|
|
approx = t.startswith("~") |
|
|
if approx: |
|
|
t = t[1:].strip() |
|
|
tol = 0.02 if approx else 0.0 |
|
|
m = re.match(r"(\d+)\s*:\s*(\d+)(?:\s*@\s*(0\.\d+))?$", t) |
|
|
if not m: |
|
|
|
|
|
continue |
|
|
ax, ay = int(m.group(1)), int(m.group(2)) |
|
|
if m.group(3): |
|
|
tol = float(m.group(3)) |
|
|
out.append((ax, ay, approx or tol > 0.0, tol)) |
|
|
return out |
|
|
|
|
|
def filter_catalog(catalog: List[Tuple[int, int]], |
|
|
min_width: int, max_width: int, |
|
|
min_height: int, max_height: int, |
|
|
min_mp: float, max_mp: float, |
|
|
multiple: int, |
|
|
orientation: str, |
|
|
aspect_str: str, |
|
|
limit: int, |
|
|
sort_key: str, |
|
|
sort_desc: bool) -> List[Dict]: |
|
|
aspect_tokens = _parse_aspect_tokens(aspect_str) |
|
|
|
|
|
rows: List[Dict] = [] |
|
|
for (w, h) in catalog: |
|
|
if w < min_width or w > max_width: |
|
|
continue |
|
|
if h < min_height or h > max_height: |
|
|
continue |
|
|
if multiple > 1 and (w % multiple != 0 or h % multiple != 0): |
|
|
continue |
|
|
mp = (w * h) / 1_000_000.0 |
|
|
if mp < min_mp or mp > max_mp: |
|
|
continue |
|
|
ori = orientation_of(w, h) |
|
|
if orientation != "any" and ori != orientation: |
|
|
continue |
|
|
|
|
|
|
|
|
if aspect_tokens: |
|
|
w0, h0 = aspect_tuple(w, h) |
|
|
ok = False |
|
|
for ax, ay, approx, tol in aspect_tokens: |
|
|
if approx: |
|
|
if abs((w / h) - (ax / ay)) <= (tol if tol > 0 else 0.02): |
|
|
ok = True |
|
|
break |
|
|
else: |
|
|
if (w0, h0) == (ax, ay): |
|
|
ok = True |
|
|
break |
|
|
if not ok: |
|
|
continue |
|
|
|
|
|
rows.append({ |
|
|
"Width": w, |
|
|
"Height": h, |
|
|
"Aspect": f"{aspect_tuple(w,h)[0]}:{aspect_tuple(w,h)[1]}", |
|
|
"Orientation": ori, |
|
|
"MP": round(mp, 3) |
|
|
}) |
|
|
|
|
|
|
|
|
key = { |
|
|
"MP": lambda r: r["MP"], |
|
|
"Width": lambda r: r["Width"], |
|
|
"Height": lambda r: r["Height"], |
|
|
}.get(sort_key, lambda r: (r["MP"], r["Width"], r["Height"])) |
|
|
rows.sort(key=key, reverse=sort_desc) |
|
|
|
|
|
return rows[:limit] |
|
|
|
|
|
def stringify_wh_list(rows: List[Dict]) -> str: |
|
|
return "\n".join(f"{r['Width']}x{r['Height']}" for r in rows) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_SHORT_PRESETS = ["512", "768", "896"] |
|
|
DEFAULT_PIXEL_BASES = ["512", "640", "768", "832", "896", "1024", "1152", "1216", "1344", "1536"] |
|
|
|
|
|
ROUNDING_CHOICES: List[RoundingMode] = ["round", "floor", "ceil"] |
|
|
|
|
|
def _parse_int_list(str_list: List[str]) -> List[int]: |
|
|
out: List[int] = [] |
|
|
for s in str_list or []: |
|
|
m = re.search(r"\d+", s) |
|
|
if m: |
|
|
out.append(int(m.group(0))) |
|
|
return out |
|
|
|
|
|
|
|
|
|
|
|
def calculate_all( |
|
|
image: Image.Image, |
|
|
shorters_raw: List[str], |
|
|
bases_raw: List[str], |
|
|
use_autopick: bool, |
|
|
model_family: ModelFamily, |
|
|
diffusion_tile: int, |
|
|
vae_tile: int, |
|
|
manual_multiple: int, |
|
|
rounding_mode: RoundingMode, |
|
|
): |
|
|
if image is None: |
|
|
raise gr.Error("Загрузите изображение слева.") |
|
|
|
|
|
w, h = image.size |
|
|
|
|
|
if use_autopick: |
|
|
m_auto, reason = autopick_multiple(model_family, diffusion_tile, vae_tile, w, h) |
|
|
rounding_multiple = m_auto |
|
|
reason_text = f"Auto multiple = **{m_auto}** — {reason}" |
|
|
else: |
|
|
rounding_multiple = manual_multiple |
|
|
reason_text = f"Manual multiple = **{manual_multiple}**" |
|
|
|
|
|
shorters = _parse_int_list(shorters_raw) or _parse_int_list(DEFAULT_SHORT_PRESETS) |
|
|
bases = _parse_int_list(bases_raw) or _parse_int_list(DEFAULT_PIXEL_BASES) |
|
|
|
|
|
rows = [] |
|
|
|
|
|
|
|
|
for s in shorters: |
|
|
nw, nh = resize_by_shorter_side(image, s, rounding_multiple, rounding_mode) |
|
|
rows.append({ |
|
|
"Preset": f"short={s}", |
|
|
"Width": nw, |
|
|
"Height": nh, |
|
|
"Aspect": simplified_ratio(nw, nh), |
|
|
"Pixels": nw * nh, |
|
|
"MP": megapixels(nw, nh), |
|
|
}) |
|
|
|
|
|
|
|
|
for base in bases: |
|
|
target_pixels = base * base |
|
|
nw, nh = resize_to_pixel_count(image, target_pixels, rounding_multiple, rounding_mode) |
|
|
rows.append({ |
|
|
"Preset": f"{base}² ({target_pixels:,})", |
|
|
"Width": nw, |
|
|
"Height": nh, |
|
|
"Aspect": simplified_ratio(nw, nh), |
|
|
"Pixels": nw * nh, |
|
|
"MP": megapixels(nw, nh), |
|
|
}) |
|
|
|
|
|
original_info = f"Original: {w}x{h} | Aspect {simplified_ratio(w, h)} | {megapixels(w, h)} MP" |
|
|
return rows, stringify_wh_list(rows), original_info, reason_text |
|
|
|
|
|
|
|
|
|
|
|
CATALOG_PATH = os.path.join(os.getcwd(), "sdxl_resolutions_by_aspect.txt") |
|
|
_LOADED_CATALOG: Optional[List[Tuple[int, int]]] = None |
|
|
|
|
|
def _load_or_generate(default_step: int) -> Tuple[List[Tuple[int, int]], str]: |
|
|
global _LOADED_CATALOG |
|
|
if _LOADED_CATALOG is None: |
|
|
candidates = [ |
|
|
CATALOG_PATH, |
|
|
os.path.join(os.getcwd(), "data", "sdxl_resolutions_by_aspect.txt"), |
|
|
"/mnt/data/sdxl_resolutions_by_aspect.txt", |
|
|
] |
|
|
for p in candidates: |
|
|
cat = load_catalog_from_file(p) |
|
|
if cat: |
|
|
_LOADED_CATALOG = cat |
|
|
return _LOADED_CATALOG, f"Catalog: loaded from file ({len(cat)} items)." |
|
|
_LOADED_CATALOG = [] |
|
|
return _LOADED_CATALOG, f"Catalog: no file, you can GENERATE grid (step {default_step})." |
|
|
|
|
|
def catalog_filter_action( |
|
|
use_file_catalog: bool, |
|
|
generate_min: int, |
|
|
generate_max: int, |
|
|
generate_step: int, |
|
|
filter_min_w: int, |
|
|
filter_max_w: int, |
|
|
filter_min_h: int, |
|
|
filter_max_h: int, |
|
|
filter_min_mp: float, |
|
|
filter_max_mp: float, |
|
|
filter_multiple: int, |
|
|
filter_orientation: str, |
|
|
filter_aspects: str, |
|
|
filter_limit: int, |
|
|
sort_key: str, |
|
|
sort_desc: bool, |
|
|
): |
|
|
base_list, inf = _load_or_generate(generate_step) |
|
|
if use_file_catalog and base_list: |
|
|
catalog = base_list |
|
|
source = inf.replace("Catalog:", "Source:") |
|
|
else: |
|
|
|
|
|
generate_min = max(128, generate_min) |
|
|
generate_max = min(4096, max(generate_min, generate_max)) |
|
|
generate_step = max(8, generate_step) |
|
|
sizes = list(range(generate_min, generate_max + 1, generate_step)) |
|
|
est = len(sizes) ** 2 |
|
|
if est > 20000: |
|
|
|
|
|
mid = (generate_min + generate_max) // 2 |
|
|
half = max(generate_step * 5, (generate_max - generate_min) // 6) |
|
|
generate_min = max(128, mid - half) |
|
|
generate_max = min(4096, mid + half) |
|
|
catalog = generate_grid_catalog(generate_min, generate_max, generate_step) |
|
|
source = f"Source: generated grid {generate_min}..{generate_max} step={generate_step} ({len(catalog)} combos)" |
|
|
|
|
|
rows = filter_catalog( |
|
|
catalog=catalog, |
|
|
min_width=filter_min_w, max_width=filter_max_w, |
|
|
min_height=filter_min_h, max_height=filter_max_h, |
|
|
min_mp=filter_min_mp, max_mp=filter_max_mp, |
|
|
multiple=filter_multiple, |
|
|
orientation=filter_orientation, |
|
|
aspect_str=filter_aspects, |
|
|
limit=filter_limit, |
|
|
sort_key=sort_key, |
|
|
sort_desc=sort_desc, |
|
|
) |
|
|
return rows, stringify_wh_list(rows), source |
|
|
|
|
|
def _rows_to_csv_bytes(rows: List[Dict]) -> bytes: |
|
|
import csv |
|
|
from io import StringIO |
|
|
buf = StringIO() |
|
|
w = csv.writer(buf) |
|
|
w.writerow(["Width", "Height", "Aspect", "Orientation", "MP"]) |
|
|
for r in rows: |
|
|
w.writerow([r["Width"], r["Height"], r["Aspect"], r["Orientation"], r["MP"]]) |
|
|
return buf.getvalue().encode("utf-8") |
|
|
|
|
|
def _rows_to_json_bytes(rows: List[Dict]) -> bytes: |
|
|
import json |
|
|
return json.dumps(rows, ensure_ascii=False, indent=2).encode("utf-8") |
|
|
|
|
|
def export_rows(rows: List[Dict], kind: str): |
|
|
"""Возвращает tuple(filename, bytes) для gr.File()""" |
|
|
if not rows: |
|
|
raise gr.Error("Нет данных для экспорта — сначала примените фильтр.") |
|
|
if kind == "csv": |
|
|
data = _rows_to_csv_bytes(rows) |
|
|
name = "filtered_catalog.csv" |
|
|
else: |
|
|
data = _rows_to_json_bytes(rows) |
|
|
name = "filtered_catalog.json" |
|
|
path = os.path.join(os.getcwd(), name) |
|
|
with open(path, "wb") as f: |
|
|
f.write(data) |
|
|
return path |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_ui_tab_called(): |
|
|
with gr.Blocks() as ui: |
|
|
gr.Markdown("### 📐 Image Ratio & Resolution Catalog Pro \n" |
|
|
"Используйте файл SDXL пресетов, либо генерируйте сетку. " |
|
|
"Аспекты поддерживают токены `16:9, ~3:2, 21:9@0.03, ~9:16@0.015`.") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
image = gr.Image(type="pil", source="upload", label="Изображение") |
|
|
original_info = gr.Markdown("") |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
|
|
|
use_autopick = gr.Checkbox(value=True, label="Auto multiple (рекомендуется)") |
|
|
model_family = gr.Radio( |
|
|
choices=["SDXL (native~1024)", "SD 1.x (native~512)", "SD 2.x (native~768)"], |
|
|
value=DEFAULT_MODEL_FAMILY, |
|
|
label="Model family (ручной выбор)" |
|
|
) |
|
|
with gr.Row(): |
|
|
diffusion_tile = gr.Number(value=0, precision=0, label="Diffusion tile (px, 0=нет)") |
|
|
vae_tile = gr.Number(value=0, precision=0, label="VAE tile (px, 0=нет)") |
|
|
manual_multiple = gr.Slider(minimum=8, maximum=256, step=8, value=64, label="Manual multiple") |
|
|
rounding_mode = gr.Radio(choices=["round", "floor", "ceil"], value="round", label="Режим округления") |
|
|
auto_reason = gr.Markdown("") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(): |
|
|
shorters = gr.CheckboxGroup( |
|
|
choices=DEFAULT_SHORT_PRESETS, |
|
|
value=DEFAULT_SHORT_PRESETS, |
|
|
label="Цели по короткой стороне (px)", |
|
|
) |
|
|
pixel_bases = gr.CheckboxGroup( |
|
|
choices=DEFAULT_PIXEL_BASES, |
|
|
value=DEFAULT_PIXEL_BASES, |
|
|
label="Цели по общему числу пикселей (берётся N²)", |
|
|
) |
|
|
run_btn = gr.Button("Рассчитать", variant="primary") |
|
|
|
|
|
with gr.Column(): |
|
|
results = gr.Dataframe( |
|
|
headers=["Preset", "Width", "Height", "Aspect", "Pixels", "MP"], |
|
|
datatype=["str", "number", "number", "str", "number", "number"], |
|
|
label="Результаты", |
|
|
interactive=False, |
|
|
wrap=True, |
|
|
overflow_row_behaviour="paginate", |
|
|
) |
|
|
copy_box = gr.Textbox(label="Список WxH (для копирования)", lines=8, interactive=False) |
|
|
|
|
|
|
|
|
gr.Markdown("---") |
|
|
gr.Markdown("#### Каталог разрешений (импорт из файла или генерация сетки)") |
|
|
|
|
|
with gr.Row(): |
|
|
use_file_catalog = gr.Checkbox(value=True, label="Использовать файл, если найден (SDXL пресеты)") |
|
|
generate_min = gr.Slider(minimum=128, maximum=4096, step=8, value=512, label="Generate: min") |
|
|
generate_max = gr.Slider(minimum=128, maximum=4096, step=8, value=2048, label="Generate: max") |
|
|
generate_step = gr.Slider(minimum=8, maximum=256, step=8, value=64, label="Generate: step") |
|
|
|
|
|
with gr.Row(): |
|
|
filter_min_w = gr.Slider(minimum=128, maximum=4096, step=8, value=512, label="min Width") |
|
|
filter_max_w = gr.Slider(minimum=128, maximum=4096, step=8, value=2048, label="max Width") |
|
|
filter_min_h = gr.Slider(minimum=128, maximum=4096, step=8, value=512, label="min Height") |
|
|
filter_max_h = gr.Slider(minimum=128, maximum=4096, step=8, value=2048, label="max Height") |
|
|
|
|
|
with gr.Row(): |
|
|
filter_min_mp = gr.Number(value=0.0, label="min MP") |
|
|
filter_max_mp = gr.Number(value=8.0, label="max MP") |
|
|
filter_multiple = gr.Slider(minimum=8, maximum=256, step=8, value=64, label="Требуемая кратность") |
|
|
filter_orientation = gr.Dropdown(choices=["any", "square", "landscape", "portrait"], value="any", label="Ориентация") |
|
|
filter_aspects = gr.Textbox(value="", label="Аспекты (пример: '16:9, ~3:2, 21:9@0.03')") |
|
|
|
|
|
with gr.Row(): |
|
|
filter_limit = gr.Slider(minimum=10, maximum=5000, step=10, value=500, label="Лимит результатов") |
|
|
sort_key = gr.Dropdown(choices=["MP", "Width", "Height"], value="MP", label="Сортировать по") |
|
|
sort_desc = gr.Checkbox(value=True, label="По убыванию") |
|
|
apply_filters = gr.Button("Применить фильтры", variant="primary") |
|
|
|
|
|
catalog_df = gr.Dataframe( |
|
|
headers=["Width", "Height", "Aspect", "Orientation", "MP"], |
|
|
datatype=["number", "number", "str", "str", "number"], |
|
|
label="Каталог (фильтрованный)", |
|
|
interactive=False, |
|
|
wrap=True, |
|
|
overflow_row_behaviour="paginate", |
|
|
) |
|
|
catalog_copy = gr.Textbox(label="Список WxH (для копирования)", lines=12, interactive=False) |
|
|
catalog_source = gr.Markdown("") |
|
|
|
|
|
with gr.Row(): |
|
|
export_csv_btn = gr.Button("Скачать CSV") |
|
|
export_json_btn = gr.Button("Скачать JSON") |
|
|
exported_file = gr.File(label="Экспорт", visible=True) |
|
|
|
|
|
|
|
|
def _toggle_manual(m_use_auto: bool): |
|
|
return gr.update(interactive=not m_use_auto) |
|
|
|
|
|
use_autopick.change(fn=_toggle_manual, inputs=[use_autopick], outputs=[manual_multiple]) |
|
|
|
|
|
for trigger in (image.upload, run_btn.click): |
|
|
trigger( |
|
|
fn=calculate_all, |
|
|
inputs=[image, shorters, pixel_bases, use_autopick, model_family, diffusion_tile, vae_tile, manual_multiple, rounding_mode], |
|
|
outputs=[results, copy_box, original_info, auto_reason], |
|
|
show_progress=False, |
|
|
) |
|
|
|
|
|
apply_filters.click( |
|
|
fn=catalog_filter_action, |
|
|
inputs=[ |
|
|
use_file_catalog, generate_min, generate_max, generate_step, |
|
|
filter_min_w, filter_max_w, filter_min_h, filter_max_h, |
|
|
filter_min_mp, filter_max_mp, filter_multiple, |
|
|
filter_orientation, filter_aspects, filter_limit, |
|
|
sort_key, sort_desc |
|
|
], |
|
|
outputs=[catalog_df, catalog_copy, catalog_source], |
|
|
show_progress=False |
|
|
) |
|
|
|
|
|
export_csv_btn.click(lambda rows: export_rows(rows, "csv"), inputs=[catalog_df], outputs=[exported_file]) |
|
|
export_json_btn.click(lambda rows: export_rows(rows, "json"), inputs=[catalog_df], outputs=[exported_file]) |
|
|
|
|
|
return (ui, "calculator+", "calculator_plus_interface"), |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
from modules import script_callbacks |
|
|
script_callbacks.on_ui_tabs(on_ui_tab_called) |
|
|
except (ImportError, ModuleNotFoundError): |
|
|
if __name__ == "__main__": |
|
|
interface, _, _ = on_ui_tab_called()[0] |
|
|
interface.launch() |
|
|
|