Update Misty Coaching Dashboard und Analyse

This commit is contained in:
TiffanyBrugger 2026-05-06 15:33:47 +00:00
parent a75a9b1177
commit 92a8363f5d
6 changed files with 304 additions and 62 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
__pycache__/
*.pyc
*.wav
session_daten.json
*.save

View file

@ -8,18 +8,21 @@ from datetime import datetime
from config import MISTY_IP
DATEN_DATEI = "session_daten.json"
FUELLWOERTER_DATEI = "fuellwoerter.json"
FUELLWOERTER = [
r'\bäh\b',
r'\bähm\b',
r'\behm\b',
r'\bmhm\b',
r'\bhm\b',
r'\bhalt\b',
r'\balso\b',
r'\bsozusagen\b',
r'\birgendwie\b',
]
# Modell einmal beim Start laden
print("--- Lade Whisper-Modell ---")
_whisper_model = whisper.load_model("base")
def lade_fuellwoerter():
if os.path.exists(FUELLWOERTER_DATEI):
with open(FUELLWOERTER_DATEI, "r", encoding="utf-8") as f:
woerter = json.load(f)
return [rf'\b{w}\b' for w in woerter]
return [
r'\bäh\b', r'\bähm\b', r'\behm\b', r'\bmhm\b', r'\bhm\b',
r'\bhalt\b', r'\balso\b', r'\bsozusagen\b', r'\birgendwie\b',
]
def setze_status(status):
if os.path.exists(DATEN_DATEI):
@ -55,8 +58,7 @@ def speichere_session(text, anzahl, gefundene_woerter, tempo, feedback, gesicht)
def analysiere(datei, echte_dauer=None):
setze_status("analysierend")
print("--- Whisper Analyse läuft ---")
model = whisper.load_model("base")
result = model.transcribe(
result = _whisper_model.transcribe(
datei,
language="German",
initial_prompt="Dies ist eine Präsentation auf Deutsch. Äh, ähm, sozusagen, halt, also, irgendwie.",
@ -64,12 +66,13 @@ def analysiere(datei, echte_dauer=None):
beam_size=5,
best_of=5
)
# Originaltext für Anzeige im Dashboard (mit Groß-/Kleinschreibung)
text_original = result["text"].strip()
# Kleinbuchstabentext nur für Füllworterkennung
text_lower = text_original.lower()
print(f"Erkannter Text: {text_original}")
# Füllwörter aus Datei laden (bei jeder Analyse aktuell)
FUELLWOERTER = lade_fuellwoerter()
anzahl = 0
gefundene_woerter = {}
for muster in FUELLWOERTER:

View file

@ -1,25 +1,68 @@
from flask import Flask, render_template, jsonify
from flask import Flask, render_template, jsonify, request, redirect, url_for, Response
import json
import os
import time
app = Flask(__name__, static_folder='static')
DATEN_DATEI = "session_daten.json"
FUELLWOERTER_DATEI = "fuellwoerter.json"
def lade_daten():
if os.path.exists(DATEN_DATEI):
with open(DATEN_DATEI, "r", encoding="utf-8") as f:
return json.load(f)
return {"status": "wartend", "sessions": []}
return {"status": "wartend", "sessions": [], "fehler": None}
def lade_fuellwoerter():
if os.path.exists(FUELLWOERTER_DATEI):
with open(FUELLWOERTER_DATEI, "r", encoding="utf-8") as f:
return json.load(f)
return ["äh", "ähm", "ehm", "mhm", "hm", "halt", "also", "sozusagen", "irgendwie"]
def speichere_fuellwoerter(woerter):
with open(FUELLWOERTER_DATEI, "w", encoding="utf-8") as f:
json.dump(woerter, f, ensure_ascii=False, indent=2)
@app.route("/")
def index():
daten = lade_daten()
return render_template("dashboard.html", daten=daten)
fuellwoerter = lade_fuellwoerter()
return render_template("dashboard.html", daten=daten, fuellwoerter=fuellwoerter)
@app.route("/api/daten")
def api_daten():
return jsonify(lade_daten())
@app.route("/stream")
def stream():
def event_stream():
letzter_stand = None
while True:
daten = lade_daten()
aktueller_stand = json.dumps(daten, ensure_ascii=False)
if aktueller_stand != letzter_stand:
letzter_stand = aktueller_stand
yield f"data: {aktueller_stand}\n\n"
time.sleep(1)
return Response(event_stream(), mimetype="text/event-stream")
@app.route("/fuellwort/hinzufuegen", methods=["POST"])
def fuellwort_hinzufuegen():
woerter = lade_fuellwoerter()
neues_wort = request.form.get("wort", "").strip().lower()
if neues_wort and neues_wort not in woerter:
woerter.append(neues_wort)
speichere_fuellwoerter(woerter)
return redirect(url_for("index"))
@app.route("/fuellwort/entfernen/<wort>")
def fuellwort_entfernen(wort):
woerter = lade_fuellwoerter()
if wort in woerter:
woerter.remove(wort)
speichere_fuellwoerter(woerter)
return redirect(url_for("index"))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
app.run(host="0.0.0.0", port=5000, debug=True, threaded=True)

12
fuellwoerter.json Normal file
View file

@ -0,0 +1,12 @@
[
"äh",
"ähm",
"ehm",
"mhm",
"hm",
"also",
"sozusagen",
"irgendwie",
"test",
"nochmal test"
]

View file

@ -15,13 +15,35 @@ aufnahme_laeuft = False
aufnahme_start = 0
def setze_neutral():
requests.post(f"http://{MISTY_IP}/api/images/display", json={"FileName": "e_DefaultContent.jpg"})
try:
requests.post(f"http://{MISTY_IP}/api/images/display",
json={"FileName": "e_DefaultContent.jpg"}, timeout=5)
except Exception:
pass
def reset_session_daten():
with open(DATEN_DATEI, "w", encoding="utf-8") as f:
json.dump({"status": "wartend", "sessions": []}, f, ensure_ascii=False, indent=2)
json.dump({"status": "wartend", "sessions": [], "fehler": None}, f, ensure_ascii=False, indent=2)
print("--- Session-Daten zurückgesetzt ---")
def pruefe_misty_verbindung():
try:
r = requests.get(f"http://{MISTY_IP}/api/device", timeout=5)
return r.status_code == 200
except Exception:
return False
def setze_fehler(nachricht):
if os.path.exists(DATEN_DATEI):
with open(DATEN_DATEI, "r", encoding="utf-8") as f:
daten = json.load(f)
else:
daten = {"status": "fehler", "sessions": [], "fehler": nachricht}
daten["status"] = "fehler"
daten["fehler"] = nachricht
with open(DATEN_DATEI, "w", encoding="utf-8") as f:
json.dump(daten, f, ensure_ascii=False, indent=2)
def coaching_prozess():
global laeuft_gerade, aufnahme_laeuft, aufnahme_start
laeuft_gerade = True
@ -31,14 +53,26 @@ def coaching_prozess():
setze_status("aufnehmend")
print("--- Fuß gedrückt! Starte Coaching ---")
requests.post(f"http://{MISTY_IP}/api/tts/speak", json={
"text": "Ich höre dir zu. Drücke meinen Fuß nochmal wenn du fertig bist.",
"voice": "de-de-x-deb-local"
})
try:
requests.post(f"http://{MISTY_IP}/api/tts/speak", json={
"text": "Ich höre dir zu. Drücke meinen Fuß nochmal wenn du fertig bist.",
"voice": "de-de-x-deb-local"
}, timeout=5)
except Exception as e:
print(f"❌ Fehler bei TTS: {e}")
time.sleep(4)
print("--- Aufnahme läuft ---")
requests.post(f"http://{MISTY_IP}/api/audio/record/start", json={"FileName": DATEI_NAME})
try:
requests.post(f"http://{MISTY_IP}/api/audio/record/start",
json={"FileName": DATEI_NAME}, timeout=5)
except Exception as e:
print(f"❌ Fehler beim Starten der Aufnahme: {e}")
setze_fehler(f"Aufnahme konnte nicht gestartet werden: {e}")
laeuft_gerade = False
return
aufnahme_start = time.time()
while aufnahme_laeuft:
@ -46,16 +80,29 @@ def coaching_prozess():
echte_dauer = time.time() - aufnahme_start
print(f"--- Aufnahme gestoppt ({round(echte_dauer)} Sekunden) ---")
requests.post(f"http://{MISTY_IP}/api/audio/record/stop")
try:
requests.post(f"http://{MISTY_IP}/api/audio/record/stop", timeout=5)
except Exception as e:
print(f"❌ Fehler beim Stoppen der Aufnahme: {e}")
time.sleep(2)
print("--- Übertragung der Audio-Datei ---")
r = requests.get(f"http://{MISTY_IP}/api/audio?FileName={DATEI_NAME}")
if r.status_code == 200:
with open(DATEI_NAME, 'wb') as f:
f.write(r.content)
else:
print("Download-Fehler!")
try:
r = requests.get(f"http://{MISTY_IP}/api/audio?FileName={DATEI_NAME}", timeout=20)
if r.status_code == 200:
with open(DATEI_NAME, 'wb') as f:
f.write(r.content)
else:
print("Download-Fehler!")
setze_fehler("Audiodatei konnte nicht heruntergeladen werden.")
setze_status("wartend")
laeuft_gerade = False
return
except Exception as e:
print(f"❌ Fehler beim Download: {e}")
setze_fehler(f"Verbindung zu Misty verloren: {e}")
setze_status("wartend")
laeuft_gerade = False
return
@ -94,12 +141,22 @@ def on_open(ws):
def on_error(ws, error):
print(f"❌ WebSocket Fehler: {error}")
setze_fehler(f"WebSocket Fehler: {error}")
def on_close(ws, close_status_code, close_msg):
print(f"❌ Verbindung getrennt: {close_status_code} - {close_msg}")
setze_neutral()
setze_status("wartend")
# Misty Verbindung prüfen vor dem Start
print("--- Prüfe Verbindung zu Misty ---")
if not pruefe_misty_verbindung():
print(f"❌ Misty nicht erreichbar unter {MISTY_IP}")
print("Bitte IP in config.py prüfen und Misty einschalten!")
setze_fehler(f"Misty nicht erreichbar unter {MISTY_IP}. Bitte IP prüfen und Misty einschalten.")
else:
print(f"✅ Misty erreichbar unter {MISTY_IP}")
ws = websocket.WebSocketApp(
f"ws://{MISTY_IP}/pubsub",
on_open=on_open,

View file

@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="3">
<title>Misty Rhetorik-Coach</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
@ -58,16 +57,10 @@ header {
padding: 10px 20px; border-radius: 100px;
font-size: 13px; font-weight: 700;
}
.status-wartend { background: var(--green-light); color: var(--green); }
.status-aufnehmend { background: var(--red-light); color: var(--red); }
.status-analysierend { background: var(--amber-light); color: var(--amber); }
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
animation: blink 1.5s ease-in-out infinite;
}
.status-wartend .status-dot { background: var(--green); }
.status-aufnehmend .status-dot { background: var(--red); }
.status-analysierend .status-dot { background: var(--amber); }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
.grid-top {
display: grid;
@ -191,7 +184,7 @@ header {
font-size: 12px; font-weight: 600; color: var(--muted);
flex-shrink: 0; min-width: 190px; justify-content: flex-end;
}
.v-face { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }
.v-face { width: 40px; height: 28px; border-radius: 6px; object-fit: cover; }
.c-green { color: var(--green); }
.c-amber { color: var(--amber); }
.c-red { color: var(--red); }
@ -199,7 +192,57 @@ header {
.empty-state {
text-align: center; padding: 40px;
color: var(--muted); font-size: 15px; font-weight: 600;
line-height: 1.8;
}
.fuellwort-card {
background: var(--white);
border-radius: 20px; padding: 24px;
border: 1px solid var(--border);
box-shadow: 0 2px 12px rgba(37,99,235,0.06);
margin-bottom: 20px;
}
.fuellwort-liste {
display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0;
}
.fuellwort-tag {
display: inline-flex; align-items: center; gap: 8px;
background: var(--blue-light); border: 1px solid var(--blue-mid);
color: var(--dark); padding: 6px 12px;
border-radius: 8px; font-size: 13px; font-weight: 700;
}
.fuellwort-tag a {
color: var(--red); text-decoration: none;
font-size: 16px; font-weight: 900; line-height: 1;
}
.fuellwort-tag a:hover { color: #991b1b; }
.fuellwort-form {
display: flex; gap: 10px; margin-top: 14px;
}
.fuellwort-input {
flex: 1; padding: 10px 16px;
border: 2px solid var(--border); border-radius: 10px;
font-family: 'Nunito', sans-serif; font-size: 14px;
font-weight: 600; color: var(--dark);
outline: none;
}
.fuellwort-input:focus { border-color: var(--blue); }
.fuellwort-btn {
background: var(--blue); color: white;
border: none; border-radius: 10px;
padding: 10px 20px; font-family: 'Nunito', sans-serif;
font-size: 14px; font-weight: 700; cursor: pointer;
}
.fuellwort-btn:hover { background: #1e40af; }
.fehler-box {
background: var(--red-light);
border: 2px solid var(--red);
border-radius: 20px; padding: 24px;
margin-bottom: 20px;
display: flex; align-items: center; gap: 16px;
}
.fehler-icon { font-size: 32px; flex-shrink: 0; }
.fehler-titel { font-size: 16px; font-weight: 800; color: var(--red); margin-bottom: 4px; }
.fehler-text { font-size: 14px; color: #7f1d1d; font-weight: 500; }
</style>
</head>
<body>
@ -209,21 +252,55 @@ header {
<div class="brand-avatar">🤖</div>
<div>
<div class="brand-name">Misty Rhetorik-Coach</div>
<div class="brand-sub">PH Weingarten · Modul M2 Entwicklung Interaktiver Medien</div>
<div class="brand-sub">PH Weingarten · Modul M2 · WS 2025/26</div>
</div>
</div>
<div class="status-pill status-{{ daten.status }}">
<div class="status-dot"></div>
{% if daten.status == "wartend" %}Wartet auf Fußdruck
{% elif daten.status == "aufnehmend" %}Aufnahme läuft...
{% elif daten.status == "analysierend" %}Analyse läuft...
{% endif %}
<div class="status-pill" id="status-pill"
style="background:{% if daten.status == 'fehler' %}var(--red-light);color:var(--red){% else %}var(--green-light);color:var(--green){% endif %};">
<div class="status-dot" id="status-dot"
style="background:{% if daten.status == 'fehler' %}var(--red){% else %}var(--green){% endif %};"></div>
<span id="status-text">
{% if daten.status == "wartend" %}Wartet auf Fußdruck
{% elif daten.status == "aufnehmend" %}Aufnahme läuft...
{% elif daten.status == "analysierend" %}Analyse läuft...
{% elif daten.status == "fehler" %}Fehler aufgetreten
{% endif %}
</span>
</div>
</header>
<!-- Fehlermeldung -->
{% if daten.status == "fehler" and daten.fehler %}
<div class="fehler-box">
<div class="fehler-icon">⚠️</div>
<div>
<div class="fehler-titel">Verbindungsfehler</div>
<div class="fehler-text">{{ daten.fehler }}</div>
</div>
</div>
{% endif %}
<!-- Füllwörter-Verwaltung -->
<div class="fuellwort-card">
<div class="card-label">Füllwörter verwalten</div>
<div class="fuellwort-liste">
{% for wort in fuellwoerter %}
<span class="fuellwort-tag">
{{ wort }}
<a href="/fuellwort/entfernen/{{ wort }}" title="Entfernen">×</a>
</span>
{% endfor %}
</div>
<form class="fuellwort-form" method="POST" action="/fuellwort/hinzufuegen">
<input class="fuellwort-input" type="text" name="wort" placeholder="Neues Füllwort eingeben..." autocomplete="off">
<button class="fuellwort-btn" type="submit">+ Hinzufügen</button>
</form>
</div>
<!-- Hauptinhalt -->
<div id="hauptinhalt">
{% if daten.sessions %}
{% set letzte = daten.sessions[-1] %}
<div class="grid-top">
<div class="card card-blue">
<span class="dots">· · · · ·<br>· · · · ·<br>· · · · ·</span>
@ -241,7 +318,6 @@ header {
{% endif %}
{% endif %}
</div>
<div class="card">
<div class="card-label">Sprechtempo</div>
<div class="metric-num c-blue">{{ letzte.tempo }}</div>
@ -273,7 +349,6 @@ header {
<div class="badge badge-red">zu schnell</div>
{% endif %}
</div>
<div class="card">
<div class="card-label">Mistys Reaktion</div>
<div class="face-card">
@ -284,7 +359,6 @@ header {
</div>
</div>
</div>
<div class="grid-bottom">
<div class="card">
<div class="card-label">Erkannter Text</div>
@ -297,14 +371,12 @@ header {
</div>
{% endif %}
</div>
<div class="card">
<div class="card-label">Feedback von Misty</div>
<div class="feedback-icon">💬</div>
<div class="feedback-body">{{ letzte.feedback }}</div>
</div>
</div>
{% if daten.sessions|length > 1 %}
<div class="card">
<div class="card-label">Sessionverlauf</div>
@ -330,19 +402,69 @@ header {
{% endfor %}
</div>
{% endif %}
{% else %}
<div class="card">
<div class="empty-state">
<div class="empty-state">
🤖 Willkommen beim Misty Rhetorik-Coach!<br><br>
Hier werden die Ergebnisse jeder Präsentation Füllwörter, Sprechtempo und Mistys Feedback gezeigt.<br><br>
Sobald der erste Durchgang abgeschlossen ist, erscheinen die Ergebnisse automatisch.<br><br>
Im Sessionverlauf ist zu sehen, wie sich die Leistung über mehrere Durchgänge hinweg verbessert.<br><br>
<strong>Drücke Mistys Fuß um die Aufnahme zu starten!</strong>
</div>
🤖 Willkommen beim Misty Rhetorik-Coach!<br><br>
Hier werden die Ergebnisse jeder Präsentation Füllwörter, Sprechtempo und Mistys Feedback gezeigt.<br><br>
Sobald der erste Durchgang abgeschlossen ist, erscheinen die Ergebnisse automatisch.<br><br>
Im Sessionverlauf ist zu sehen, wie sich die Leistung über mehrere Durchgänge hinweg verbessert.<br><br>
<strong>👣 Drücke Mistys Fuß um die Aufnahme zu starten!</strong>
</div>
</div>
{% endif %}
</div>
<script>
const statusPill = document.getElementById('status-pill');
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
function aktualisiereStatus(status) {
if (status === 'wartend') {
statusPill.style.background = 'var(--green-light)';
statusPill.style.color = 'var(--green)';
statusDot.style.background = 'var(--green)';
statusText.textContent = 'Wartet auf Fußdruck';
} else if (status === 'aufnehmend') {
statusPill.style.background = 'var(--red-light)';
statusPill.style.color = 'var(--red)';
statusDot.style.background = 'var(--red)';
statusText.textContent = 'Aufnahme läuft...';
} else if (status === 'analysierend') {
statusPill.style.background = 'var(--amber-light)';
statusPill.style.color = 'var(--amber)';
statusDot.style.background = 'var(--amber)';
statusText.textContent = 'Analyse läuft...';
} else if (status === 'fehler') {
statusPill.style.background = 'var(--red-light)';
statusPill.style.color = 'var(--red)';
statusDot.style.background = 'var(--red)';
statusText.textContent = 'Fehler aufgetreten';
}
}
let letzteSessionAnzahl = {{ daten.sessions|length }};
let letzterFehler = {{ (daten.fehler if daten.fehler is defined else none)|tojson }};
const evtSource = new EventSource('/stream');
evtSource.onmessage = function(e) {
const daten = JSON.parse(e.data);
aktualisiereStatus(daten.status);
const neueAnzahl = daten.sessions ? daten.sessions.length : 0;
const neuerFehler = daten.fehler || null;
if (neueAnzahl !== letzteSessionAnzahl || neuerFehler !== letzterFehler) {
letzteSessionAnzahl = neueAnzahl;
letzterFehler = neuerFehler;
window.location.reload();
}
};
evtSource.onerror = function() {
console.log('SSE Verbindung unterbrochen, versuche neu...');
};
</script>
</body>
</html>