Skip to content

Commit 0f2ccc2

Browse files
committed
refactor: replace backend category branches with frontend model profile system
1 parent 8e8ffab commit 0f2ccc2

8 files changed

Lines changed: 412 additions & 73 deletions

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ Extensive preset library organized by use case:
8181
| **Qwen-Image** | Qwen-Image workflows | Keeps images in the ~0.6 MP to 4.2 MP range, preserving valid inputs and scaling out-of-range inputs |
8282
| **ZImageTurbo** | Z-Image-Turbo workflows | Selects the closest active category resolution by aspect ratio and pixel count while preserving orientation; the built-in list uses official resolutions |
8383

84+
### Adding a Model Profile
85+
86+
Model-specific behavior is declared in `js/calculations/model_profiles.js`. To add
87+
a model that uses an existing calculation strategy:
88+
89+
1. Add its resolutions to `js/presets/preset_categories.js`.
90+
2. Add one profile containing its strategy, options, and information message.
91+
3. Add focused tests for the model's expected resolutions.
92+
93+
The frontend includes the profile in the existing preset JSON sent to Python, so
94+
no backend category branch or workflow schema change is required. If a model
95+
needs a genuinely new algorithm, register one reusable strategy in
96+
`core/auto_detect.py` and add its name to the frontend strategy contract. The
97+
contract tests ensure every built-in category has a supported profile and an
98+
information message. The backend legacy profile map exists only to keep older
99+
saved workflows working.
100+
84101
---
85102

86103
## 🚀 Installation

core/auto_detect.py

Lines changed: 164 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,50 @@
88

99

1010
ASPECT_RATIO_TOLERANCE = 0.01
11+
CALCULATION_CONFIG_VERSION = 1
12+
CALCULATION_CONFIG_VERSION_KEY = "__resolutionMasterConfigVersion"
13+
CALCULATION_PROFILE_KEY = "__resolutionMasterProfile"
14+
CALCULATION_PRESETS_KEY = "__resolutionMasterPresets"
15+
16+
17+
LEGACY_MODEL_PROFILES = {
18+
"Standard": {"strategy": "closest_aspect"},
19+
"SDXL": {"strategy": "closest_preset"},
20+
"Flux": {
21+
"strategy": "flux_like",
22+
"options": {
23+
"max_megapixels": 4.0,
24+
"max_dimension": 2560,
25+
"min_dimension": 320,
26+
"multiple": 32,
27+
},
28+
},
29+
"Flux.2": {
30+
"strategy": "flux_like",
31+
"options": {
32+
"max_megapixels": 6.0,
33+
"max_dimension": 3840,
34+
"min_dimension": 320,
35+
"multiple": 16,
36+
},
37+
},
38+
"WAN": {
39+
"strategy": "wan_pixel_range",
40+
"options": {"min_pixels": 182080, "max_pixels": 1195560, "multiple": 16},
41+
},
42+
"HiDream Dev": {"strategy": "closest_preset"},
43+
"Qwen-Image": {
44+
"strategy": "pixel_range",
45+
"options": {"min_pixels": 589824, "max_pixels": 4194304},
46+
},
47+
"ZImageTurbo": {"strategy": "closest_preset"},
48+
"Krea 2 Turbo": {"strategy": "closest_preset"},
49+
"Krea 2 RAW": {"strategy": "closest_preset"},
50+
"Social Media": {"strategy": "closest_aspect"},
51+
"Print": {"strategy": "closest_aspect"},
52+
"Cinema": {"strategy": "closest_aspect"},
53+
"Display Resolutions": {"strategy": "closest_aspect"},
54+
}
1155

1256

1357
def safe_int(value, default=0):
@@ -24,16 +68,32 @@ def safe_float(value, default=0.0):
2468
return default
2569

2670

