Выбор мозгов: Ollama или веб-версия DeepSeek

Разбор свободных фраз, теги и подсказку по фотографии теперь может делать
не своя модель, а веб-версия DeepSeek. Своего клиента DeepSeek здесь нет
и не нужно: рядом живёт отдельный сервис deepseek-api, который держит
вкладку браузера и отдаёт наружу протокол Ollama. Для hlamingo вся разница
сводится к тому, по какому адресу спрашивать.

  HLAMINGO_ENGINE=ollama|deepseek
  HLAMINGO_DEEPSEEK_HOST=http://localhost:11500

Эмбеддинги в выбор не входят и всегда идут на OLLAMA_HOST: веб-чат векторов
не отдаёт, а подделать их нечем. Поэтому адрес Ollama нужен и в режиме
DeepSeek, иначе молча умирает поиск по смыслу.

Предохранитель в ollama.py теперь считается по каждому адресу отдельно.
Иначе лежачая Ollama гасила бы живой DeepSeek: неудача по одному адресу
на полминуты объявляла мёртвым и второй.

Проба адреса больше не подменяет config.OLLAMA_HOST на время проверки —
адрес передаётся явным параметром. Запросы идут в несколько потоков,
и подмена задевала чужие.

В настройках (вкладка "шестерёнка") появился выбор "Кто думает", поле
адреса DeepSeek и вторая кнопка проверки связи. Списка моделей теперь два:
для разбора фраз — у выбранного движка, для поиска по смыслу — у Ollama.

Замер на разборе фразы и генерации тегов: Ollama qwen3:8b 2-3 с,
DeepSeek 8-12 с плюс 15 с на подъём браузера при первом запросе.

