feat(homepage): Audiobookshelf's own mark, and the voice endpoint in the model block

Audiobookshelf carried the improvised "AB" hex monogram — the fallback for
apps with no icon. Simple Icons carries its brand mark, so the card now
uses /img/apps/audiobookshelf.svg the way Plex and Jellyfin use theirs; the
monogram went to .AGENTTRASH.

frank-tts (Qwen3-TTS 1.7 B base talker on the gfx1151 iGPU) was an
admin-only status tile, so the household read about five cloned voices in
/docs and saw no sign of the door that speaks them. It is now a Model
Information card on the user page beside the three chat models, carrying
the ask-Frank permission note and a privacy note about cloned voices.

miCard grew the shape that requires: art falls back to the card's own `art`
when a card has no StarCraft `unit`, the accent falls back to `.mi-tool`,
and a `tagline` with no `sfx` renders as text instead of a play button that
plays nothing. No audio asset ships with the card on purpose: every pack on
that endpoint is a replica of a real person's voice, so a sample clip would
put a synthetic person's voice in the repo and on the page.

Seams: the model-card contract splits unit cards (equivalence, supervision,
playable voice line) from tool cards (permission note), and the voice
stack's homepage seam now pins category=user with the reason.
This commit is contained in:
Frank committed 2026-11-04 13:39:37 -08:00
1 parent e6a85176c4
commit 53e4a777b4
8 files changed
+60 -25

No files matched your search

+5 -3
View File
@@ -79,15 +79,17 @@ function voiceBtn(text, files) {
function miCard(svc, state) {
const m = svc.model;
const art = svc.art || '/img/unit-' + m.unit + '.jpg';
const parts = ['<div class="mi-top">' +
'<img class="mi-art" src="/img/unit-' + esc(m.unit) + '.jpg" alt="' + esc(m.unit) + '">' +
'<img class="mi-art" src="' + esc(art) + '" alt="' + esc(m.unit || svc.name) + '">' +
'<div class="mi-head"><div class="mi-id"><span class="badge ' + state + '">' + BADGE_LABEL[state] + '</span>' +
'<span class="mi-name">' + esc(svc.name) + '</span></div>' +
'<div class="mi-role">' + esc(m.role) + '</div>' +
(m.hf ? '<a class="mi-hf" href="' + esc(m.hf) +
'" target="_blank" rel="noopener">model on huggingface &#8599;</a>' : '') +
'</div></div>',
voiceBtn(m.tagline, m.sfx)
(m.sfx && m.sfx.length ? voiceBtn(m.tagline, m.sfx)
: m.tagline ? '<div class="mi-tag">' + esc(m.tagline) + '</div>' : '')
];
if (m.equiv) parts.push('<div class="mi-equiv">' + esc(m.equiv) + '</div>');
if (m.spec) parts.push('<div class="mi-spec">' + esc(m.spec) + '</div>');
@@ -99,7 +101,7 @@ function miCard(svc, state) {
if (m.warn) parts.push('<details class="mi-warn"><summary>&#9888; privacy note</summary>' +
'<div class="warn-body">' + esc(m.warn) + '</div></details>');
const card = document.createElement('article');
card.className = 'mi-card mi-' + esc(m.unit);
card.className = 'mi-card mi-' + esc(m.unit || 'tool');
card.innerHTML = parts.join('');
return card;
}