27-
def load_presets(presets_json):
71+
def load_calculation_config(presets_json):
2872
try:
29-
presets = json.loads(presets_json or "{}")
30-
if isinstance(presets, dict):
31-
return presets
32-
log.warning("Ignoring presets JSON because decoded value is not an object")
33-
return {}
73+
decoded = json.loads(presets_json or "{}")
74+
if not isinstance(decoded, dict):
75+
log.warning("Ignoring presets JSON because decoded value is not an object")
76+
return {}, {}
77+
78+
if decoded.get(CALCULATION_CONFIG_VERSION_KEY) == CALCULATION_CONFIG_VERSION:
79+
presets = decoded.get(CALCULATION_PRESETS_KEY)
80+
profile = decoded.get(CALCULATION_PROFILE_KEY)
81+
if not isinstance(presets, dict):
82+
log.warning("Ignoring calculation presets because config value is not an object")
83+
presets = {}
84+
if not isinstance(profile, dict):
85+
profile = {}
86+
return presets, profile
87+
88+
return decoded, {}
3489
except (TypeError, ValueError) as error:
3590
log.warning("Failed to parse presets JSON", error)
36-
return {}
91+
return {}, {}
92+
93+
94+
def load_presets(presets_json):
95+
presets, _profile = load_calculation_config(presets_json)
96+
return presets
3797

3898

3999
def choose_best_scaling_option(current_pixels, option1, option2):
@@ -138,51 +198,97 @@ def apply_flux_like_calculation(width, height, max_mp, max_dim, min_dim, multipl
138198
}
139199

140200

141-
def apply_custom_calculation(width, height, category, presets):
142-
if category == "Flux":
143-
return apply_flux_like_calculation(width, height, 4.0, 2560, 320, 32)
144-
if category == "Flux.2":
145-
return apply_flux_like_calculation(width, height, 6.0, 3840, 320, 16)
146-
if category == "WAN":
147-
target_pixels = max(182080, min(1195560, width * height))
148-
aspect = width / height
149-
target_height = math.sqrt(target_pixels / aspect)
150-
target_width = target_height * aspect
151-
return {
152-
"width": round(target_width / 16) * 16,
153-
"height": round(target_height / 16) * 16,
154-
}
155-
if category == "Qwen-Image":
156-
current_pixels = width * height
157-
min_pixels = 589824
158-
max_pixels = 4194304
159-
if min_pixels <= current_pixels <= max_pixels:
160-
return {"width": width, "height": height}
161-
target_pixels = min_pixels if current_pixels < min_pixels else max_pixels
162-
aspect = width / height
163-
target_height = math.sqrt(target_pixels / aspect)
164-
return {"width": round(target_height * aspect), "height": round(target_height)}
165-
166-
if category in ("SDXL", "HiDream Dev", "Krea 2 Turbo", "Krea 2 RAW", "ZImageTurbo"):
167-
closest = find_closest_preset(width, height, presets)
168-
if not closest:
169-
return {"width": width, "height": height}
170-
return {"width": closest["width"], "height": closest["height"]}
171-
172-
if category not in ("Standard", "Social Media", "Print", "Cinema", "Display Resolutions"):
173-
return {"width": width, "height": height}
201+
def _unchanged_dimensions(width, height):
202+
return {"width": width, "height": height}
203+
204+
205+
def _apply_closest_preset_strategy(width, height, presets, _options):
206+
closest = find_closest_preset(width, height, presets)
207+
if not closest:
208+
return _unchanged_dimensions(width, height)
209+
return {"width": closest["width"], "height": closest["height"]}
210+
174211

212+
def _apply_closest_aspect_strategy(width, height, presets, options):
175213
closest = find_closest_preset(width, height, presets)
176214
if not closest:
177-
return {"width": width, "height": height}
215+
return _unchanged_dimensions(width, height)
178216

179217
preset_aspect = closest["width"] / closest["height"]
180218
current_aspect = width / height
181-
if abs(current_aspect - preset_aspect) < 0.01:
182-
return {"width": width, "height": height}
219+
tolerance = max(0.0, safe_float(options.get("tolerance"), ASPECT_RATIO_TOLERANCE))
220+
if abs(current_aspect - preset_aspect) < tolerance:
221+
return _unchanged_dimensions(width, height)
183222
return scale_to_preset_aspect_ratio(width, height, preset_aspect)
184223

185224

