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:
1 parent
e6a85176c4
commit
53e4a777b4
8 files changed
+60
-25
No files matched your search
@@ -981,9 +981,12 @@ DRV
|
||||
# there is no address the checker could dial, and a card that can never be
|
||||
# green is worse than no card. The façade carries the pair's signal — and
|
||||
# since the 2026-10-18 deploy that signal is a real probe (`wip: false`), not
|
||||
# a placeholder. It stays `showLink: false` and admin-only: an HTTP API has no
|
||||
# page to open, and the checker counts ANY HTTP response as up, so the card is
|
||||
# honest without a link and without a `/health` path in the probe.
|
||||
# a placeholder. It stays `showLink: false`: an HTTP API has no page to open,
|
||||
# and the checker counts ANY HTTP response as up, so the card is honest
|
||||
# without a link and without a `/health` path in the probe.
|
||||
# 2026-11-04: the card moved from admin to the user page's Model Information
|
||||
# block — the household guide already documents the voices (/docs#models), so
|
||||
# the family reading about them should also see whether the door is up.
|
||||
python3 - "$REPO_ROOT" <<'PY'
|
||||
import json, os, sys
|
||||
root = sys.argv[1]
|
||||
@@ -996,8 +999,10 @@ assert fts["wip"] is False, \
|
||||
"the endpoint is deployed; a wip card would hide its real state: " + repr(fts.get("wip"))
|
||||
assert fts["showLink"] is False and fts["url"] == "", \
|
||||
"an API endpoint has no UI to link to — it is a status card, not a tile"
|
||||
assert fts["category"] == "admin", \
|
||||
"consumers dial this; it is not an app the user homepage should advertise"
|
||||
assert fts["category"] == "user" and fts["section"] == "AI", \
|
||||
"the voice endpoint belongs to the user page's Model Information block: " + repr(fts["category"])
|
||||
assert isinstance(fts.get("model"), dict) and fts["model"].get("role"), \
|
||||
"the card renders as a Model Information card, which needs a model object"
|
||||
assert os.path.exists(f"{root}/shared/homepage{fts['art']}"), f"missing art {fts['art']}"
|
||||
PY
|
||||
}
|
||||
|
||||
+21
-11
@@ -76,23 +76,33 @@ PY
|
||||
|
||||
@test "the Model Information component is complete: text, art, voices, warnings" {
|
||||
# The fused model panel is the family's only description of the fleet.
|
||||
# Every model card must carry: equivalence note, supervision level, the
|
||||
# unit art, at least one playable voice line, and (for uncensored models)
|
||||
# the legal warning. The compaction recommendation must name the zealot.
|
||||
# Every card must carry: role, spec, use, an inputs list, and art that
|
||||
# exists. A card with a StarCraft unit identity (a chat model) must also
|
||||
# carry the equivalence note, the supervision level, and a playable unit
|
||||
# voice line whose file ships; a unit-less tool card (frank-tts speaks, it
|
||||
# does not chat) quotes no unit, so it carries the ask-Frank permission
|
||||
# note instead. Uncensored models must carry the legal warning. The
|
||||
# compaction recommendation must name the zealot.
|
||||
python3 - "$REPO_ROOT/shared/homepage/services.json" "$REPO_ROOT/shared/homepage" <<'PY'
|
||||
import json, os, sys
|
||||
services = json.load(open(sys.argv[1]))
|
||||
root = sys.argv[2]
|
||||
fleet = [s for s in services if "model" in s]
|
||||
assert {s["name"] for s in fleet} == {"frank-dragoon", "frank-archon", "frank-zealot"}, [s["name"] for s in fleet]
|
||||
assert {s["name"] for s in fleet} == {"frank-dragoon", "frank-archon", "frank-zealot", "frank-tts"}, [s["name"] for s in fleet]
|
||||
for s in fleet:
|
||||
m = s["model"]
|
||||
assert m["equiv"].strip(), f"{s['name']} has no equivalence note"
|
||||
assert m["supervision"].strip(), f"{s['name']} has no supervision note"
|
||||
assert m["sfx"], f"{s['name']} has no voice line"
|
||||
assert os.path.isfile(os.path.join(root, "img", "unit-" + m["unit"] + ".jpg")), f"{s['name']} art missing"
|
||||
for f in m["sfx"] + m.get("voice", {}).get("sfx", []):
|
||||
assert m["role"].strip() and m["spec"].strip() and m["use"].strip(), f"{s['name']} needs role, spec and use"
|
||||
assert m.get("inputs"), f"{s['name']} has no inputs list"
|
||||
art = s.get("art") or "img/unit-%s.jpg" % m["unit"]
|
||||
assert os.path.isfile(os.path.join(root, art.lstrip("/"))), f"{s['name']} art missing: {art}"
|
||||
for f in list(m.get("sfx", [])) + m.get("voice", {}).get("sfx", []):
|
||||
assert os.path.isfile(os.path.join(root, "sfx", f)), f"{s['name']} voice {f} missing"
|
||||
if m.get("unit"):
|
||||
assert m["equiv"].strip(), f"{s['name']} has no equivalence note"
|
||||
assert m["supervision"].strip(), f"{s['name']} has no supervision note"
|
||||
assert m.get("sfx"), f"{s['name']} has no voice line"
|
||||
else:
|
||||
assert m.get("permission", "").strip(), f"{s['name']} has no permission note"
|
||||
zealot = next(s for s in fleet if s["name"] == "frank-zealot")
|
||||
assert "compaction" in zealot["model"].get("compaction", ""), "zealot must be recommended as compaction model"
|
||||
assert zealot["model"].get("compaction", "") != "", "compaction advice must be its own visible field"
|
||||
@@ -102,8 +112,8 @@ for s in fleet:
|
||||
uncensored = "ncensored" in m["spec"] or "bliterated" in m["spec"]
|
||||
if uncensored:
|
||||
assert "legal" in m.get("warn", "").lower(), f"{s['name']} is uncensored but has no legal warning"
|
||||
assert [s["name"] for s in fleet] == ["frank-zealot", "frank-dragoon", "frank-archon"], \
|
||||
"card render order must be zealot, dragoon, archon: " + str([s["name"] for s in fleet])
|
||||
assert [s["name"] for s in fleet] == ["frank-zealot", "frank-dragoon", "frank-archon", "frank-tts"], \
|
||||
"card render order must be zealot, dragoon, archon, then the voice tool: " + str([s["name"] for s in fleet])
|
||||
w = {s["name"]: s["model"].get("warn", "") for s in fleet}
|
||||
assert "legal risk" in w["frank-dragoon"].lower() and "everybody" in w["frank-dragoon"].lower(), \
|
||||
"the abliterated endpoint must carry the common legal warning"
|
||||
|
||||
Reference in new issue
Block a user