Тесты: 115 успешно, 0 провалено.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Alex Cube
2026-08-26 12:48:30 +03:00
co-authored by Claude Opus 5
parent 1ae98b0b6f
commit 4d8175931b
7 changed files with 359 additions and 117 deletions
+103 -31
View File
@@ -239,27 +239,50 @@
<!-- ================= Настройки ================= -->
<section class="tab" id="tab-setup">
<p class="hint" id="setupIntro">Здесь настраивается связь с Ollama — это «мозги»
помощника. Без неё всё остальное работает, но не будет разбора фраз,
автоматических тегов и поиска по смыслу.</p>
<p class="hint" id="setupIntro">Здесь настраиваются «мозги» помощника. Без них
всё остальное работает, но не будет разбора свободных фраз, автоматических
тегов и подсказок по фотографии.</p>
<label>Кто думает</label>
<select id="cfgEngine">
<option value="ollama">Ollama — своя модель на своём компьютере</option>
<option value="deepseek">DeepSeek — веб-версия через браузер</option>
</select>
<p class="hint">Ollama работает без интернета и ни о чём никому не сообщает.
DeepSeek умнее, но отвечает медленнее, требует открытого браузера
с выполненным входом, и ваши фразы уходят на его серверы.</p>
<div id="deepseekRow" hidden>
<label>Адрес сервиса DeepSeek</label>
<input type="text" id="cfgDeepseekHost" placeholder="http://localhost:11500"
autocomplete="off" inputmode="url">
<p class="hint">Это отдельная программа <code>deepseek-api</code>: она держит
вкладку с чатом DeepSeek и притворяется Ollama. Её нужно запустить
(<code>start.cmd</code>) — сама она не поднимется.</p>
<div class="row mt">
<button class="act ghost" id="cfgProbeDeepseek">Проверить DeepSeek</button>
</div>
<div id="cfgProbeDeepseekResult"></div>
</div>
<label>Адрес Ollama</label>
<input type="text" id="cfgHost" placeholder="http://192.168.1.50:11434" autocomplete="off"
inputmode="url">
<p class="hint">Если Ollama на этом же компьютере — <code>http://localhost:11434</code>.
Если на другой машине — её адрес в локальной сети.</p>
<p class="hint" id="cfgHostHint">Если Ollama на этом же компьютере —
<code>http://localhost:11434</code>. Если на другой машине — её адрес
в локальной сети.</p>
<div class="row mt">
<button class="act ghost" id="cfgProbe">Проверить связь</button>
</div>
<div id="cfgProbeResult"></div>
<label>Модель для разбора фраз</label>
<label id="cfgChatLabel">Модель для разбора фраз</label>
<select id="cfgChat"></select>
<label>Модель для поиска по смыслу</label>
<select id="cfgEmbed"></select>
<p class="hint">Список заполняется моделями, которые реально установлены.
Нужных нет — на машине с Ollama: <code>ollama pull qwen2.5:7b</code>
и <code>ollama pull nomic-embed-text</code>.</p>
<p class="hint">Список заполняется моделями, которые реально есть.
Нужных нет — на машине с Ollama: <code>ollama pull qwen3:8b</code>
и <code>ollama pull bge-m3</code>.</p>
<div class="mt"><button class="act" id="cfgSave">Сохранить</button></div>
<p class="hint" id="cfgWhere"></p>
@@ -346,10 +369,12 @@ api('/api/health').then(h => {
if (h.ollama) return;
const b = $('#banner');
b.style.display = 'block';
const who = h.engine === 'deepseek' ? 'сервис DeepSeek' : 'Ollama';
b.textContent = h.fresh
? 'Похоже, помощник только установлен, а Ollama по адресу ' + h.host + ' не отвечает. '
: 'ИИ недоступен (' + h.host + '). Кнопки работают полностью; не будет только '
+ 'тегов и разбора свободных фраз. ';
? 'Похоже, помощник только установлен, а ' + who + ' по адресу ' + h.host
+ ' не отвечает. '
: 'ИИ недоступен: ' + who + ' по адресу ' + h.host + ' молчит. Кнопки работают '
+ 'полностью; не будет только тегов и разбора свободных фраз. ';
const link = document.createElement('a');
link.href = '#';
link.textContent = 'Настроить →';
@@ -362,13 +387,21 @@ api('/api/health').then(h => {
// ---------- настройки ----------
async function loadConfig() {
const d = await api('/api/config');
$('#cfgEngine').value = d.engine || 'ollama';
$('#cfgHost').value = d.ollama_host || '';
fillModels(d.models || [], d.chat_model, d.embed_model);
$('#cfgDeepseekHost').value = d.deepseek_host || '';
showEngine();
fillModels(d.models || [], d.embed_models || [], d.chat_model, d.embed_model);
$('#cfgWhere').textContent = 'Настройки хранятся в файле ' + d.env_file
+ ' — он не попадает в репозиторий.';
if (!d.available) {
$('#cfgProbeResult').innerHTML =
'<div class="card"><div class="addr taken">Ollama по этому адресу не отвечает.</div></div>';
// Ollama нужна всегда: даже когда думает DeepSeek, векторы для поиска
// по смыслу считает она — веб-чат их не отдаёт.
$('#cfgProbeResult').innerHTML = d.embed_available ? ''
: '<div class="card"><div class="addr taken">Ollama по этому адресу не отвечает.</div></div>';
if (d.engine === 'deepseek') {
$('#cfgProbeDeepseekResult').innerHTML = d.available ? ''
: '<div class="card"><div class="addr taken">Сервис DeepSeek не отвечает. '
+ 'Запущен ли deepseek-api и открыт ли браузер с чатом?</div></div>';
}
showBackupState(d.backup || {});
}
@@ -460,30 +493,67 @@ $('#archiveInput').onchange = async () => {
// Модель выбирается из установленных; текущее значение показываем даже если
// его нет в списке — иначе выбор молча сбросился бы на чужую модель.
function fillModels(models, chat, embed) {
const opts = extra => ['<option value="">— не задана —</option>']
.concat((models.length ? models : []).map(m =>
'<option value="' + esc(m) + '">' + esc(m) + '</option>'))
.concat(extra && !models.includes(extra)
// Списков два: разбор фраз может считать DeepSeek, а векторы для поиска
// по смыслу — всегда настоящая Ollama.
function fillModels(chatModels, embedModels, chat, embed) {
const opts = (list, extra) => ['<option value="">— не задана —</option>']
.concat(list.map(m => '<option value="' + esc(m) + '">' + esc(m) + '</option>'))
.concat(extra && !list.includes(extra)
? ['<option value="' + esc(extra) + '">' + esc(extra) + ' (не установлена)</option>'] : [])
.join('');
$('#cfgChat').innerHTML = opts(chat);
$('#cfgEmbed').innerHTML = opts(embed);
$('#cfgChat').innerHTML = opts(chatModels, chat);
$('#cfgEmbed').innerHTML = opts(embedModels, embed);
$('#cfgChat').value = chat || '';
$('#cfgEmbed').value = embed || '';
}
$('#cfgProbe').onclick = async () => {
const btn = $('#cfgProbe');
btn.disabled = true; btn.textContent = 'Проверяю…';
// Поле адреса DeepSeek показываем только когда он выбран, а подпись у Ollama
// меняем: в режиме DeepSeek она нужна уже не для мыслей, а только для поиска.
function showEngine() {
const ds = $('#cfgEngine').value === 'deepseek';
$('#deepseekRow').hidden = !ds;
$('#cfgHostHint').innerHTML = ds
? 'Нужна и в этом режиме: поиск по смыслу считает только Ollama — веб-чат '
+ 'векторов не отдаёт. Если её нет, останется поиск по названию и тегам.'
: 'Если Ollama на этом же компьютере — <code>http://localhost:11434</code>. '
+ 'Если на другой машине — её адрес в локальной сети.';
$('#cfgChatLabel').textContent = ds
? 'Чат DeepSeek для разбора фраз' : 'Модель для разбора фраз';
}
$('#cfgEngine').onchange = showEngine;
// Обе кнопки проверки делают одно и то же, отличаются только адресом и тем,
// куда класть ответ.
async function probeHost(btn, input, box, label) {
const b = $(btn), was = $(btn).textContent;
b.disabled = true; b.textContent = 'Проверяю…';
try {
const d = await api('/api/config/probe', {method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ollama_host: $('#cfgHost').value.trim()})});
$('#cfgProbeResult').innerHTML = '<div class="card"><div class="addr'
body: JSON.stringify({host: $(input).value.trim()})});
$(box).innerHTML = '<div class="card"><div class="addr'
+ (d.ok ? '' : ' taken') + '">' + esc(d.reply || '') + '</div></div>';
if (d.ok) fillModels(d.models || [], $('#cfgChat').value, $('#cfgEmbed').value);
} finally { btn.disabled = false; btn.textContent = 'Проверить связь'; }
return d;
} finally { b.disabled = false; b.textContent = was; }
}
$('#cfgProbe').onclick = async () => {
const d = await probeHost('#cfgProbe', '#cfgHost', '#cfgProbeResult');
if (!d.ok) return;
// Модели Ollama — всегда для поиска по смыслу; для разбора фраз они годятся
// только когда думает она же.
const ds = $('#cfgEngine').value === 'deepseek';
fillModels(ds ? [...$('#cfgChat').options].map(o => o.value).filter(Boolean)
: (d.models || []),
d.models || [], $('#cfgChat').value, $('#cfgEmbed').value);
};
$('#cfgProbeDeepseek').onclick = async () => {
const d = await probeHost('#cfgProbeDeepseek', '#cfgDeepseekHost',
'#cfgProbeDeepseekResult');
if (d.ok) fillModels(d.models || [],
[...$('#cfgEmbed').options].map(o => o.value).filter(Boolean),
$('#cfgChat').value, $('#cfgEmbed').value);
};
$('#cfgSave').onclick = async () => {
@@ -492,7 +562,9 @@ $('#cfgSave').onclick = async () => {
try {
const d = await api('/api/config', {method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({ollama_host: $('#cfgHost').value.trim(),
body: JSON.stringify({engine: $('#cfgEngine').value,
ollama_host: $('#cfgHost').value.trim(),
deepseek_host: $('#cfgDeepseekHost').value.trim(),
chat_model: $('#cfgChat').value,
embed_model: $('#cfgEmbed').value,
backup_dir: $('#cfgBackupDir').value.trim()})});