w1x-cli 3.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- w1x/__init__.py +5 -0
- w1x/app.py +754 -0
- w1x/config.py +189 -0
- w1x/engine.py +565 -0
- w1x/memory.py +272 -0
- w1x/models.py +458 -0
- w1x/puller.py +955 -0
- w1x/terminal.py +661 -0
- w1x/testpy.py +325 -0
- w1x/tools.py +897 -0
- w1x_cli-3.5.0.dist-info/METADATA +178 -0
- w1x_cli-3.5.0.dist-info/RECORD +16 -0
- w1x_cli-3.5.0.dist-info/WHEEL +5 -0
- w1x_cli-3.5.0.dist-info/entry_points.txt +2 -0
- w1x_cli-3.5.0.dist-info/licenses/LICENSE +21 -0
- w1x_cli-3.5.0.dist-info/top_level.txt +1 -0
w1x/models.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import threading
|
|
6
|
+
from ollama import generate, list as ollama_list, ps
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.live import Live
|
|
10
|
+
|
|
11
|
+
from .config import (
|
|
12
|
+
EXIT_COMMANDS,
|
|
13
|
+
DATA_DIR,
|
|
14
|
+
load_user_config,
|
|
15
|
+
save_user_config,
|
|
16
|
+
get_keep_alive,
|
|
17
|
+
get_context_tokens,
|
|
18
|
+
)
|
|
19
|
+
from .terminal import (
|
|
20
|
+
console,
|
|
21
|
+
session,
|
|
22
|
+
run_with_spinner,
|
|
23
|
+
HTML,
|
|
24
|
+
)
|
|
25
|
+
from .puller import first_time_setup_wizard, pull_model, GLOBAL_PULL_QUEUE
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _start_global_ctrl_q_listener():
|
|
29
|
+
"""Фоновый перехватчик Ctrl+Q для мгновенного снятия всех процессов и выгрузки VRAM."""
|
|
30
|
+
if os.name != "nt":
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
import ctypes
|
|
34
|
+
|
|
35
|
+
def _listener():
|
|
36
|
+
user32 = ctypes.windll.user32
|
|
37
|
+
while True:
|
|
38
|
+
time.sleep(0.04)
|
|
39
|
+
try:
|
|
40
|
+
# 0x11: VK_CONTROL, 0x51: VK_Q
|
|
41
|
+
ctrl_down = bool(user32.GetAsyncKeyState(0x11) & 0x8000)
|
|
42
|
+
q_down = bool(user32.GetAsyncKeyState(0x51) & 0x8000)
|
|
43
|
+
if ctrl_down and q_down:
|
|
44
|
+
sys.stdout.write("\n\n[Мгновенный выход (Ctrl+Q). Выгружаю модели из VRAM...]\n")
|
|
45
|
+
sys.stdout.flush()
|
|
46
|
+
try:
|
|
47
|
+
unload_all_models()
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
os._exit(0)
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
t = threading.Thread(target=_listener, daemon=True, name="CtrlQGlobalListener")
|
|
55
|
+
t.start()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_start_global_ctrl_q_listener()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def model_name(item):
|
|
62
|
+
return getattr(item, "model", None) or (item.get("name") if isinstance(item, dict) else getattr(item, "name", None))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def installed_models():
|
|
66
|
+
try:
|
|
67
|
+
names = [model_name(item) for item in ollama_list().models]
|
|
68
|
+
return [name for name in names if name]
|
|
69
|
+
except Exception as error:
|
|
70
|
+
err_str = str(error).lower()
|
|
71
|
+
if any(w in err_str for w in ("connection", "connect", "refused", "111", "10061")):
|
|
72
|
+
console.print(
|
|
73
|
+
Panel(
|
|
74
|
+
"[bold red]❌ Не удалось подключиться к серверу Ollama.[/bold red]\n\n"
|
|
75
|
+
"• Убедитесь, что [bold green]Ollama[/bold green] запущена в Windows (значок в трее рядом с часами).\n"
|
|
76
|
+
"• Если Ollama ещё не установлена — скачайте её с официального сайта: [bold cyan]https://ollama.com[/bold cyan]",
|
|
77
|
+
title="[bold]Сервер Ollama недоступен[/bold]",
|
|
78
|
+
border_style="red",
|
|
79
|
+
padding=(1, 2),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
console.print(f"[red]Не удалось получить список моделей: {error}[/red]")
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def running_models():
|
|
88
|
+
try:
|
|
89
|
+
return [
|
|
90
|
+
name
|
|
91
|
+
for item in ps().models
|
|
92
|
+
if (name := model_name(item))
|
|
93
|
+
]
|
|
94
|
+
except Exception:
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def unload_all_models():
|
|
99
|
+
unloaded = []
|
|
100
|
+
for name in running_models():
|
|
101
|
+
try:
|
|
102
|
+
generate(model=name, prompt="", keep_alive=0)
|
|
103
|
+
unloaded.append(name)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
return unloaded
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if os.name == "nt":
|
|
110
|
+
try:
|
|
111
|
+
import ctypes
|
|
112
|
+
from ctypes import wintypes
|
|
113
|
+
|
|
114
|
+
_PHANDLER_ROUTINE = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.DWORD)
|
|
115
|
+
|
|
116
|
+
def _console_ctrl_handler(ctrl_type):
|
|
117
|
+
if ctrl_type in {2, 5, 6}:
|
|
118
|
+
try:
|
|
119
|
+
unload_all_models()
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
_handler = _PHANDLER_ROUTINE(_console_ctrl_handler)
|
|
125
|
+
ctypes.windll.kernel32.SetConsoleCtrlHandler(_handler, True)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def reset_all_models():
|
|
131
|
+
return unload_all_models()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_PRELOAD_THREAD = None
|
|
135
|
+
_PRELOAD_EVENT = threading.Event()
|
|
136
|
+
_PRELOAD_ERROR = None
|
|
137
|
+
_PRELOADED_MODEL = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _background_preload_worker(name):
|
|
141
|
+
global _PRELOAD_ERROR, _PRELOADED_MODEL
|
|
142
|
+
try:
|
|
143
|
+
generate(
|
|
144
|
+
model=name,
|
|
145
|
+
prompt="",
|
|
146
|
+
keep_alive=get_keep_alive(),
|
|
147
|
+
options={"num_ctx": get_context_tokens()},
|
|
148
|
+
)
|
|
149
|
+
_PRELOADED_MODEL = name
|
|
150
|
+
except Exception as e:
|
|
151
|
+
_PRELOAD_ERROR = e
|
|
152
|
+
finally:
|
|
153
|
+
_PRELOAD_EVENT.set()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def start_background_preload(name):
|
|
157
|
+
global _PRELOAD_THREAD, _PRELOAD_EVENT, _PRELOAD_ERROR, _PRELOADED_MODEL
|
|
158
|
+
_PRELOAD_EVENT.clear()
|
|
159
|
+
_PRELOAD_ERROR = None
|
|
160
|
+
_PRELOADED_MODEL = None
|
|
161
|
+
_PRELOAD_THREAD = threading.Thread(
|
|
162
|
+
target=_background_preload_worker,
|
|
163
|
+
args=(name,),
|
|
164
|
+
daemon=True,
|
|
165
|
+
name=f"Preload-{name}",
|
|
166
|
+
)
|
|
167
|
+
_PRELOAD_THREAD.start()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def wait_for_model_ready(name=None):
|
|
171
|
+
"""Подождать готовность модели перед отправкой первого запроса (если она ещё догружается в фоне)."""
|
|
172
|
+
global _PRELOAD_EVENT, _PRELOAD_ERROR, _PRELOAD_THREAD
|
|
173
|
+
if _PRELOAD_THREAD is None:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if _PRELOAD_EVENT.is_set():
|
|
177
|
+
if _PRELOAD_ERROR:
|
|
178
|
+
raise _PRELOAD_ERROR
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
run_with_spinner(
|
|
182
|
+
f"Догружаю {name or _PRELOADED_MODEL or 'модель'} в память...",
|
|
183
|
+
_PRELOAD_EVENT.wait,
|
|
184
|
+
)
|
|
185
|
+
if _PRELOAD_ERROR:
|
|
186
|
+
raise _PRELOAD_ERROR
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def preload_model(name):
|
|
190
|
+
run_with_spinner(
|
|
191
|
+
f"Загружаю {name}...",
|
|
192
|
+
lambda: generate(
|
|
193
|
+
model=name,
|
|
194
|
+
prompt="",
|
|
195
|
+
keep_alive=get_keep_alive(),
|
|
196
|
+
options={"num_ctx": get_context_tokens()},
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
console.print(f"[green]✓[/green] [bold]{name}[/bold] готова.")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_last_model(models):
|
|
203
|
+
"""Получить уже загруженную в VRAM модель, либо модель из настроек."""
|
|
204
|
+
if not models:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
# 1. Если прямо сейчас в памяти VRAM уже есть загруженная модель — используем её в первую очередь!
|
|
208
|
+
try:
|
|
209
|
+
active = running_models()
|
|
210
|
+
if active:
|
|
211
|
+
for act in active:
|
|
212
|
+
for m in models:
|
|
213
|
+
if act == m or act.split(":")[0] == m.split(":")[0]:
|
|
214
|
+
return m
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
# 2. Иначе читаем модель по умолчанию из настроек пользователя
|
|
219
|
+
cfg = load_user_config()
|
|
220
|
+
saved = cfg.get("default_model")
|
|
221
|
+
if saved in models:
|
|
222
|
+
return saved
|
|
223
|
+
|
|
224
|
+
# 3. Иначе ищем легковесную быструю модель
|
|
225
|
+
for pref in ("qwen3.5:4b", "qwen2.5:7b", "qwen2.5:3b", "llama3.2:3b"):
|
|
226
|
+
for m in models:
|
|
227
|
+
if pref in m:
|
|
228
|
+
return m
|
|
229
|
+
return models[0]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def save_last_model(name):
|
|
233
|
+
cfg = load_user_config()
|
|
234
|
+
cfg["default_model"] = name
|
|
235
|
+
save_user_config(cfg)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def render_models_panel(models, selected_idx, active_in_vram):
|
|
239
|
+
"""Отрисовать панель моделей с подсветкой текущего курсора (черным по белому)."""
|
|
240
|
+
lines = []
|
|
241
|
+
total_items = len(models) + 1
|
|
242
|
+
|
|
243
|
+
for i in range(total_items):
|
|
244
|
+
is_cursor = (i == selected_idx)
|
|
245
|
+
if i < len(models):
|
|
246
|
+
name = models[i]
|
|
247
|
+
is_warm = any(name == act or name.split(":")[0] == act.split(":")[0] for act in active_in_vram)
|
|
248
|
+
num_str = f"{i+1:2d}."
|
|
249
|
+
|
|
250
|
+
tag_str = " [VRAM]" if is_warm else ""
|
|
251
|
+
|
|
252
|
+
if is_cursor:
|
|
253
|
+
line_content = f" > {num_str} {name:<35} {tag_str} [ВЫБРАНА - Enter]"
|
|
254
|
+
lines.append(f"[bold black on bright_white]{line_content:<78}[/bold black on bright_white]")
|
|
255
|
+
else:
|
|
256
|
+
lines.append(f" [cyan]{num_str}[/cyan] [white]{name:<35}[/white] [dim magenta]{tag_str}[/dim magenta]")
|
|
257
|
+
else:
|
|
258
|
+
num_str = " +."
|
|
259
|
+
label = "+ Скачать новую модель из библиотеки Ollama (Тир-Лист)"
|
|
260
|
+
if is_cursor:
|
|
261
|
+
line_content = f" > {num_str} {label}"
|
|
262
|
+
lines.append(f"\n[bold black on bright_cyan]{line_content:<78}[/bold black on bright_cyan]")
|
|
263
|
+
else:
|
|
264
|
+
lines.append(f"\n [cyan]{num_str}[/cyan] [dim cyan]{label}[/dim cyan]")
|
|
265
|
+
|
|
266
|
+
return Panel(
|
|
267
|
+
"\n".join(lines),
|
|
268
|
+
title="[bold]Установленные модели Ollama[/bold]",
|
|
269
|
+
subtitle="[dim]↑ / ↓ — выбор стрелками • Enter — подтвердить • q — выход[/dim]",
|
|
270
|
+
border_style="green",
|
|
271
|
+
padding=(0, 1),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def print_models(models, default_model=None):
|
|
276
|
+
active_in_vram = set()
|
|
277
|
+
try:
|
|
278
|
+
active_in_vram = set(running_models())
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
idx = models.index(default_model) if default_model in models else 0
|
|
282
|
+
console.print(render_models_panel(models, idx, active_in_vram))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def choose_model(current=None):
|
|
286
|
+
models = installed_models()
|
|
287
|
+
|
|
288
|
+
if not models:
|
|
289
|
+
# Если ни одной модели нет — запускаем приветственный мастер скачивания
|
|
290
|
+
new_model = first_time_setup_wizard()
|
|
291
|
+
if new_model:
|
|
292
|
+
models = installed_models()
|
|
293
|
+
if not models:
|
|
294
|
+
console.print("[yellow]Модель не выбрана.[/yellow]")
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
default_model = current or get_last_model(models)
|
|
298
|
+
selected_idx = models.index(default_model) if default_model in models else 0
|
|
299
|
+
total_items = len(models) + 1
|
|
300
|
+
|
|
301
|
+
active_in_vram = set()
|
|
302
|
+
try:
|
|
303
|
+
active_in_vram = set(running_models())
|
|
304
|
+
except Exception:
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
# Интерактивный выбор по стрелкам (Windows / macOS / Linux)
|
|
308
|
+
from .puller import poll_key_crossplatform
|
|
309
|
+
console.print()
|
|
310
|
+
with Live(render_models_panel(models, selected_idx, active_in_vram), console=console, auto_refresh=False, transient=True) as live:
|
|
311
|
+
while True:
|
|
312
|
+
live.update(render_models_panel(models, selected_idx, active_in_vram), refresh=True)
|
|
313
|
+
action = poll_key_crossplatform()
|
|
314
|
+
if action:
|
|
315
|
+
if action == "UP":
|
|
316
|
+
selected_idx = (selected_idx - 1) % total_items
|
|
317
|
+
elif action == "DOWN":
|
|
318
|
+
selected_idx = (selected_idx + 1) % total_items
|
|
319
|
+
elif action == "ENTER":
|
|
320
|
+
break
|
|
321
|
+
elif action == "EXIT":
|
|
322
|
+
return None
|
|
323
|
+
elif action.isdigit():
|
|
324
|
+
num = int(action)
|
|
325
|
+
if 1 <= num <= len(models):
|
|
326
|
+
selected_idx = num - 1
|
|
327
|
+
break
|
|
328
|
+
time.sleep(0.04)
|
|
329
|
+
|
|
330
|
+
# Если выбрали пункт скачивания новой модели:
|
|
331
|
+
if selected_idx == len(models):
|
|
332
|
+
downloaded = first_time_setup_wizard()
|
|
333
|
+
if downloaded:
|
|
334
|
+
save_last_model(downloaded)
|
|
335
|
+
return downloaded
|
|
336
|
+
return default_model
|
|
337
|
+
|
|
338
|
+
chosen = models[selected_idx]
|
|
339
|
+
save_last_model(chosen)
|
|
340
|
+
console.print(render_models_panel(models, selected_idx, active_in_vram))
|
|
341
|
+
return chosen
|
|
342
|
+
|
|
343
|
+
# Фоллбек для обычного ввода
|
|
344
|
+
print_models(models, default_model=default_model)
|
|
345
|
+
console.print(
|
|
346
|
+
f"[dim]Нажми [bold green]Enter[/bold green] для [bold green]{default_model}[/bold green], "
|
|
347
|
+
"или введи номер/имя. (q — выход)[/dim]"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
while True:
|
|
351
|
+
prompt_html = f"<b><ansigreen>Модель [{default_model}] › </ansigreen></b>"
|
|
352
|
+
choice = session.prompt(
|
|
353
|
+
HTML(prompt_html)
|
|
354
|
+
).strip()
|
|
355
|
+
|
|
356
|
+
if not choice:
|
|
357
|
+
if default_model:
|
|
358
|
+
save_last_model(default_model)
|
|
359
|
+
return default_model
|
|
360
|
+
if current:
|
|
361
|
+
return current
|
|
362
|
+
if choice.lower() in EXIT_COMMANDS:
|
|
363
|
+
return None
|
|
364
|
+
if choice.isdigit() and 1 <= int(choice) <= len(models):
|
|
365
|
+
selected = models[int(choice) - 1]
|
|
366
|
+
save_last_model(selected)
|
|
367
|
+
return selected
|
|
368
|
+
if choice in models:
|
|
369
|
+
save_last_model(choice)
|
|
370
|
+
return choice
|
|
371
|
+
matched = [m for m in models if choice.lower() in m.lower()]
|
|
372
|
+
if len(matched) == 1:
|
|
373
|
+
save_last_model(matched[0])
|
|
374
|
+
return matched[0]
|
|
375
|
+
console.print("[red]Такой установленной модели нет.[/red]")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def interactive_config_menu():
|
|
379
|
+
"""Интерактивное меню настройки W1x CLI."""
|
|
380
|
+
models = installed_models()
|
|
381
|
+
while True:
|
|
382
|
+
cfg = load_user_config()
|
|
383
|
+
current_m = cfg.get("default_model", "не выбрана")
|
|
384
|
+
keep_mins = cfg.get("keep_alive_minutes", 30)
|
|
385
|
+
ctx_tokens = cfg.get("context_tokens", 8192)
|
|
386
|
+
stats = cfg.get("stats_enabled", False)
|
|
387
|
+
|
|
388
|
+
console.print()
|
|
389
|
+
console.print(
|
|
390
|
+
Panel(
|
|
391
|
+
f"[bold cyan]1.[/bold cyan] Модель по умолчанию: [bold green]{current_m}[/bold green]\n"
|
|
392
|
+
f"[bold cyan]2.[/bold cyan] Время удержания в VRAM: [bold yellow]{keep_mins} мин.[/bold yellow] (0 = выгружать сразу, 999 = держать всегда)\n"
|
|
393
|
+
f"[bold cyan]3.[/bold cyan] Размер контекста (num_ctx): [bold magenta]{ctx_tokens} токенов[/bold magenta] (~{int(ctx_tokens*3.5)} симв.)\n"
|
|
394
|
+
f"[bold cyan]4.[/bold cyan] Статистика скорости ответов: [bold]{'Включена' if stats else 'Выключена'}[/bold]\n"
|
|
395
|
+
f"[bold cyan]5.[/bold cyan] Скачать новую модель из библиотеки Ollama\n"
|
|
396
|
+
f"[bold cyan]6.[/bold cyan] Выгрузить все модели из VRAM прямо сейчас\n"
|
|
397
|
+
"[bold cyan]0.[/bold cyan] Выйти из настроек (Enter)",
|
|
398
|
+
title="[bold]⚙️ Настройки W1x CLI[/bold]",
|
|
399
|
+
border_style="cyan",
|
|
400
|
+
padding=(0, 1),
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
choice = session.prompt(
|
|
406
|
+
HTML("<b><ansigreen>Настройка [0] › </ansigreen></b>")
|
|
407
|
+
).strip()
|
|
408
|
+
except (KeyboardInterrupt, EOFError):
|
|
409
|
+
break
|
|
410
|
+
|
|
411
|
+
if not choice or choice in ("0", "q", "exit"):
|
|
412
|
+
break
|
|
413
|
+
|
|
414
|
+
if choice == "1":
|
|
415
|
+
console.print()
|
|
416
|
+
new_m = choose_model(current=current_m)
|
|
417
|
+
if new_m:
|
|
418
|
+
cfg["default_model"] = new_m
|
|
419
|
+
save_user_config(cfg)
|
|
420
|
+
console.print(f"[green]✓ Модель по умолчанию изменена на {new_m}[/green]")
|
|
421
|
+
|
|
422
|
+
elif choice == "2":
|
|
423
|
+
console.print()
|
|
424
|
+
val = session.prompt(
|
|
425
|
+
HTML(f"<b><ansigreen>Минут удержания в VRAM [{keep_mins}] › </ansigreen></b>")
|
|
426
|
+
).strip()
|
|
427
|
+
if val.isdigit():
|
|
428
|
+
cfg["keep_alive_minutes"] = int(val)
|
|
429
|
+
save_user_config(cfg)
|
|
430
|
+
console.print(f"[green]✓ Удержание в VRAM установлено: {val} мин.[/green]")
|
|
431
|
+
|
|
432
|
+
elif choice == "3":
|
|
433
|
+
console.print()
|
|
434
|
+
console.print("[dim]Популярные размеры: 2048 (минимум), 4096, 8192 (базовый), 16384 (большой), 32768 (макс)[/dim]")
|
|
435
|
+
val = session.prompt(
|
|
436
|
+
HTML(f"<b><ansigreen>Размер контекста (токенов) [{ctx_tokens}] › </ansigreen></b>")
|
|
437
|
+
).strip()
|
|
438
|
+
if val.isdigit() and int(val) >= 512:
|
|
439
|
+
cfg["context_tokens"] = int(val)
|
|
440
|
+
save_user_config(cfg)
|
|
441
|
+
console.print(f"[green]✓ Размер контекста установлен: {val} токенов (~{int(int(val)*3.5)} симв.)[/green]")
|
|
442
|
+
|
|
443
|
+
elif choice == "4":
|
|
444
|
+
cfg["stats_enabled"] = not stats
|
|
445
|
+
save_user_config(cfg)
|
|
446
|
+
state = "включена" if not stats else "выключена"
|
|
447
|
+
console.print(f"[green]✓ Статистика {state}.[/green]")
|
|
448
|
+
|
|
449
|
+
elif choice == "5":
|
|
450
|
+
downloaded = first_time_setup_wizard()
|
|
451
|
+
if downloaded:
|
|
452
|
+
cfg["default_model"] = downloaded
|
|
453
|
+
save_user_config(cfg)
|
|
454
|
+
|
|
455
|
+
elif choice == "6":
|
|
456
|
+
unloaded = unload_all_models()
|
|
457
|
+
msg = ", ".join(unloaded) if unloaded else "нет активных моделей"
|
|
458
|
+
console.print(f"[green]✓ Модели выгружены из VRAM ({msg}).[/green]")
|