"""Validated, per-browser settings; never a source of generation authorization.""" import math import re from pathlib import Path import gradio as gr from jujo_loras import NONE_LORA from int8_backend import MODEL_ALIASES ASSETS = Path(__file__).with_name("assets") / "settings" PROMPTS = {"prompt", "negative_prompt"} class SettingsSchema: def __init__(self, controls): self.defaults = {key: component.value for key, component in controls.items()} self.rules = {} for key, component in controls.items(): if isinstance(component, gr.Checkbox): rule = ("bool", key.startswith("lora") or component.interactive is not False) elif isinstance(component, (gr.Dropdown, gr.Radio)): rule = ("choice", [item[1] for item in component.choices]) elif isinstance(component, gr.Slider): rule = ("number", component.minimum, component.maximum, component.step) elif isinstance(component, gr.Number): rule = ("number", -1, 2**64 - 1, 1) else: rule = ("text", 20000) self.rules[key] = rule def sanitize(self, raw, include_prompts=False): raw = raw if isinstance(raw, dict) else {} clean = dict(self.defaults) for key, rule in self.rules.items(): if key not in raw: continue value = raw.get(key) if key == "seed" and isinstance(value, str) and re.fullmatch(r"-?\d{1,20}", value): value = int(value) if key in PROMPTS and not include_prompts: continue if rule[0] == "bool": clean[key] = value if type(value) is bool and rule[1] else False elif rule[0] == "choice": if key == "model" and isinstance(value, str): replacement = MODEL_ALIASES.get(value) if replacement in rule[1]: value = replacement if value in rule[1]: clean[key] = value elif key.endswith("_file"): clean[key] = NONE_LORA elif rule[0] == "text": if isinstance(value, str) and len(value) <= rule[1]: clean[key] = value elif type(value) in (int, float): # Check bounds first: isfinite(very_large_int) can overflow. if rule[1] <= value <= rule[2] and math.isfinite(value): step = rule[3] clean[key] = round(round(value / step) * step, 8) if step != 1 else int(value) for index in range(1, 5): if clean.get(f"lora{index}_file") == NONE_LORA: clean[f"lora{index}_enabled"] = False return clean class SettingsStorage(gr.HTML): def api_info(self): return {"type": "object", "additionalProperties": True} def bind_browser_settings(family, controls, preset_status=None, preset_specs=None, generation_info=None): schema = SettingsSchema(controls) ready = gr.State(False) exact_seed = gr.State(None) bridge = SettingsStorage( value=None, family=family, field_names=list(controls), rules=schema.rules, defaults=schema.defaults, html_template=(ASSETS / "settings.html").read_text(encoding="utf-8"), css_template=(ASSETS / "settings.css").read_text(encoding="utf-8"), js_on_load=(ASSETS / "settings.js").read_text(encoding="utf-8"), ) components = list(controls.values()) def restore(payload, *current): payload = payload if isinstance(payload, dict) else {} action = payload.get("action") if action == "favorite_save": values = schema.sanitize(dict(zip(controls, current)), True) if current[len(controls)] is not None: values["seed"] = int(current[len(controls)]) error = "" if payload.get("seed_source") == "last": match = re.search(r"\| seed (\d+)\b", str(current[-1])) if generation_info is not None else None if match: values["seed"] = int(match[1]) else: error = "このページではまだ生成が完了していません。入力Seedで保存するか、生成後に再試行してください。" values["seed"] = str(values["seed"]) return (*[gr.skip() for _ in components], *([gr.skip()] if preset_status is not None else []), gr.skip(), gr.skip(), {"action": "favorite_saved", "values": values, "error": error, "request": payload.get("request")}) reset = payload.get("action") == "reset" record = payload.get("record") valid = (isinstance(record, dict) and record.get("version") == 1 and record.get("family") == family and record.get("enabled") is True) values = schema.sanitize(record.get("values"), record.get("prompts") is True) if valid and not reset else dict(schema.defaults) warnings = [] if action == "favorite_apply" and valid: raw = record.get("values", {}) raw = raw if isinstance(raw, dict) else {} warnings = [ key for key, value in raw.items() if key in schema.rules and schema.rules[key][0] == "choice" and value not in schema.rules[key][1] and not (key == "model" and isinstance(value, str) and MODEL_ALIASES.get(value) in schema.rules[key][1]) ] seed = str(values["seed"]) if valid and not reset else None updates = [] for key in controls: options = {"value": values[key]} if key.startswith("lora") and (key.endswith("_enabled") or key.endswith("_strength")): slot = key.split("_")[0] options["interactive"] = values[f"{slot}_file"] != NONE_LORA updates.append(gr.update(**options)) if preset_status is not None: status = preset_specs[values["preset"]]["status"] if valid and not reset: status += "\n\n_保存された手動調整を含む設定を復元しました。プリセット名より現在の各項目の値が優先されます。_" updates.append(status) stored_values = dict(values, seed=str(values["seed"])) if action == "favorite_apply" else values return (*updates, True, seed, {"action": "ready", "values": stored_values, "favorite_applied": action == "favorite_apply", "warning": "未登録の項目を初期値/未選択へ変更しました:" + "、".join(warnings) if warnings else ""}) def snapshot(initialized, saved_seed, *values): if not initialized: return gr.skip() # Prompt exclusion happens in the browser; the current UI values are # already session data and are not written to any server-side file. current = dict(zip(controls, values)) if saved_seed is not None: current["seed"] = str(saved_seed) return {"action": "sync", "values": current} bridge.submit( restore, inputs=[bridge, *components, exact_seed, *([generation_info] if generation_info is not None else [])], outputs=[*components, *([preset_status] if preset_status is not None else []), ready, exact_seed, bridge], queue=False, api_visibility="private", show_progress="hidden", ) gr.on( triggers=[component.change for component in [*components, exact_seed]], fn=snapshot, inputs=[ready, exact_seed, *components], outputs=bridge, queue=False, trigger_mode="always_last", api_visibility="private", show_progress="hidden", ) controls["seed"].input(lambda: None, outputs=exact_seed, queue=False, api_visibility="private") return exact_seed