Add spoken tool progress acknowledgements

This commit is contained in:
Mikei386
2026-08-23 13:54:39 +02:00
parent 4ae8ab948e
commit 727827ac32
12 changed files with 219 additions and 5 deletions
+1
View File
@@ -19,6 +19,7 @@ logs/
*.log
*.wav
*.mp3
!platform/openwebui/theme/tool-status/*.mp3
*.png
*.jpg
*.jpeg
+3
View File
@@ -71,6 +71,9 @@ Dark-Theme namens **Midnight Aurora**. CSS und Start-Loader liegen unter
`platform/openwebui/theme/` und werden schreibgeschützt in den Container
eingebunden. Der Hintergrund bewegt sich bewusst langsam; Browser mit aktivierter
Option „Bewegung reduzieren“ erhalten automatisch eine unbewegte Variante.
Kurze Werkzeugbestätigungen werden ebenfalls lokal aus statischen Clips
abgespielt. Sie laufen nur bei aktivierter automatischer Sprachausgabe, kosten
keine Modell-Tokens und verraten dem Modell keine zusätzlichen Daten.
## Dokumentation
+1
View File
@@ -710,6 +710,7 @@ services:
- ./platform/openwebui/theme/custom.css:/app/build/static/custom.css:ro
- ./platform/openwebui/theme/loader.js:/app/build/static/loader.js:ro
- ./platform/openwebui/theme/midnight-aurora.svg:/app/build/static/midnight-aurora.svg:ro
- ./platform/openwebui/theme/tool-status:/app/build/static/tool-status:ro
environment:
WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY:?WEBUI_SECRET_KEY is required}"
DEFAULT_MODELS: mikeai-medium
+7
View File
@@ -138,6 +138,13 @@ dem internen Router und dessen aktuellem Schlüssel. Das ist erforderlich, weil
persistente Providerwerte nach einer Schlüsselrotation Vorrang vor den
Container-Umgebungsvariablen haben.
Der Filterinstaller richtet außerdem die lokalen gesprochenen
Werkzeugbestätigungen ein. Die vorgerenderten Ansagen werden über
`/static/tool-status/` read-only ausgeliefert und nur bei aktivierter
automatischer Sprachausgabe abgespielt. Zur Abnahme eine Websuche und eine
Unraid-Abfrage auslösen: Vor längeren Aufrufen muss genau eine kurze passende
Ansage kommen; bei ausgeschalteter automatischer Sprachausgabe bleibt sie aus.
Der Modellinstaller setzt außerdem für den ermittelten OpenWebUI-Benutzer das
native Chat-Hintergrundbild `/static/midnight-aurora.svg`. Das Bild wird durch
Compose read-only eingebunden. `custom.css` verändert bewusst nicht mehr die
+15 -1
View File
@@ -16,7 +16,15 @@ Reihenfolge ist absichtlich festgelegt:
Passwörter, JWTs und private Schlüssel aus Tool-Ergebnissen, bevor sie das
Modell erreichen, sowie aus fertigen Modellantworten. Nutzereingaben und
Authentifizierungswege werden nicht verändert.
5. `MikeAI Local Performance Metrics`, Priorität 90: erfasst nach Abschluss
5. `MikeAI Spoken Tool Status`, Priorität 80: erkennt den ersten echten
Werkzeugaufruf eines Antwortlaufs und löst im Browser genau eine kurze,
passende Ansage für Web, Unraid, Home Assistant, Medienverwaltung oder
sonstige Werkzeuge aus. Die fünf MP3-Clips sind vorgerendert und liegen
unter `platform/openwebui/theme/tool-status/`; dadurch blockiert die Ansage
weder XTTS noch das Werkzeug. Sie wird nur abgespielt, wenn der Benutzer in
OpenWebUI die automatische Sprachausgabe aktiviert hat. Endet ein sehr
schneller Lauf innerhalb der kurzen Wartezeit, wird die Ansage verworfen.
6. `MikeAI Local Performance Metrics`, Priorität 90: erfasst nach Abschluss
ausschließlich technische Zahlen wie Laufzeit, Tokenzähler, Token/s und
Tool-Anzahl. Nutzer-, Chat- und Nachrichten-IDs sowie sämtliche Textinhalte
werden weder geschrieben noch gehasht gespeichert.
@@ -70,6 +78,12 @@ Die rotierende, inhaltsfreie Metrikdatei liegt im persistenten
OpenWebUI-Volume unter `mike-ai-request-metrics.jsonl` (maximal 5 MiB plus eine
Rotation). Sie darf für Benchmarks ausgewertet werden, ohne Chats auszulesen.
Die Werkzeugansagen enthalten weder Prompt- noch Ergebnistext und werden nicht
in die Unterhaltung oder den Modellkontext geschrieben. Der lokale
OpenWebUI-Browser-Event prüft die persönliche Einstellung
`responseAutoPlayback` über denselben Browser-Login. Ein Fehler, eine
Browser-Autoplay-Sperre oder ein fehlender Clip bleibt folgenlos für den Chat.
## Aktionen an Modellantworten
Die globale Action `MikeAI Quick Actions` ergänzt die Nachrichtenleiste um:
@@ -0,0 +1,182 @@
"""
title: MikeAI Spoken Tool Status
author: MikeAI
version: 1.0.0
description: Plays one short, local acknowledgement when a real tool call starts.
"""
from __future__ import annotations
import json
import threading
from pydantic import BaseModel
class Filter:
class Valves(BaseModel):
priority: int = 80
enabled: bool = True
_announced: set[str] = set()
_lock = threading.Lock()
def __init__(self):
self.valves = self.Valves()
self.toggle = False
@staticmethod
def _key(metadata: dict | None) -> str:
if isinstance(metadata, dict):
value = metadata.get("message_id") or metadata.get("id") or metadata.get("chat_id")
if value:
return str(value)
return "current"
@staticmethod
def _tool_names(event: dict) -> list[str]:
names: list[str] = []
candidates = []
direct = event.get("tool_calls")
if direct:
candidates.append(direct)
for choice in event.get("choices") or []:
if not isinstance(choice, dict):
continue
for container_name in ("delta", "message"):
container = choice.get(container_name)
if isinstance(container, dict) and container.get("tool_calls"):
candidates.append(container["tool_calls"])
for candidate in candidates:
calls = candidate if isinstance(candidate, list) else [candidate]
for call in calls:
if not isinstance(call, dict):
continue
function = call.get("function")
if isinstance(function, dict) and function.get("name"):
names.append(str(function["name"]))
elif call.get("name"):
names.append(str(call["name"]))
return names
@staticmethod
def _category(names: list[str]) -> str:
joined = " ".join(names).lower().replace("-", "_")
if any(token in joined for token in ("local_web", "web_search", "web_compare", "web_research")):
return "web"
if any(token in joined for token in ("unraid", "mua_")):
return "unraid"
if any(token in joined for token in ("homeassistant", "home_assistant", "hass_")):
return "homeassistant"
if any(token in joined for token in ("sonarr", "radarr", "arr_")):
return "media"
return "generic"
async def inlet(self, body: dict, __metadata__: dict = None, **kwargs) -> dict:
if self.valves.enabled:
key = self._key(__metadata__)
with self._lock:
self._announced.discard(key)
return body
async def stream(
self,
event: dict,
__metadata__: dict = None,
__event_emitter__=None,
**kwargs,
) -> dict:
if not self.valves.enabled or __event_emitter__ is None or not isinstance(event, dict):
return event
names = self._tool_names(event)
if not names:
return event
key = self._key(__metadata__)
with self._lock:
if key in self._announced:
return event
self._announced.add(key)
if len(self._announced) > 1024:
self._announced.clear()
self._announced.add(key)
category = self._category(names)
javascript = f"""
(() => {{
const category = {json.dumps(category)};
const key = {json.dumps(key)};
const allowed = ['web', 'unraid', 'homeassistant', 'media', 'generic'];
const safeCategory = allowed.includes(category) ? category : 'generic';
const timers = window.__mikeAIToolStatusTimers ||= new Map();
if (timers.has(key)) return;
const timer = window.setTimeout(async () => {{
timers.delete(key);
let enabled = false;
try {{
const local = JSON.parse(localStorage.getItem('settings') || '{{}}');
enabled = (local.ui || local).responseAutoPlayback === true;
const token = localStorage.getItem('token');
if (token) {{
const response = await fetch('/api/v1/users/user/settings', {{
headers: {{ Authorization: `Bearer ${{token}}` }},
cache: 'no-store'
}});
if (response.ok) {{
const settings = await response.json();
enabled = (settings.ui || settings).responseAutoPlayback === true;
}}
}}
}} catch (_) {{}}
if (!enabled) return;
try {{
const audio = new Audio(`/static/tool-status/${{safeCategory}}.mp3?status=v1`);
audio.preload = 'auto';
audio.volume = 0.92;
await audio.play();
}} catch (_) {{}}
}}, 850);
timers.set(key, timer);
}})();
""".strip()
try:
await __event_emitter__(
{"type": "execute", "data": {"code": javascript}}
)
except Exception:
pass
return event
async def outlet(
self,
body: dict,
__metadata__: dict = None,
__event_emitter__=None,
**kwargs,
) -> dict:
key = self._key(__metadata__)
with self._lock:
self._announced.discard(key)
if __event_emitter__ is not None:
javascript = f"""
(() => {{
const key = {json.dumps(key)};
const timers = window.__mikeAIToolStatusTimers;
const timer = timers && timers.get(key);
if (timer !== undefined) {{
window.clearTimeout(timer);
timers.delete(key);
}}
}})();
""".strip()
try:
await __event_emitter__(
{"type": "execute", "data": {"code": javascript}}
)
except Exception:
pass
return body
+10 -4
View File
@@ -9,7 +9,7 @@ ACTION_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/actions" && pwd)
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
for file in reasoning_default_off.py thinking.py stability_guard.py secret_redaction.py local_performance_metrics.py; do
for file in reasoning_default_off.py thinking.py stability_guard.py secret_redaction.py spoken_tool_status.py local_performance_metrics.py; do
[[ -s $FILTER_DIR/$file ]] || die "Filterdatei fehlt: $file"
done
[[ -s $ACTION_DIR/quick_actions.py ]] || die "Actiondatei fehlt: quick_actions.py"
@@ -58,10 +58,11 @@ if not {"key", "value", "updated_at"} <= config_columns:
owner = requested_owner
if not owner:
existing = con.execute(
"select user_id from function where id in (?, ?, ?, ?, ?, ?) order by id limit 1",
"select user_id from function where id in (?, ?, ?, ?, ?, ?, ?) order by id limit 1",
(
"reasoning_default_off", "thinking", "stability_guard",
"secret_redaction", "local_performance_metrics", "quick_actions",
"secret_redaction", "spoken_tool_status", "local_performance_metrics",
"quick_actions",
),
).fetchone()
if existing:
@@ -80,6 +81,11 @@ functions = [
("thinking", "Thinking", "filter", 20, filter_dir, ""),
("stability_guard", "MikeAI Stability Guard", "filter", 30, filter_dir, ""),
("secret_redaction", "MikeAI Secret Redaction", "filter", 40, filter_dir, ""),
(
"spoken_tool_status", "MikeAI Spoken Tool Status", "filter", 80, filter_dir,
"Spielt bei aktiver automatischer Sprachausgabe einmalig eine kurze lokale Ansage, "
"sobald ein echtes Werkzeug gestartet wird.",
),
("local_performance_metrics", "MikeAI Local Performance Metrics", "filter", 90, filter_dir, ""),
(
"quick_actions", "MikeAI Quick Actions", "action", 100, action_dir,
@@ -222,7 +228,7 @@ with con:
)
print(
"OpenWebUI konfiguriert: Default Off=10, Thinking=20, "
"Stability Guard=30, Secret Redaction=40, Local Metrics=90, "
"Stability Guard=30, Secret Redaction=40, Spoken Tool Status=80, Local Metrics=90, "
"Quick Actions=100, Folgefragen=aus, Piper-TTS=aktiv, Werkzeugwahl=optimiert"
)
PY
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.