feat(engines): searchable dropdown for the Active LLM and OCR model pickers
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4b59d06479
commit
5852d7a2d5
@ -7,6 +7,9 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- **Searchable model dropdown** for the Active Language Model and Active OCR Model pickers (Engines page): the long native model list is now the same type-to-search dropdown used for voices, with model-specific wording and no avatars.
|
||||
|
||||
### Changed
|
||||
- **Character images: period-safe, uninjured, less uniform.** Costume and appearance text sent to the image model is now rewritten to period garments (Uniform, Weste, Hut, Sporen, Jacke, Schal … became livery tabard, jerkin, felt cap, doublet …) because the extracted sheets contained such words for ~45 characters and the images came out in wartime/cowboy dress; injury clauses (blood, wounds, bandages, burns, limping, torn clothes) are removed clause by clause from appearance, costume and the story-moment text instead of dropping whole sentences; characters with armour or weapons get an individual garment cut and colour scheme (stable per name, colours named in the sheet win) instead of one shared list of gambeson/brigandine/plate. Saved prompts are cleaned the same way at render time. Adult figures are described as eight head-heights tall with a small head, to counter the short, stubby turnaround figures. The stored sheets are unchanged.
|
||||
- **The turnaround now shows the same person as the profile picture.** Front, side and rear views are drawn one by one by the edit model from the identity portrait (the old text-only triptych invented a new face). Book setting reworded to Dark Eye high-fantasy medieval (was "early Renaissance 1420-1500", which produced musketeer/wartime looks), shields/cloaks no longer added to everyone, soot/dirt removed like injuries, and prop tiles are object-only (they used to show a person). Existing image pairs are unchanged until regenerated.
|
||||
|
||||
10
static/dist/main.min.js
vendored
10
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -10,7 +10,7 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#2563EB">
|
||||
<meta name="app-version" content="1.34.0">
|
||||
<meta name="app-version" content="1.34.1">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||
@ -27,12 +27,12 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.34.0">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.34.0">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.34.0">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.34.0">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.34.0">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.34.0">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.34.1">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.34.1">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.34.1">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.34.1">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.34.1">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.34.1">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
|
||||
@ -299,8 +299,19 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Model lists are long, so the model <select>s become the searchable dropdown used elsewhere.
|
||||
// The <select> stays the source of truth; the picker re-reads it whenever its options change.
|
||||
function upgradeModelSelect(id) {
|
||||
const sel = document.getElementById(id);
|
||||
const picker = window.BenchmarkVoicePicker;
|
||||
if (!sel || !picker) return;
|
||||
picker.upgrade(id, { noun: 'models', plain: true, empty: 'No models', placeholder: '— fetch models —' });
|
||||
new MutationObserver(() => picker.syncFromSelect(id)).observe(sel, { childList: true });
|
||||
}
|
||||
|
||||
// ── Active Language Model (global picker) ───────────────────────────────
|
||||
(function initActiveLlmPanel() {
|
||||
upgradeModelSelect('llm-active-model');
|
||||
const urlInp = $('llm-active-url');
|
||||
const modelSel = $('llm-active-model');
|
||||
const refreshBtn = $('llm-active-refresh');
|
||||
@ -443,6 +454,7 @@
|
||||
// proxies to whatever URL it's given rather than assuming it's the LLM one.
|
||||
(function initActiveOcrPanel() {
|
||||
const urlInp = $('ocr-active-url');
|
||||
upgradeModelSelect('ocr-active-model');
|
||||
const modelSel = $('ocr-active-model');
|
||||
const refreshBtn = $('ocr-active-refresh');
|
||||
const statusEl = $('ocr-active-status');
|
||||
|
||||
@ -113,14 +113,14 @@
|
||||
return;
|
||||
}
|
||||
const stats = statsFor(item);
|
||||
content.innerHTML = `${avatarHtml(item, 24)}<span class="bvp-trigger-main"><span class="bvp-trigger-name">${esc(item.label || item.id)}</span>${stats ? `<span class="bvp-trigger-stats">${esc(stats)}</span>` : ''}</span>`;
|
||||
content.innerHTML = `${picker.plain ? '' : avatarHtml(item, 24)}<span class="bvp-trigger-main"><span class="bvp-trigger-name">${esc(item.label || item.id)}</span>${stats ? `<span class="bvp-trigger-stats">${esc(stats)}</span>` : ''}</span>`;
|
||||
}
|
||||
|
||||
function renderList(picker, filter) {
|
||||
const q = String(filter || '').trim().toLowerCase();
|
||||
const items = q ? picker.items.filter(item => searchText(item).includes(q)) : picker.items;
|
||||
if (!items.length) {
|
||||
picker.list.innerHTML = `<div class="bvp-empty">${esc(q ? 'No voices found' : picker.empty)}</div>`;
|
||||
picker.list.innerHTML = `<div class="bvp-empty">${esc(q ? `No ${picker.noun} found` : picker.empty)}</div>`;
|
||||
return;
|
||||
}
|
||||
picker.list.innerHTML = items.map(item => {
|
||||
@ -128,7 +128,7 @@
|
||||
const stats = statsFor(item);
|
||||
const sub = [meta.lang || meta.language, meta.file_type].filter(Boolean).join(' · ');
|
||||
return `<button class="bvp-item${item.id === picker.select.value ? ' selected' : ''}" type="button" data-value="${esc(item.id)}">
|
||||
${avatarHtml(item, 28)}
|
||||
${picker.plain ? '' : avatarHtml(item, 28)}
|
||||
<span class="bvp-item-main"><span class="bvp-item-name">${esc(item.label || item.id)}</span>${sub ? `<span class="bvp-item-sub">${esc(sub)}</span>` : ''}</span>
|
||||
${stats ? `<span class="bvp-item-stats">${esc(stats)}</span>` : ''}
|
||||
</button>`;
|
||||
@ -175,6 +175,8 @@
|
||||
items: [],
|
||||
placeholder: options?.placeholder || Array.from(select.options).find(o => !o.value)?.textContent || '— select after fetch —',
|
||||
empty: options?.empty || 'No voices',
|
||||
noun: options?.noun || 'voices',
|
||||
plain: !!options?.plain,
|
||||
};
|
||||
select.dataset.bvpUpgraded = '1';
|
||||
select.style.display = 'none';
|
||||
@ -189,7 +191,7 @@
|
||||
<span class="mdi mdi-chevron-down bvp-chevron"></span>
|
||||
</button>
|
||||
<div class="bvp-drop" hidden>
|
||||
<div class="bvp-search-wrap"><span class="mdi mdi-magnify bvp-search-icon"></span><input class="bvp-search" type="search" placeholder="Search voices..." autocomplete="off" spellcheck="false"></div>
|
||||
<div class="bvp-search-wrap"><span class="mdi mdi-magnify bvp-search-icon"></span><input class="bvp-search" type="search" placeholder="Search ${esc(picker.noun)}..." autocomplete="off" spellcheck="false"></div>
|
||||
<div class="bvp-list"></div>
|
||||
</div>`;
|
||||
|
||||
@ -250,6 +252,8 @@
|
||||
function syncFromSelect(selectId) {
|
||||
const picker = pickers[selectId];
|
||||
if (!picker) return;
|
||||
const blank = Array.from(picker.select.options).find(o => !o.value);
|
||||
if (blank && blank.textContent) picker.placeholder = blank.textContent;
|
||||
picker.items = Array.from(picker.select.options).filter(o => o.value).map(o => ({ id: o.value, label: o.textContent || o.value, meta: libraryVoice(o.value) || {} }));
|
||||
renderTrigger(picker);
|
||||
renderList(picker, '');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user