225+
def _apply_flux_like_strategy(width, height, _presets, options):
226+
min_dimension = max(1, safe_int(options.get("min_dimension"), 320))
227+
max_dimension = max(min_dimension, safe_int(options.get("max_dimension"), 2560))
228+
return apply_flux_like_calculation(
229+
width,
230+
height,
231+
max(0.001, safe_float(options.get("max_megapixels"), 4.0)),
232+
max_dimension,
233+
min_dimension,
234+
max(1, safe_int(options.get("multiple"), 32)),
235+
)
236+
237+
238+
def _apply_wan_pixel_range_strategy(width, height, _presets, options):
239+
min_pixels = max(1, safe_int(options.get("min_pixels"), 182080))
240+
max_pixels = max(min_pixels, safe_int(options.get("max_pixels"), 1195560))
241+
multiple = max(1, safe_int(options.get("multiple"), 16))
242+
target_pixels = max(min_pixels, min(max_pixels, width * height))
243+
aspect = width / height
244+
target_height = math.sqrt(target_pixels / aspect)
245+
target_width = target_height * aspect
246+
return {
247+
"width": max(multiple, round(target_width / multiple) * multiple),
248+
"height": max(multiple, round(target_height / multiple) * multiple),
249+
}
250+
251+
252+
def _apply_pixel_range_strategy(width, height, _presets, options):
253+
current_pixels = width * height
254+
min_pixels = max(1, safe_int(options.get("min_pixels"), 589824))
255+
max_pixels = max(min_pixels, safe_int(options.get("max_pixels"), 4194304))
256+
if min_pixels <= current_pixels <= max_pixels:
257+
return _unchanged_dimensions(width, height)
258+
target_pixels = min_pixels if current_pixels < min_pixels else max_pixels
259+
aspect = width / height
260+
target_height = math.sqrt(target_pixels / aspect)
261+
return {"width": round(target_height * aspect), "height": round(target_height)}
262+
263+
264+
CALCULATION_STRATEGIES = {
265+
"closest_aspect": _apply_closest_aspect_strategy,
266+
"closest_preset": _apply_closest_preset_strategy,
267+
"flux_like": _apply_flux_like_strategy,
268+
"pixel_range": _apply_pixel_range_strategy,
269+
"wan_pixel_range": _apply_wan_pixel_range_strategy,
270+
}
271+
272+
273+
def apply_custom_calculation(width, height, category, presets, profile=None):
274+
resolved_profile = profile if isinstance(profile, dict) and profile.get("strategy") else None
275+
if resolved_profile is None:
276+
resolved_profile = LEGACY_MODEL_PROFILES.get(category)
277+
if not resolved_profile:
278+
return _unchanged_dimensions(width, height)
279+
280+
strategy_name = resolved_profile.get("strategy")
281+
strategy = CALCULATION_STRATEGIES.get(strategy_name)
282+
if strategy is None:
283+
log.warning("Unknown calculation strategy", strategy_name, "for category", category)
284+
return _unchanged_dimensions(width, height)
285+
286+
options = resolved_profile.get("options")
287+
if not isinstance(options, dict):
288+
options = {}
289+
return strategy(width, height, presets, options)
290+
291+
186292
def calculate_auto_fit(width, height, category, smart_fit, presets, preserve_scaling_ratio=False):
187293
closest = find_closest_preset(width, height, presets)
188294
if not closest:
@@ -326,7 +432,7 @@ def calculate_resolution(
326432
target_resolution = max(1, safe_int(target_resolution, 1080))
327433
target_megapixels = max(0.0, safe_float(target_megapixels, 2.0))
328434
selected_category = selected_category or ""
329-
presets = load_presets(presets_json)
435+
presets, calculation_profile = load_calculation_config(presets_json)
330436
selected_preset = None
331437

332438
log.debug(
@@ -363,7 +469,13 @@ def calculate_resolution(
363469

364470
elif action == "custom_calc":
365471
if selected_category:
366-
calculated = apply_custom_calculation(width, height, selected_category, presets)
472+
calculated = apply_custom_calculation(
473+
width,
474+
height,
475+
selected_category,
476+
presets,
477+
calculation_profile,
478+
)
367479
width, height = calculated["width"], calculated["height"]
368480

369481
elif action == "auto_detect":
@@ -389,7 +501,13 @@ def calculate_resolution(
389501
width, height = snapped["width"], snapped["height"]
390502

391503
if use_custom_calc and selected_category:
392-
calculated = apply_custom_calculation(width, height, selected_category, presets)
504+
calculated = apply_custom_calculation(
505+
width,
506+
height,
507+
selected_category,
508+
presets,
509+
calculation_profile,
510+
)
393511
width, height = calculated["width"], calculated["height"]
394512

395513
elif action in ("rescale", "target_resolution_from_scale"):

0 commit comments

Comments
 (0)