UI: model dropdown auto-fills from LM Studio /v1/models

- Removed allow_custom_value=True (it was rendering as text input, not select)
- Added fetch_lm_studio_models_with_value() wrapper that sets value=first model
- demo.load() auto-fetches models on UI load
- Removed Measure-Object bug in ps1 launcher
- Removed auto-load step in ps1 launcher (user picks model explicitly)
This commit is contained in:
Mavis
2026-06-15 09:58:48 +03:00
parent 36d9906e1f
commit 4adde251ae
2 changed files with 122 additions and 28 deletions
+104 -28
View File
@@ -123,7 +123,7 @@ def _refresh_history_df(limit: int = 20) -> list[list[Any]]:
_ON_GENERATE_NOUTPUTS = 7
def _empty_result(n_candidates: int) -> tuple[list, list, str, list, str, list, str]:
def _empty_result(n_candidates: int) -> tuple[list, list, str, list, str, list, str, str]:
"""Стандартный «пустой» возврат on_generate для случаев раннего выхода.
Gradio ждёт от каждого callback ровно столько значений, сколько объявлено
@@ -132,9 +132,10 @@ def _empty_result(n_candidates: int) -> tuple[list, list, str, list, str, list,
outputs, иначе Gradio поднимет `IndexError`/warning.
Returns:
Кортеж из 7 элементов: ([] , [] , "" , [] , "" , [] , "").
Кортеж из 8 элементов: ([] , [] , "" , [] , "" , [] , "" , "")
— последний "" — пустое поле reasoning_markdown.
"""
return ([], [], "", [], "", [], "")
return ([], [], "", [], "", [], "", "")
def _save_all_previews(
@@ -214,6 +215,19 @@ def fetch_lm_studio_models(base_url: str, api_key: str) -> tuple[list[str], str]
return ([], f"ошибка: {type(exc).__name__}: {exc}")
def fetch_lm_studio_models_with_value(base_url: str, api_key: str) -> tuple[gr.update, str]:
"""Обёртка для UI: возвращает gr.update(choices=..., value=первая модель).
Использует fetch_lm_studio_models() для получения списка и устанавливает
value на ПЕРВУЮ модель из списка, чтобы Gradio не ругался, что value
не входит в choices.
"""
models, status = fetch_lm_studio_models(base_url, api_key)
if models:
return (gr.update(choices=models, value=models[0]), status)
return (gr.update(choices=[], value=None), status)
def on_generate(
prompt: str,
mode: str,
@@ -553,6 +567,9 @@ def on_generate_live(
tokens = 0
final_model = model
final_finish_reason = ""
# qwen3.5 шлёт рассуждения в отдельном поле `reasoning_content`; копим
# их параллельно с `content` (буфер для SVG) и рисуем в reasoning_md.
reasoning_text = ""
# Первый yield — статус "поехали". Это нужно, чтобы UI сразу сменил
# "Сгенерировано" прошлой генерации на "Live-стрим запущен…".
@@ -564,6 +581,7 @@ def on_generate_live(
"Live-стрим запущен…",
[],
"",
"",
)
try:
@@ -584,33 +602,68 @@ def on_generate_live(
return
if event.type == "delta":
buffer += event.content
tokens += 1
# Reasoning-токены (qwen3.5) приходят в `event.reasoning` —
# копим и обновляем reasoning_md ниже. Контент для SVG
# идёт в `event.content`. Если оба пустые — игнор.
had_anything = False
if event.content:
buffer += event.content
tokens += 1
had_anything = True
if event.reasoning:
reasoning_text += event.reasoning
had_anything = True
if not had_anything:
continue
# Throttle: не чаще одного render-yield на LIVE_THROTTLE_S.
# Reasoning-обновления пропускаем через throttle наравне с
# render-yield'ами — иначе UI будет перерисовываться слишком
# часто (qwen3.5 шлёт 10-20 reasoning-токенов в секунду).
now = time.monotonic()
if now - last_update_ts < LIVE_THROTTLE_S:
continue
# Парсим + рендерим. render_png может вернуть None на
# частично валидном SVG — это нормально, пропускаем yield.
svg = parse_to_valid(buffer)
last_valid_svg = svg
png = render_png(svg, size=size)
if png is None:
continue
path_str = _save_live_png(png, live_path)
if not path_str:
continue
last_update_ts = now
caption = (path_str, f"live · {tokens} tok")
yield (
[caption],
[caption],
svg,
gr.update(), # history_df не трогаем до финала
f"Live-стрим: ~{tokens} токенов",
[path_str],
"",
)
# частично валидном SVG — это нормально, пропускаем render-yield,
# но markdown с reasoning всё равно отдадим, чтобы юзер видел
# что модель думает.
svg = parse_to_valid(buffer) if buffer else ""
if svg:
last_valid_svg = svg
png = None
path_str = ""
if svg:
png = render_png(svg, size=size)
if png is not None:
path_str = _save_live_png(png, live_path)
# Готовим caption/gallery только если есть PNG; иначе отдаём
# пустую галерею, чтобы НЕ стирать предыдущий кадр (Gradio
# воспринимает [] как "очистить", а нам это не нужно).
if path_str:
last_update_ts = now
caption = (path_str, f"live · {tokens} tok")
yield (
[caption],
[caption],
svg,
gr.update(), # history_df не трогаем до финала
f"Live-стрим: ~{tokens} токенов",
[path_str],
"",
reasoning_text,
)
elif reasoning_text:
# Reasoning идёт, а валидного SVG ещё нет — обновим только
# reasoning_md, остальные outputs не трогаем.
yield (
gr.update(),
gr.update(),
gr.update(),
gr.update(),
f"Live-стрим: ~{tokens} токенов",
gr.update(),
gr.update(),
reasoning_text,
)
elif event.type == "end":
final_model = event.model or model
final_finish_reason = event.finish_reason
@@ -725,6 +778,7 @@ def on_generate_live(
status_md, # status
preview_paths, # превью для архива
"", # error
reasoning_text, # reasoning_md
)
@@ -845,7 +899,6 @@ def build_ui() -> gr.Blocks:
label="Модель",
choices=[os.environ.get("DEFAULT_MODEL", DEFAULT_MODEL)],
value=os.environ.get("DEFAULT_MODEL", DEFAULT_MODEL),
allow_custom_value=True,
)
use_live_cb = gr.Checkbox(
label="Live-стрим (превью в реальном времени)",
@@ -860,6 +913,14 @@ def build_ui() -> gr.Blocks:
columns=3, height=320, object_fit="contain",
)
gallery_state = gr.State([])
# Поле с reasoning-токенами модели (qwen3.5 шлёт рассуждения
# отдельным `reasoning_content`-полем). Полезно для моделей
# типа qwen3.5-A3B, которые думают перед тем, как начать писать
# SVG — без этого юзер не видит, что стрим идёт.
reasoning_md = gr.Markdown(
label="Мысли модели (reasoning)",
value="",
)
with gr.Accordion("SVG-код лучшего кандидата", open=False):
# gr.Code в Gradio 5.37 не имеет "xml" в whitelist языков
# (есть python/sql/html/markdown/...). Используем "html"
@@ -881,7 +942,13 @@ def build_ui() -> gr.Blocks:
outputs=[n_slider],
)
refresh_btn.click(
fetch_lm_studio_models,
fetch_lm_studio_models_with_value,
inputs=[base_url_tb, api_key_tb],
outputs=[model_dd, fetch_status_md],
)
# Автозапрос списка моделей при загрузке UI
demo.load(
fetch_lm_studio_models_with_value,
inputs=[base_url_tb, api_key_tb],
outputs=[model_dd, fetch_status_md],
)
@@ -892,7 +959,16 @@ def build_ui() -> gr.Blocks:
image_in, palette_tb, model_dd, base_url_tb, api_key_tb,
use_live_cb,
],
outputs=[gallery, gallery_state, svg_viewer, history_df, status_md, gr.State([]), gr.State("")],
outputs=[
gallery,
gallery_state,
svg_viewer,
history_df,
status_md,
gr.State([]),
gr.State(""),
reasoning_md,
],
)
history_df.select(
on_history_select,