Update Misty Coaching Dashboard und Analyse
This commit is contained in:
parent
a75a9b1177
commit
92a8363f5d
6 changed files with 304 additions and 62 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
*.wav
|
||||
session_daten.json
|
||||
*.save
|
||||
33
analyse.py
33
analyse.py
|
|
@ -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:
|
||||
|
|
|
|||
51
dashboard.py
51
dashboard.py
|
|
@ -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
12
fuellwoerter.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
"äh",
|
||||
"ähm",
|
||||
"ehm",
|
||||
"mhm",
|
||||
"hm",
|
||||
"also",
|
||||
"sozusagen",
|
||||
"irgendwie",
|
||||
"test",
|
||||
"nochmal test"
|
||||
]
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue