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;
}
@@ -0,0 +1 @@
<svg fill="#82612C" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Audiobookshelf</title><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0Zm-.023.402A11.598 11.598 0 0 1 23.575 12a11.598 11.598 0 0 1-11.598 11.598A11.598 11.598 0 0 1 .378 12 11.598 11.598 0 0 1 11.977.402Zm0 1.776a7.093 7.093 0 0 0-7.092 7.093v1.536a6.395 6.395 0 0 0-.439.33.35.35 0 0 0-.126.27v1.84a.36.36 0 0 0 .126.272c.22.182.722.564 1.504.956v.179c0 .483.31.873.694.873.384 0 .694-.392.694-.873v-4.415c0-.483-.31-.873-.694-.873-.369 0-.67.359-.694.812h-.002v-.91a6.027 6.027 0 1 1 12.054.003v.91c-.025-.454-.326-.813-.695-.813-.384 0-.694.391-.694.873v4.415c0 .483.31.873.694.873.384 0 .695-.392.695-.873v-.179a7.964 7.964 0 0 0 1.503-.956.35.35 0 0 0 .126-.272v-1.843a.342.342 0 0 0-.124-.27 5.932 5.932 0 0 0-.438-.329V9.271a7.093 7.093 0 0 0-7.092-7.093zm-3.34 5.548a.84.84 0 0 0-.84.84v9.405c0 .464.376.84.84.84h.866a.84.84 0 0 0 .84-.84V8.566a.84.84 0 0 0-.84-.84Zm2.905 0a.84.84 0 0 0-.84.84v9.405c0 .464.377.84.84.84h.867a.84.84 0 0 0 .84-.84V8.566a.84.84 0 0 0-.84-.84zm2.908 0a.84.84 0 0 0-.84.84v9.405c0 .464.376.84.84.84h.867a.84.84 0 0 0 .84-.84V8.566a.84.84 0 0 0-.84-.84zM8.112 9.983h1.915v.2H8.112Zm2.906 0h1.915v.2h-1.915Zm2.908 0h1.915v.2h-1.915zm-7.58 9.119a.633.633 0 0 0 0 1.265h11.26a.632.632 0 0 0 0-1.265z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><polygon points="48,6 86,27 86,69 48,90 10,69 10,27" fill="#1c1300" stroke="#3fc1c9" stroke-width="3"/><text x="48" y="60" font-family="monospace" font-size="30" font-weight="bold" fill="#3fc1c9" text-anchor="middle">AB</text></svg>

Before

Width:  |  Height:  |  Size: 293 B

+13 -4
View File
@@ -522,7 +522,7 @@
"method": "http",
"wip": false,
"showLink": true,
"art": "/img/apps/mono-ab.svg"
"art": "/img/apps/audiobookshelf.svg"
},
{
"name": "Postgres",
@@ -552,11 +552,20 @@
},
{
"name": "frank-tts",
"category": "admin",
"category": "user",
"section": "AI",
"description": "The fleet's cloned-voice speech endpoint: POST /v1/audio/speech with voice=<name>, audio back in that voice. Published on 7772; the engine behind it (frank-tts-engine) is unpublished on purpose. It is also the harness speech role.",
"model": {
"role": "The fleet's voice — it speaks, it does not chat",
"hf": "https://huggingface.co/Serveurperso/Qwen3-TTS-GGUF",
"tagline": "Your library, read aloud in a voice you know.",
"spec": "Qwen3-TTS 1.7 B base talker (Q4_K_M) with its 12 Hz tokenizer, run by qwentts.cpp on the gfx1151 iGPU. The frank-tts door answers POST /v1/audio/speech with voice=<name> and hands back audio in that voice.",
"inputs": ["text"],
"use": "Narration: Open Notebook episodes, the operator harness's spoken output, and anything that should sound like a person instead of a text-to-speech robot.",
"permission": "Ask Frank for a voice — each pack is built from that person's own recording.",
"warn": "Cloned voices. Every pack on this endpoint is a replica of a real person's voice. Audio coming out of it is not that person speaking and must never be passed off as them."
},
"url": "",
"docs": "",
"docs": "/docs#models",
"probe": "127.0.0.1:7772",
"method": "http",
"wip": false,
+2
View File
@@ -263,6 +263,7 @@ html[data-bare="1"] .top { border-image: none; }
.mi-zealot { --mi: #4fb4ff; } /* Zealot: Khai's blue blades */
.mi-archon { --mi: #a8ecff; } /* Archon: pure psychic light */
.mi-dragoon { --mi: #5fe0d0; } /* Dragoon: phase-crystal teal */
.mi-tool { --mi: #ffc86b; } /* frank-tts: amber — a tool, not a unit */
.mi-top { display: flex; align-items: center; gap: 10px; }
.mi-art {
width: 56px;
@@ -294,6 +295,7 @@ html[data-bare="1"] .top { border-image: none; }
.mi-voice:hover { text-shadow: 0 0 8px var(--mi); }
.mi-voice.mi-speaking { animation: mi-pulse 0.7s ease-in-out infinite; }
@keyframes mi-pulse { 50% { text-shadow: 0 0 12px var(--mi); } }
.mi-tag { color: var(--mi); margin: 4px 0; }
.mi-equiv { color: var(--head); font-style: italic; margin: 4px 0; }
.mi-spec { color: var(--fg); margin: 4px 0; }
.mi-inputs { color: var(--dim); }