- Как 27B уместили в 16 ГБ
- Почему CachyOS
- Что даёт локальный чат на CachyOS
- Адреса
- 1. Видеокарта и пакеты
- 2. Служба Ollama
- 3. Веса и модель jinx
- 4. Поиск SearXNG
- 5. Чат Open WebUI
- 5.1. Переменные окружения
- 5.2. Патч клиента SearXNG (SafeSearch)
- 5.3. Запуск контейнера
- 5.4. Фильтр режимов (Jinx Modes)
- 6. Ярлык включения и выключения
- Проверка: локальный чат работает
- Чего в этой схеме намеренно нет
Локальный чат на одном компьютере: модель считается на видеокарте, окно открывается в браузере, поиск идёт через локальный SearXNG. Отдельного агента (Telegram-бот, внешний orchestrator) нет: Open WebUI говорит с Ollama напрямую. Так выглядит рабочий локальный чат без облака.
Проверено на CachyOS, NVIDIA GeForce RTX 5060 Ti 16 ГБ, Ollama 0.40.6 (Vulkan). Модель: Qwen3.8-27B Uncensored, квантование IQ4_XS. Нужен Ollama не ниже 0.40: в GGUF архитектура называется qwen35.
Раньше в этой инструкции стоял Qwen3-14B Q5_K_M. Его хватало «на минимум», но ответы были заметно слабее. Сейчас канон локального чата — Qwen3.8-27B на той же карте 16 ГБ. Кто уже собрал стек: достаточно сменить веса, Modelfile и добавить фильтр режимов — Docker-чат и SearXNG можно не трогать.
Снятие ограничений не отменяет закон. Не просите текст, который вредит людям, помогает мошенничеству или касается несовершеннолетних.
Как 27B уместили в 16 ГБ
На бумаге «полноценная» 27B в высоком кванте на 16 ГБ не влезает. В статье про Qwen3.8-27B на clore.ai разумным ориентиром был Q6_K на карте от 32 ГБ. Дома на RTX 5060 Ti 16 ГБ сработало сочетание четырёх решений — не магия, а осознанный компромисс. Железо под другие сценарии — в обзоре локальных ИИ.
- Квантование IQ4_XS вместо Q5/Q6/Q8. Файл весов около 14,3 ГиБ. Качество заметно выше старого 14B Q5_K_M; скорость и запас VRAM лучше, чем у более «толстых» квантов той же 27B.
- Окно контекста 8192 токена (
num_ctx), а не 16384 и тем более не 64k. KV-кэш растёт с длиной окна: на 16 ГБ лишний контекст съедает карту быстрее, чем польза от длинной истории. - Хвост слоёв в RAM. Пока модель в работе, видеокарта почти заполнена (~15 ГБ), небольшой хвост Ollama держит в оперативной памяти. Это видно в
ollama ps: GPU + частичный offload. - Без Hermes и без «think всегда». Отдельный агент с огромным системным промптом и окном 64k на этой карте с 27B ломает и VRAM, и русский ответ. Размышления включает фильтр Jinx Modes только там, где они нужны (tech/research), а быт и «напиши сочинение» идут коротко.
Итог: на 16 ГБ вы получаете умную 27B для домашнего чата, но не «максимальный» квант и не длинный контекст как у арендованной 32–96 ГБ. Для игр стек всё равно гасят ярлыком — карта занята почти целиком.
Почему CachyOS
Мне очень нравится CachyOS. Это Arch Linux с упором на скорость: ядро и пакеты с оптимизациями под современные CPU, NVIDIA ставится без цирка, обновления rolling — свежие драйверы и Ollama вместе с системой.
- Быстрое ядро и пакеты под современные CPU.
- Удобная установка Arch без ручной сборки с нуля.
- Нормальная поддержка NVIDIA: здесь
ollama-vulkanи модуль ядра под текущий CachyOS. - Rolling-релиз: не ждёте «следующий релиз дистрибутива».
На момент прошлой редакции CachyOS лидировал в рейтинге просмотров страниц DistroWatch за 12 месяцев — это не число установок, а интерес к страницам дистрибутива.
Что даёт локальный чат на CachyOS
Коротко про локальный чат на CachyOS: Ollama с моделью jinx, браузерный Open WebUI и SearXNG на одной машине. Ниже — как собрать такой локальный чат и уложить Qwen3.8-27B в 16 ГБ.
| Часть | Назначение |
|---|---|
| Ollama + модель jinx | Счёт на GPU. В чате — Джинкс (кириллическое имя модели Ollama не принимает). |
| Open WebUI | Чат в браузере, порт 8080. |
| SearXNG | Поиск для кнопки-глобуса, только localhost, порт 8888. |
| Ярлык «Джинкс» | Показывает, включён ли стек, и полностью его гасит или поднимает. |
Окно контекста модели — 8192 токена, лимит ответа — 6144. Размышления включаются не всегда: фильтр Jinx Modes выбирает режим (chat / school / tech / research и т.д.). Поиск в новых чатах включён по умолчанию; для быта и школьных «напиши текст» фильтр может его отключить.
Веса подгружаются после первого сообщения и сами выгружаются после ~5 минут тишины.
Адреса
| Адрес | Что это |
|---|---|
127.0.0.1 | Сам компьютер. Ollama и SearXNG слушают только себя. |
http://127.0.0.1:8080 | Чат на этом же ПК. |
http://<LAN-IP>:8080 | Чат с телефона/ноутбука в домашней сети. |
http://127.0.0.1:11434 | API Ollama (только локально). |
http://127.0.0.1:8888 | SearXNG (только локально; с телефона поиск идёт через Open WebUI на ПК). |
Каталоги в инструкции: ~/.config/jinx и ~/.local/share/jinx. На уже собранной машине пути могут отличаться — важны службы и параметры.
1. Видеокарта и пакеты
nvidia-smi должен показать карту и объём памяти. На CachyOS для открытого модуля NVIDIA обычно нужен пакет вроде linux-cachyos-nvidia-open под текущее ядро.
sudo pacman -S ollama ollama-vulkan docker python-pyqt6 kdialog nvidia-utils
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
После группы docker — новый вход в сессию. Бэкенд здесь Vulkan (ollama-vulkan), отдельный CUDA-пакет не обязателен.
2. Служба Ollama
Файл /etc/systemd/system/ollama.service.d/context.conf:
[Service]
Environment="OLLAMA_KV_CACHE_TYPE=q8_0"
Environment="OLLAMA_FLASH_ATTENTION=1"
Environment="OLLAMA_CONTEXT_LENGTH=65536"
Environment="OLLAMA_GPU_OVERHEAD=1610612736"
OLLAMA_CONTEXT_LENGTH— потолок службы. Окно модели задаётся в Modelfile (num_ctx 8192). Сильно подниматьnum_ctxна 16 ГБ нельзя.OLLAMA_GPU_OVERHEAD— запас ~1,5 ГиБ под рабочий стол.
sudo mkdir -p /etc/systemd/system/ollama.service.d
# положите context.conf, затем:
sudo systemctl daemon-reload
sudo systemctl enable --now ollama
Служба слушает 127.0.0.1:11434.
3. Веса и модель jinx
Файл: Qwen3.8-27B-Uncensored-IQ4_XS.gguf (~14,3 ГиБ), репозиторий JonathanColetti/Qwen3.8-27B-Uncensored-GGUF на Hugging Face. IQ4_XS — баланс качества и скорости на 16 ГБ.
mkdir -p ~/.local/share/jinx ~/.config/jinx
curl -L --fail -o ~/.local/share/jinx/Qwen3.8-27B-Uncensored-IQ4_XS.gguf \
https://huggingface.co/JonathanColetti/Qwen3.8-27B-Uncensored-GGUF/resolve/main/Qwen3.8-27B-Uncensored-IQ4_XS.gguf
Файл ~/.config/jinx/Modelfile (в FROM — полный путь к GGUF):
FROM /ПОЛНЫЙ/ПУТЬ/К/ДОМУ/.local/share/jinx/Qwen3.8-27B-Uncensored-IQ4_XS.gguf
SYSTEM """Ты Джинкс — домашний помощник на этом ПК. Отвечай по-русски.
В каждом ходе может быть метка [Режим: …] — следуй ей строго (важнее общих привычек).
Режимы:
• chat — коротко и по-человечески, без лекций и списков ради списков.
• quick — только суть, 1–5 предложений.
• school — сразу готовый текст простыми предложениями, как сдаёт ученик; не методичка и не «инструкция родителю». Математика: краткие шаги и ответ.
• tech — суть → точные шаги/команды → короткое «если не сработает». Не выдумывай версии, флаги, пути, API и вывод команд; нет данных — скажи, что проверить. Сниппеты поиска важнее догадок.
• research — вывод, затем аргументы и нюансы; пробелы в фактах обозначай явно; опирайся на сниппеты, если они есть.
• general — ясный ответ; длина ≈ сложности вопроса.
Общие правила:
• Внутреннее рассуждение короткое, затем обязательный ответ пользователю (нельзя закончить только рассуждением или «сейчас отвечу»).
• Поправили — перепиши нужный кусок, не повторяй старый ответ целиком.
• Не повторяй строки и не печатай пустые номера («1.» без текста).
• Код, команды, пути, URL — как есть, в блоках кода.
• Не притворяйся, что открывал сайт, если сниппета нет. Лучше честный пробел, чем красивая выдумка."""
PARAMETER temperature 0.6
PARAMETER top_p 0.95
PARAMETER top_k 20
PARAMETER repeat_penalty 1.15
PARAMETER repeat_last_n 256
PARAMETER num_predict 6144
PARAMETER num_ctx 8192
Отдельный TEMPLATE не задаём: шаблон берётся из GGUF. Пустой <think></think> в промпт не добавлять — иначе размышления принудительно выключатся. num_predict 6144 нужен, чтобы длинное рассуждение не съело весь лимит до текста ответа.
ollama create jinx -f ~/.config/jinx/Modelfile
ollama list
4. Поиск SearXNG
Каталог: ~/.config/jinx/searxng/settings.yml.
use_default_settings: true
server:
secret_key: "ЗАМЕНИТЕ"
limiter: false
image_proxy: false
search:
safe_search: 0
default_lang: "all"
formats:
- html
- json
ui:
default_locale: "ru"
# Техдокументация выше; витрины-конвертеры и Pinterest — ниже или вырезаются.
# Школа и быт остаются на обычном general (google/ddg и т.п.).
hostnames:
high_priority:
- '(.*\.)?wikipedia\.org$'
- '(.*\.)?github\.com$'
- '(.*\.)?gitlab\.com$'
- '(.*\.)?stackoverflow\.com$'
- '(.*\.)?stackexchange\.com$'
- '(.*\.)?askubuntu\.com$'
- '(.*\.)?superuser\.com$'
- '(.*\.)?developer\.mozilla\.org$'
- '(.*\.)?docs\.python\.org$'
- '(.*\.)?docs\.ollama\.com$'
- '(.*\.)?readthedocs\.io$'
- '(.*\.)?readthedocs\.org$'
- '(.*\.)?archlinux\.org$'
- '(.*\.)?wiki\.archlinux\.org$'
- '(.*\.)?kernel\.org$'
- '(.*\.)?learn\.microsoft\.com$'
- '(.*\.)?ietf\.org$'
- '(.*\.)?rfc-editor\.org$'
- '(.*\.)?arxiv\.org$'
- '(.*\.)?npmjs\.com$'
- '(.*\.)?pypi\.org$'
- '(.*\.)?crates\.io$'
- '(.*\.)?pkg\.go\.dev$'
- '(.*\.)?go\.dev$'
low_priority:
- '(.*\.)?facebook\.com$'
- '(.*\.)?instagram\.com$'
- '(.*\.)?tiktok\.com$'
- '(.*\.)?quora\.com$'
- '(.*\.)?baidu\.com$'
- '(.*\.)?coinbase\.com$'
- '(.*\.)?binance\.com$'
remove:
- '(.*\.)?pinterest\..*'
- '(.*\.)?investing\.com$'
- '(.*\.)?alegsaonline\.com$'
engines:
- name: wikipedia
engine: wikipedia
shortcut: wp
display_type: ["infobox", "list"]
categories: [general]
- name: github
engine: github
shortcut: gh
categories: [it, general]
- name: stackoverflow
engine: stackexchange
shortcut: st
api_site: 'stackoverflow'
categories: [it, q&a, general]
- name: askubuntu
engine: stackexchange
shortcut: ubuntu
api_site: 'askubuntu'
categories: [it, q&a, general]
openssl rand -hex 32 # подставить вместо ЗАМЕНИТЕ
limiter: false — чтобы чат с этого же ПК ходил в поиск. Формат json нужен Open WebUI. default_lang: "all" — не резать выдачу только русским.
Не подключайте в каждый запрос всю вкладку SearXNG it: там часто шумит MDN и портятся бытовые ответы. Достаточно hostnames + GitHub/SO в general.
mkdir -p ~/.config/jinx/searxng
# положите settings.yml, затем:
docker run -d \
--name searxng \
--network host \
--restart unless-stopped \
-e SEARXNG_PORT=8888 \
-e GRANIAN_HOST=127.0.0.1 \
-v "$HOME/.config/jinx/searxng:/etc/searxng" \
searxng/searxng:latest
GRANIAN_HOST=127.0.0.1 — поиск не торчит в LAN напрямую.
5. Чат Open WebUI
5.1. Переменные окружения
mkdir -p ~/.config/jinx/open-webui-patches
install -m 600 /dev/null ~/.config/jinx/open-webui.env
Файл ~/.config/jinx/open-webui.env:
DEFAULT_MODEL_PARAMS={"function_calling":"legacy","repeat_penalty":1.15,"num_predict":6144}
DEFAULT_MODEL_METADATA={"capabilities":{"web_search":true},"defaultFeatureIds":["web_search"]}
ENABLE_OLLAMA_API=true
OLLAMA_BASE_URL=http://127.0.0.1:11434
DEFAULT_MODELS=jinx
ENABLE_OPENAI_API=false
DEFAULT_LOCALE=ru
ENABLE_WEB_SEARCH=true
WEB_SEARCH_ENGINE=searxng
SEARXNG_QUERY_URL=http://127.0.0.1:8888/search?q=<query>&format=json&language=all
SEARXNG_LANGUAGE=all
WEB_SEARCH_RESULT_COUNT=8
BYPASS_WEB_SEARCH_WEB_LOADER=true
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL=true
function_calling":"legacy"— иначе встроенные tools на локальной модели легко зацикливают ответ. Поиск — через глобус.BYPASS_WEB_SEARCH_WEB_LOADER— не скачивать каждую страницу целиком; работают сниппеты.- В
DEFAULT_MODEL_PARAMSнетthink: true: режим задаёт фильтр Jinx Modes. - Переменные из .env подхватываются при создании контейнера; история живёт в томе
open-webui— том не удаляйте.
5.2. Патч клиента SearXNG (SafeSearch)
Open WebUI по умолчанию шлёт в SearXNG SafeSearch=1. Нужен default safesearch: '0'.
# вытащить файл из образа и поправить default
cid=$(docker create ghcr.io/open-webui/open-webui:main)
docker cp "$cid":/app/backend/open_webui/retrieval/web/searxng.py \
~/.config/jinx/open-webui-patches/searxng.py
docker rm "$cid"
# в файле найти safesearch / kwargs.get('safesearch' …) и поставить default '0'
В файле найдите safesearch / kwargs.get('safesearch' …) и поставьте default '0'. Смонтируйте этот файл в контейнер как в шаге ниже.
5.3. Запуск контейнера
docker run -d \
--name open-webui \
--network host \
--restart unless-stopped \
--env-file "$HOME/.config/jinx/open-webui.env" \
-v open-webui:/app/backend/data \
-v "$HOME/.config/jinx/open-webui-patches/searxng.py:/app/backend/open_webui/retrieval/web/searxng.py:ro" \
ghcr.io/open-webui/open-webui:main
На ПК: http://127.0.0.1:8080. С другого устройства в доме: http://<LAN-IP>:8080. Первая учётка — администратор. Модель: jinx. Поиск — глобус. Контейнеры чата и поиска GPU не используют.
5.4. Фильтр режимов (Jinx Modes)
Без фильтра размышления пришлось бы всегда включать или всегда выключать. Фильтр выбирает режим (chat / quick / school / tech / research / general), включает или выключает think, для chat и школьных «напиши сочинение» отключает web search, добавляет метку [Режим: …].
Установка: Workspace → Functions → New Function, тип Filter, вставить код ниже, включить Active и Global. Принудительно в чате: режим school, режим tech, подумай, без рассуждений, кратко. После правок функции иногда нужен перезапуск контейнера open-webui.
"""
title: Jinx Modes
author: kiberlis
version: 1.0.0
required_open_webui_version: 0.4.0
description: Режим чата, think и web search для модели jinx.
"""
from __future__ import annotations
import re
from typing import Optional
from pydantic import BaseModel, Field
MODE_MARK = re.compile(r"^\s*режим\s+(\w+)\s*$", re.I | re.U)
THINK_ON = re.compile(r"^\s*(подумай|с рассуждением|think)\s*$", re.I | re.U)
THINK_OFF = re.compile(r"^\s*(без рассуждений|без think|no.?think)\s*$", re.I | re.U)
SHORT = re.compile(r"^\s*(кратко|коротко)\s*$", re.I | re.U)
SCHOOL_WRITE = re.compile(
r"(сочинен|изложен|напиши\s+текст|для\s+\d+\s*класс|школьн)",
re.I | re.U,
)
TECH = re.compile(
r"(ошибк|traceback|exception|docker|systemd|nvidia|cuda|ollama|"
r"конфиг|config|api|github|stack\s*overflow|установи|команду|"
r"pacman|apt |ssh |yaml|json|python|bash|скрипт)",
re.I | re.U,
)
RESEARCH = re.compile(
r"(сравни|обзор|почему|разбер|исследу|проанализир|что лучше|"
r"плюсы и минусы|research)",
re.I | re.U,
)
QUICK = re.compile(r"^(да|нет|ок|спасибо|привет|здравствуй)\b", re.I | re.U)
class Filter:
class Valves(BaseModel):
priority: int = Field(default=0)
def __init__(self):
self.valves = self.Valves()
def _last_user(self, messages: list) -> str:
for m in reversed(messages or []):
if m.get("role") == "user":
c = m.get("content")
if isinstance(c, str):
return c
if isinstance(c, list):
parts = [
x.get("text", "")
for x in c
if isinstance(x, dict) and x.get("type") == "text"
]
return "\n".join(parts)
return ""
def _pick_mode(self, text: str, forced: Optional[str]) -> str:
if forced:
return forced
t = (text or "").strip()
if SHORT.search(t) or (len(t) < 40 and QUICK.search(t)):
return "quick"
if SCHOOL_WRITE.search(t):
return "school"
if TECH.search(t):
return "tech"
if RESEARCH.search(t) or len(t) > 400:
return "research"
if len(t) < 120:
return "chat"
return "general"
def _think_for(self, mode: str, force_on: bool, force_off: bool) -> bool:
if force_off:
return False
if force_on:
return True
return mode in ("tech", "research")
def _search_for(self, mode: str, text: str) -> bool:
if mode in ("chat", "quick"):
return False
if mode == "school" and SCHOOL_WRITE.search(text or ""):
return False
return mode in ("tech", "research", "general", "school")
def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
messages = list(body.get("messages") or [])
forced = None
force_think_on = False
force_think_off = False
cleaned = []
for m in messages:
if m.get("role") != "user" or not isinstance(m.get("content"), str):
cleaned.append(m)
continue
raw = m["content"]
lines = []
for line in raw.splitlines():
mm = MODE_MARK.match(line)
if mm:
forced = mm.group(1).lower()
if forced == "подумай":
force_think_on = True
forced = None
continue
if THINK_ON.match(line):
force_think_on = True
continue
if THINK_OFF.match(line):
force_think_off = True
continue
if SHORT.match(line) and len(raw.strip()) < 20:
forced = forced or "quick"
continue
lines.append(line)
nm = dict(m)
nm["content"] = "\n".join(lines).strip() or raw
cleaned.append(nm)
text = self._last_user(cleaned)
mode = self._pick_mode(text, forced)
if mode not in ("chat", "quick", "school", "tech", "research", "general"):
mode = "general"
think = self._think_for(mode, force_think_on, force_think_off)
want_search = self._search_for(mode, text)
mark = f"[Режим: {mode}]"
sys_msg = {
"role": "system",
"content": (
f"{mark} Следуй правилам этого режима из SYSTEM модели jinx. "
f"Рассуждения (think): {'включены' if think else 'выключены'}."
),
}
# Put mode mark near the end so it wins over older system fluff.
out_msgs = []
inserted = False
for m in cleaned:
out_msgs.append(m)
if m.get("role") == "user" and not inserted:
# insert system just before last user later
pass
# Rebuild: all non-trailing-user, then sys_msg, then last user
if cleaned and cleaned[-1].get("role") == "user":
out_msgs = cleaned[:-1] + [sys_msg, cleaned[-1]]
else:
out_msgs = cleaned + [sys_msg]
body = dict(body)
body["messages"] = out_msgs
opts = dict(body.get("options") or {})
opts["think"] = think
body["options"] = opts
features = dict(body.get("features") or {})
if not want_search:
features["web_search"] = False
body["features"] = features
return body
6. Ярлык включения и выключения
Нужны python-pyqt6 и kdialog. Выключение останавливает контейнеры и Ollama. Включение поднимает службы без загрузки весов в VRAM.
Сохранить как ~/.local/bin/jinx-toggle, chmod 755:
#!/usr/bin/env python3
"""Show whether Jinx is running and turn the whole local stack on or off."""
import os
import subprocess
import tempfile
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
OLLAMA = "ollama"
OFF_LABEL = {"ollama": "остановлена", "chat": "остановлен", "search": "остановлен"}
def container_running(name):
result = subprocess.run(
["docker", "inspect", "--format", "{{.State.Running}}", name],
capture_output=True,
text=True,
)
return result.returncode == 0 and result.stdout.strip() == "true"
def service_state():
active = subprocess.run(
["systemctl", "is-active", "--quiet", OLLAMA]
).returncode == 0
return {
"ollama": active,
"chat": container_running("open-webui"),
"search": container_running("searxng"),
}
def stack_on(state):
return state["ollama"] or state["chat"] or state["search"]
class SwitchWorker(QThread):
finished_ok = pyqtSignal(bool, str)
def __init__(self, turn_off):
super().__init__()
self.turn_off = turn_off
def run(self):
ask = tempfile.NamedTemporaryFile("w", delete=False)
ask.write(
"#!/bin/sh\n"
"exec /usr/bin/kdialog --password "
"\"Пароль, чтобы включить или выключить Джинкс\"\n"
)
ask.close()
os.chmod(ask.name, 0o700)
env = os.environ.copy()
env["SUDO_ASKPASS"] = ask.name
env.setdefault("DISPLAY", ":0")
env.setdefault("WAYLAND_DISPLAY", "wayland-1")
env.setdefault("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
try:
if subprocess.run(["sudo", "-A", "-v"], env=env).returncode != 0:
self.finished_ok.emit(False, "Пароль не принят. Состояние не изменилось.")
return
if self.turn_off:
ok, message = self._stop(env)
else:
ok, message = self._start(env)
self.finished_ok.emit(ok, message)
finally:
os.unlink(ask.name)
def _systemctl(self, env, *args):
return subprocess.run(
["sudo", "-A", "/usr/bin/systemctl", *args],
env=env,
capture_output=True,
text=True,
)
def _stop(self, env):
subprocess.run(
["docker", "stop", "open-webui", "searxng"],
capture_output=True,
text=True,
)
disabled = self._systemctl(env, "disable", OLLAMA)
stopped = self._systemctl(env, "stop", OLLAMA)
if disabled.returncode != 0 or stopped.returncode != 0:
return False, "Не удалось остановить Ollama."
return True, "Ollama, чат и поиск остановлены. Видеокарта свободна."
def _start(self, env):
enabled = self._systemctl(env, "enable", OLLAMA)
started = self._systemctl(env, "start", OLLAMA)
chat = subprocess.run(
["docker", "start", "searxng", "open-webui"],
capture_output=True,
text=True,
)
if enabled.returncode != 0 or started.returncode != 0 or chat.returncode != 0:
return False, "Не удалось запустить службы."
return True, "Чат снова доступен. Видеокарта занята только после сообщения в чат."
class JinxWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Джинкс")
self.setFixedWidth(440)
self.worker = None
self.status = QLabel("Проверяю…")
self.status.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status.setFont(QFont("Sans Serif", 26, QFont.Weight.Bold))
self.rows = {}
details = QVBoxLayout()
details.setSpacing(6)
for key, title in (("ollama", "Ollama"), ("chat", "Чат"), ("search", "Поиск")):
row = QHBoxLayout()
name = QLabel(title)
name.setFont(QFont("Sans Serif", 12))
value = QLabel("—")
value.setFont(QFont("Sans Serif", 12, QFont.Weight.DemiBold))
value.setAlignment(Qt.AlignmentFlag.AlignRight)
row.addWidget(name)
row.addStretch(1)
row.addWidget(value)
details.addLayout(row)
self.rows[key] = value
self.note = QLabel("")
self.note.setWordWrap(True)
self.note.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.button = QPushButton("Подождите")
self.button.setMinimumHeight(44)
self.button.clicked.connect(self.toggle)
layout = QVBoxLayout(self)
layout.setContentsMargins(28, 24, 28, 24)
layout.setSpacing(16)
layout.addWidget(self.status)
layout.addLayout(details)
layout.addWidget(self.note)
layout.addWidget(self.button)
self.timer = QTimer(self)
self.timer.setInterval(2000)
self.timer.timeout.connect(self.refresh)
self.refresh()
self.timer.start()
def refresh(self):
if self.worker is not None:
return
state = service_state()
on = stack_on(state)
complete = all(state.values()) if on else not any(state.values())
if on and complete:
self.status.setText("Включена")
self.status.setStyleSheet("color: #1b7f3a;")
self.button.setText("Выключить")
self.note.setText("Видеокарта занята только после сообщения в чат.")
elif on:
self.status.setText("Включена частично")
self.status.setStyleSheet("color: #9a6700;")
self.button.setText("Выключить полностью")
self.note.setText("Часть служб ещё работает. Кнопка остановит все.")
else:
self.status.setText("Выключена")
self.status.setStyleSheet("color: #8a8f98;")
self.button.setText("Включить")
self.note.setText("Ollama, чат и поиск остановлены. Видеокарта свободна.")
for key, value in state.items():
label = self.rows[key]
label.setText("работает" if value else OFF_LABEL[key])
label.setStyleSheet("color: #1b7f3a;" if value else "color: #8a8f98;")
self.button.setEnabled(True)
def toggle(self):
state = service_state()
self.button.setEnabled(False)
self.timer.stop()
self.worker = SwitchWorker(stack_on(state))
self.worker.finished_ok.connect(self._finished)
self.worker.start()
def _finished(self, ok, message):
self.worker = None
self.note.setText(message)
if not ok:
self.status.setText("Не изменилась")
self.status.setStyleSheet("color: #9a6700;")
self.refresh()
self.timer.start()
def main():
app = QApplication([])
window = JinxWindow()
window.show()
raise SystemExit(app.exec())
if __name__ == "__main__":
main()
Ярлыки: ~/.local/share/applications/jinx-toggle.desktop и по желанию файл на рабочем столе.
[Desktop Entry]
Type=Application
Name=Джинкс
Comment=Показать, включена ли Джинкс, и переключить Ollama, чат и поиск
Exec=/home/USER/.local/bin/jinx-toggle
Icon=cpu
Terminal=false
Categories=Utility;
StartupNotify=false
В Exec замените USER на имя учётной записи. На рабочем столе: chmod +x.
Проверка: локальный чат работает
- ollama list содержит jinx.
- Короткий вопрос получает ответ; nvidia-smi показывает занятую VRAM; ollama ps — модель на GPU (часть слоёв может быть в RAM).
- «Привет» отвечает быстро (без длинного think); сложный технический вопрос может включить рассуждения.
- Глобус находит сниппеты; на технических запросах чаще docs/GitHub, а не конвертеры валют.
- Ярлык «Джинкс» пишет «Включена» / «Выключена». После выключения чат на 8080 недоступен, карта свободна.
Чего в этой схеме намеренно нет
- Отдельного агента с огромным системным промптом и окном 64k.
- Принудительного think: true на все сообщения.
- Скачивания полных страниц при каждом поиске.
- Публичного SearXNG в интернет (только 127.0.0.1).
Итог: локальный чат на CachyOS с Qwen3.8-27B на 16 ГБ — рабочий домашний вариант, если принять IQ4_XS и окно 8192. Полная актуальная версия этой инструкции — на сайте kiberlis.ru.








