Spaces:
Runtime error
Runtime error
feat: build Qwen Image Editor app (Edit/Compose tabs, Fast/Quality, ZeroGPU + local CUDA) with tests
d713f9b | """Tests for models.py — constants, env helpers, fit_dimensions, scheduler config.""" | |
| from __future__ import annotations | |
| import importlib.util | |
| import math | |
| import pytest | |
| from PIL import Image | |
| import models | |
| # --------------------------------------------------------------------------- | |
| # Constants | |
| # --------------------------------------------------------------------------- | |
| def test_model_id(): | |
| assert models.MODEL_ID == "Qwen/Qwen-Image-Edit-2511" | |
| def test_lora_repo(): | |
| assert models.LORA_REPO == "lightx2v/Qwen-Image-Edit-2511-Lightning" | |
| def test_lora_file(): | |
| assert models.LORA_FILE == "Qwen-Image-Edit-2511-Lightning-4steps-V1.0-bf16.safetensors" | |
| def test_lora_adapter_name(): | |
| assert models.LORA_ADAPTER_NAME == "lightning" | |
| # --------------------------------------------------------------------------- | |
| # LIGHTNING_SCHEDULER_CONFIG | |
| # --------------------------------------------------------------------------- | |
| def test_scheduler_config_time_shift_type(): | |
| assert models.LIGHTNING_SCHEDULER_CONFIG["time_shift_type"] == "exponential" | |
| def test_scheduler_config_base_shift(): | |
| assert models.LIGHTNING_SCHEDULER_CONFIG["base_shift"] == math.log(3) | |
| def test_scheduler_config_max_shift(): | |
| assert models.LIGHTNING_SCHEDULER_CONFIG["max_shift"] == math.log(3) | |
| def test_scheduler_config_use_dynamic_shifting(): | |
| assert models.LIGHTNING_SCHEDULER_CONFIG["use_dynamic_shifting"] is True | |
| def test_scheduler_config_has_required_keys(): | |
| required = { | |
| "base_image_seq_len", | |
| "base_shift", | |
| "invert_sigmas", | |
| "max_image_seq_len", | |
| "max_shift", | |
| "num_train_timesteps", | |
| "shift", | |
| "shift_terminal", | |
| "stochastic_sampling", | |
| "time_shift_type", | |
| "use_beta_sigmas", | |
| "use_dynamic_shifting", | |
| "use_exponential_sigmas", | |
| "use_karras_sigmas", | |
| } | |
| assert required.issubset(models.LIGHTNING_SCHEDULER_CONFIG.keys()) | |
| # --------------------------------------------------------------------------- | |
| # on_spaces | |
| # --------------------------------------------------------------------------- | |
| def test_on_spaces_false_when_env_absent(monkeypatch): | |
| monkeypatch.delenv("SPACES_ZERO_GPU", raising=False) | |
| assert models.on_spaces() is False | |
| def test_on_spaces_true_when_env_set(monkeypatch): | |
| monkeypatch.setenv("SPACES_ZERO_GPU", "1") | |
| assert models.on_spaces() is True | |
| def test_on_spaces_true_for_nonempty_string(monkeypatch): | |
| monkeypatch.setenv("SPACES_ZERO_GPU", "true") | |
| assert models.on_spaces() is True | |
| def test_on_spaces_false_for_empty_string(monkeypatch): | |
| monkeypatch.setenv("SPACES_ZERO_GPU", "") | |
| assert models.on_spaces() is False | |
| # --------------------------------------------------------------------------- | |
| # fit_dimensions helpers | |
| # --------------------------------------------------------------------------- | |
| def _img(w: int, h: int) -> Image.Image: | |
| return Image.new("RGB", (w, h)) | |
| # --- multiples of 16 --- | |
| def test_fit_dimensions_landscape_multiple_of_16(): | |
| w, h = models.fit_dimensions(_img(1920, 1080)) | |
| assert w % 16 == 0 | |
| assert h % 16 == 0 | |
| def test_fit_dimensions_portrait_multiple_of_16(): | |
| w, h = models.fit_dimensions(_img(1080, 1920)) | |
| assert w % 16 == 0 | |
| assert h % 16 == 0 | |
| def test_fit_dimensions_square_multiple_of_16(): | |
| w, h = models.fit_dimensions(_img(2000, 2000)) | |
| assert w % 16 == 0 | |
| assert h % 16 == 0 | |
| # --- area cap --- | |
| def test_fit_dimensions_area_cap_landscape(): | |
| w, h = models.fit_dimensions(_img(1920, 1080)) | |
| assert w * h <= 1024 * 1024 | |
| def test_fit_dimensions_area_cap_portrait(): | |
| w, h = models.fit_dimensions(_img(1080, 1920)) | |
| assert w * h <= 1024 * 1024 | |
| def test_fit_dimensions_area_cap_large_square(): | |
| w, h = models.fit_dimensions(_img(2048, 2048)) | |
| assert w * h <= 1024 * 1024 | |
| def test_fit_dimensions_custom_max_pixels(): | |
| w, h = models.fit_dimensions(_img(1024, 1024), max_pixels=512 * 512) | |
| assert w * h <= 512 * 512 | |
| # --- minimum 256 --- | |
| def test_fit_dimensions_min_256_tiny_image(): | |
| w, h = models.fit_dimensions(_img(32, 32)) | |
| assert w >= 256 | |
| assert h >= 256 | |
| def test_fit_dimensions_min_256_small_image(): | |
| w, h = models.fit_dimensions(_img(100, 100)) | |
| assert w >= 256 | |
| assert h >= 256 | |
| def test_fit_dimensions_min_256_thin_portrait(): | |
| # Thin portrait: short side would be tiny without min clamp | |
| w, h = models.fit_dimensions(_img(64, 512)) | |
| assert w >= 256 | |
| assert h >= 256 | |
| # --- aspect ratio within tolerance --- | |
| def test_fit_dimensions_aspect_landscape_within_5pct(): | |
| original_ratio = 1920 / 1080 | |
| w, h = models.fit_dimensions(_img(1920, 1080)) | |
| new_ratio = w / h | |
| assert abs(new_ratio - original_ratio) / original_ratio < 0.05 | |
| def test_fit_dimensions_aspect_portrait_within_5pct(): | |
| original_ratio = 1080 / 1920 | |
| w, h = models.fit_dimensions(_img(1080, 1920)) | |
| new_ratio = w / h | |
| assert abs(new_ratio - original_ratio) / original_ratio < 0.05 | |
| def test_fit_dimensions_aspect_wide_within_5pct(): | |
| # 16:9 wide variant | |
| original_ratio = 1280 / 720 | |
| w, h = models.fit_dimensions(_img(1280, 720)) | |
| new_ratio = w / h | |
| assert abs(new_ratio - original_ratio) / original_ratio < 0.05 | |
| # --- no-op when image already fits --- | |
| def test_fit_dimensions_no_change_when_already_fits(): | |
| # 768x1024 = 786432 < 1048576, already multiples of 16, both >= 256 | |
| w, h = models.fit_dimensions(_img(768, 1024)) | |
| assert w == 768 | |
| assert h == 1024 | |
| def test_fit_dimensions_exact_max_pixels(): | |
| # Exactly 1024x1024 = max_pixels — no scaling, no rounding needed | |
| w, h = models.fit_dimensions(_img(1024, 1024)) | |
| assert w == 1024 | |
| assert h == 1024 | |
| # --- custom multiple --- | |
| def test_fit_dimensions_custom_multiple(): | |
| w, h = models.fit_dimensions(_img(1920, 1080), multiple=32) | |
| assert w % 32 == 0 | |
| assert h % 32 == 0 | |
| # --------------------------------------------------------------------------- | |
| # auto_device — requires torch; skip gracefully when absent | |
| # --------------------------------------------------------------------------- | |
| def test_auto_device_returns_valid_device(): | |
| device = models.auto_device() | |
| assert device in ("cuda", "mps", "cpu") | |
| # --------------------------------------------------------------------------- | |
| # should_cpu_offload — basic logic tests (no real GPU needed) | |
| # --------------------------------------------------------------------------- | |
| def test_should_cpu_offload_false_on_spaces(monkeypatch): | |
| monkeypatch.setenv("SPACES_ZERO_GPU", "1") | |
| assert models.should_cpu_offload("cuda") is False | |
| def test_should_cpu_offload_false_for_mps(monkeypatch): | |
| monkeypatch.delenv("SPACES_ZERO_GPU", raising=False) | |
| assert models.should_cpu_offload("mps") is False | |
| def test_should_cpu_offload_false_for_cpu(monkeypatch): | |
| monkeypatch.delenv("SPACES_ZERO_GPU", raising=False) | |
| assert models.should_cpu_offload("cpu") is False | |
| def test_should_cpu_offload_false_when_torch_unavailable(monkeypatch): | |
| monkeypatch.delenv("SPACES_ZERO_GPU", raising=False) | |
| # Simulate torch raising on cuda check by patching the function's import | |
| import sys | |
| original = sys.modules.get("torch") | |
| sys.modules["torch"] = None # type: ignore[assignment] | |
| try: | |
| result = models.should_cpu_offload("cuda") | |
| finally: | |
| if original is None: | |
| sys.modules.pop("torch", None) | |
| else: | |
| sys.modules["torch"] = original | |
| assert result is False | |