first commit

This commit is contained in:
TiffanyBrugger 2026-05-06 12:46:05 +00:00
commit a75a9b1177
28 changed files with 1466 additions and 0 deletions

Binary file not shown.

Binary file not shown.

153
analyse.py Normal file
View file

@ -0,0 +1,153 @@
import whisper
import requests
import re
import json
import os
from datetime import datetime
from config import MISTY_IP
DATEN_DATEI = "session_daten.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',
]
def setze_status(status):
if os.path.exists(DATEN_DATEI):
with open(DATEN_DATEI, "r", encoding="utf-8") as f:
daten = json.load(f)
else:
daten = {"status": status, "sessions": []}
daten["status"] = status
with open(DATEN_DATEI, "w", encoding="utf-8") as f:
json.dump(daten, f, ensure_ascii=False, indent=2)
def speichere_session(text, anzahl, gefundene_woerter, tempo, feedback, gesicht):
if os.path.exists(DATEN_DATEI):
with open(DATEN_DATEI, "r", encoding="utf-8") as f:
daten = json.load(f)
else:
daten = {"status": "wartend", "sessions": []}
session = {
"nummer": len(daten["sessions"]) + 1,
"zeit": datetime.now().strftime("%H:%M"),
"text": text,
"fuellwoerter_anzahl": anzahl,
"gefundene_woerter": gefundene_woerter,
"tempo": tempo,
"feedback": feedback,
"gesicht": gesicht
}
daten["sessions"].append(session)
daten["status"] = "wartend"
with open(DATEN_DATEI, "w", encoding="utf-8") as f:
json.dump(daten, f, ensure_ascii=False, indent=2)
def analysiere(datei, echte_dauer=None):
setze_status("analysierend")
print("--- Whisper Analyse läuft ---")
model = whisper.load_model("base")
result = model.transcribe(
datei,
language="German",
initial_prompt="Dies ist eine Präsentation auf Deutsch. Äh, ähm, sozusagen, halt, also, irgendwie.",
temperature=0,
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}")
anzahl = 0
gefundene_woerter = {}
for muster in FUELLWOERTER:
treffer = len(re.findall(muster, text_lower))
if treffer > 0:
wort = muster.replace(r'\b', '')
gefundene_woerter[wort] = treffer
print(f"{wort}: {treffer}x")
anzahl += treffer
print(f"Füllwörter gesamt: {anzahl}")
anzahl_woerter = len(text_lower.split())
if echte_dauer and echte_dauer > 0:
tempo = round((anzahl_woerter / echte_dauer) * 60)
else:
segmente = result["segments"]
if segmente:
dauer = segmente[-1]["end"] - segmente[0]["start"]
tempo = round((anzahl_woerter / dauer) * 60) if dauer > 0 else 0
else:
tempo = 0
print(f"Sprechtempo: {tempo} Wörter/Minute")
return anzahl, text_original, gefundene_woerter, tempo
def formatiere_woerter(gefundene_woerter):
teile = []
for wort, anzahl in gefundene_woerter.items():
if anzahl == 1:
teile.append(f"{wort} einmal")
else:
teile.append(f"{wort} {anzahl}mal")
if len(teile) == 1:
return teile[0]
return ", ".join(teile[:-1]) + " und " + teile[-1]
def gib_feedback(anzahl, gefundene_woerter, tempo, text=""):
fuellwoerter_schlecht = anzahl >= 3
tempo_ok = 90 <= tempo <= 150
if not fuellwoerter_schlecht and tempo_ok:
img = "e_Love.jpg"
elif fuellwoerter_schlecht and not tempo_ok:
img = "e_Sadness.jpg"
else:
img = "e_Contempt.jpg"
if anzahl == 0:
fuellwort_msg = "Keine Füllwörter gefunden. Hervorragend!"
elif anzahl == 1:
woerter_text = formatiere_woerter(gefundene_woerter)
fuellwort_msg = f"Du hattest ein Füllwort. Du hast das Füllwort {woerter_text} benutzt."
elif anzahl <= 3:
woerter_text = formatiere_woerter(gefundene_woerter)
fuellwort_msg = f"Du hattest {anzahl} Füllwörter. Du hast benutzt: {woerter_text}."
else:
woerter_text = formatiere_woerter(gefundene_woerter)
fuellwort_msg = f"Du hattest {anzahl} Füllwörter. Du hast benutzt: {woerter_text}. Bitte übe noch etwas."
if tempo == 0:
tempo_msg = ""
elif tempo < 90:
tempo_msg = "Dein Sprechtempo war etwas zu langsam. Versuche etwas flüssiger zu sprechen."
elif tempo <= 150:
tempo_msg = "Dein Sprechtempo war sehr angenehm."
else:
tempo_msg = "Dein Sprechtempo war etwas zu schnell. Versuche dich etwas zu verlangsamen."
msg = fuellwort_msg
if tempo_msg:
msg += " " + tempo_msg
speichere_session(text, anzahl, gefundene_woerter, tempo, msg, img)
requests.post(f"http://{MISTY_IP}/api/images/display", json={"FileName": img})
requests.post(f"http://{MISTY_IP}/api/tts/speak", json={
"text": msg,
"voice": "de-de-x-deb-local"
})
print(f"Gesicht: {img}")
print(f"Misty sagt: {msg}")

BIN
aufnahme.wav Normal file

Binary file not shown.

4
config.py Normal file
View file

@ -0,0 +1,4 @@
# config.py
# Zentrale Stelle für Netzwerkparameter
MISTY_IP = "192.168.68.65"
RTSP_URL = f"rtsp://{MISTY_IP}:1936"

25
dashboard.py Normal file
View file

@ -0,0 +1,25 @@
from flask import Flask, render_template, jsonify
import json
import os
app = Flask(__name__, static_folder='static')
DATEN_DATEI = "session_daten.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": []}
@app.route("/")
def index():
daten = lade_daten()
return render_template("dashboard.html", daten=daten)
@app.route("/api/daten")
def api_daten():
return jsonify(lade_daten())
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

View file

@ -0,0 +1,38 @@
import requests, json, time
from config import MISTY_IP
ROBOT_IP = MISTY_IP
PORT = 1936
def post(path, payload=None):
url = f"http://{ROBOT_IP}{path}"
r = requests.post(url, data=json.dumps(payload or {}),
headers={"Content-Type": "application/json"},
timeout=10)
print(f"{path} -> {r.status_code}")
try:
print(r.json())
except Exception:
print(r.text)
return r
print("🔌 Enable AV streaming service...")
post("/api/services/avstreaming/enable", {})
print("📡 Start AV streaming (Misty as RTSP server)...")
post("/api/avstreaming/start", {
"url": f"rtspd:{PORT}",
"width": 640,
"height": 480,
"frameRate": 30,
"videoBitRate": 5000000,
"audioBitRate": 128000,
"audioSampleRateHz": 44100,
"userName": None,
"password": None
})
print("⏳ Warte 2 Sekunden bis der RTSP-Server steht...")
time.sleep(2)
print(f"✅ Jetzt sollte gehen: rtsp://{ROBOT_IP}:{PORT}")

View file

@ -0,0 +1,26 @@
import requests
import json
from config import MISTY_IP
def post(path, payload=None):
url = f"http://{MISTY_IP}{path}"
r = requests.post(
url,
data=json.dumps(payload or {}),
headers={"Content-Type": "application/json"},
timeout=5
)
print(f"{path} -> {r.status_code}")
try:
print(r.json())
except Exception:
print(r.text)
print(f"🛑 Stoppe AV Streaming auf {MISTY_IP}...")
post("/api/avstreaming/stop")
print("\n🔌 Deaktiviere AV Streaming Service...")
post("/api/services/avstreaming/disable")
print("\n✅ Misty AV-Streaming vollständig beendet.")

View file

@ -0,0 +1,58 @@
import whisper
import numpy as np
import subprocess
import sys
from config import RTSP_URL
def run_live_stream():
print(f"--- Starte Live-Transkription ---")
print(f"Verbindung zu: {RTSP_URL}")
# 1. Whisper Modell laden (base ist schnell genug für Live)
print("Lade KI-Modell...")
model = whisper.load_model("base")
# 2. FFmpeg Befehl für den Live-Stream
# Wir ziehen Audio direkt von Misty und wandeln es in das Whisper-Format
command = [
'ffmpeg',
'-rtsp_transport', 'tcp', # TCP ist stabiler für den Roboter
'-i', RTSP_URL,
'-ar', '16000', # Whisper braucht 16kHz
'-ac', '1', # Mono
'-f', 's16le', # Raw PCM Format
'-' # Ausgabe an Pipe (stdout)
]
# Startet den FFmpeg-Prozess im Hintergrund
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
print("\n[LIVE] Höre zu... (Strg+C zum Beenden)\n")
try:
while True:
# Wir lesen einen 5-Sekunden Block für die Analyse
# 16000 Samples/s * 2 Bytes/Sample * 5s = 160000 Bytes
raw_audio = process.stdout.read(160000)
if not raw_audio:
break
# In Numpy-Array umwandeln und normalisieren
audio_np = np.frombuffer(raw_audio, dtype=np.int16).astype(np.float32) / 32768.0
# Transkription durchführen
result = model.transcribe(audio_np, fp16=False, language="de")
# Text ausgeben
text = result['text'].strip()
if text:
print(f"Erkannt: {text}")
except KeyboardInterrupt:
print("\nStoppe Live-Stream...")
finally:
process.terminate()
if __name__ == "__main__":
run_live_stream()

Binary file not shown.

53
old/aufnahme.py Normal file
View file

@ -0,0 +1,53 @@
import requests
import time
# --- KONFIGURATION ---
# Die IP deines Roboters
MISTY_IP = "192.168.68.58"
# So soll die Datei auf dem Roboter und dem Server heißen
DATEI_NAME = "praesentation_aufnahme.wav"
# Dauer der Aufnahme in Sekunden
AUFNAHME_DAUER = 10
def aufnahme_starten():
# 1. Misty bescheid geben
print("--- Misty bereitet sich vor ---")
requests.post(f"http://{MISTY_IP}/api/tts/speak", json={
"text": "Ich höre dir jetzt für 10 Sekunden zu. Fang an nach dem ich fertig bin mit sprechen.",
"speechLocale": "de-DE"
})
# Wir warten 4 Sekunden, damit sie sich nicht selbst beim Sprechen aufnimmt
time.sleep(4)
# 2. Aufnahme auf dem Roboter starten
print(f"--- Schritt 1: Aufnahme läuft für {AUFNAHME_DAUER} Sekunden ---")
start_url = f"http://{MISTY_IP}/api/audio/record/start"
requests.post(start_url, json={"FileName": DATEI_NAME})
# Das Programm pausiert hier für 10 Sekunden, während du sprichst
time.sleep(AUFNAHME_DAUER)
# 3. Aufnahme stoppen
print("--- Schritt 2: Aufnahme wird beendet ---")
stop_url = f"http://{MISTY_IP}/api/audio/record/stop"
requests.post(stop_url)
# 2 Sekunden warten, damit die Datei auf dem Roboter-Speicher fertig geschrieben wird
time.sleep(2)
# 4. Datei vom Roboter auf den Ubuntu-Server kopieren
print(f"--- Schritt 3: Datei '{DATEI_NAME}' wird heruntergeladen ---")
download_url = f"http://{MISTY_IP}/api/audio?FileName={DATEI_NAME}"
r = requests.get(download_url)
if r.status_code == 200:
# Die Daten werden binär in eine lokale Datei geschrieben
with open(DATEI_NAME, 'wb') as f:
f.write(r.content)
print(f"Erfolg! Die Datei liegt jetzt bereit für die Analyse.")
else:
print(f"Fehler beim Download: Statuscode {r.status_code}")
if __name__ == "__main__":
aufnahme_starten()

36
old/avstream_test.py Normal file
View file

@ -0,0 +1,36 @@
import requests
import json
import time
ROBOT_IP = "192.168.68.64"
PORT = 1936
def post(path, payload):
url = f"http://{ROBOT_IP}{path}"
r = requests.post(url, data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=10)
print(path, r.status_code)
try:
print(r.json())
except Exception:
print(r.text)
return r
# 1) Enable AV streaming service
post("/api/services/avstreaming/enable", {})
# 2) Start AV streaming (Misty as RTSP server)
payload = {
"url": f"rtspd:{PORT}",
"width": 640,
"height": 480,
"frameRate": 30,
"videoBitRate": 5000000,
"audioBitRate": 128000,
"audioSampleRateHz": 44100,
"userName": None,
"password": None
}
post("/api/avstreaming/start", payload)
print(f"\nJetzt in VLC öffnen: rtsp://{ROBOT_IP}:{PORT}\n")
time.sleep(999999)

97
old/bumper_start.py Normal file
View file

@ -0,0 +1,97 @@
import websocket
import json
import requests
import time
import whisper
import threading
# --- KONFIGURATION ---
MISTY_IP = "192.168.68.58"
DATEI_NAME = "bumper_aufnahme.wav"
AUFNAHME_DAUER = 10
# Flag, um zu verhindern, dass die Analyse mehrfach gleichzeitig startet
laeuft_gerade = False
def coaching_prozess():
global laeuft_gerade
laeuft_gerade = True
# 1. Start-Signal
print("--- Bumper gedrückt! Starte Coaching ---")
requests.post(f"http://{MISTY_IP}/api/tts/speak", json={
"text": "Bumper erkannt. Ich höre dir jetzt für 10 Sekunden zu.",
"speechLocale": "de-DE"
})
time.sleep(4)
# 2. Aufnahme
print(f"--- Aufnahme läuft ({AUFNAHME_DAUER}s) ---")
requests.post(f"http://{MISTY_IP}/api/audio/record/start", json={"FileName": DATEI_NAME})
time.sleep(AUFNAHME_DAUER)
requests.post(f"http://{MISTY_IP}/api/audio/record/stop")
time.sleep(2)
# 3. Download
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!"); laeuft_gerade = False; return
# 4. KI-Analyse
print("--- Whisper Analyse läuft ---")
model = whisper.load_model("base")
result = model.transcribe(DATEI_NAME, language="German", initial_prompt="Äh, ähm.")
text = result["text"].lower()
# Zählen
anzahl_aehm = text.count("ähm")
anzahl_aeh = text.replace("ähm", "TEMP").count("äh")
gesamt = anzahl_aeh + anzahl_aehm
# 5. Feedback
if gesamt > 0:
msg = f"Ich habe {gesamt} Füllwörter gehört. Versuche flüssiger zu sprechen."
img = "e_DisorientedConfused.jpg"
else:
msg = "Hervorragend! Keine Füllwörter gefunden."
img = "e_Joy.jpg"
requests.post(f"http://{MISTY_IP}/api/images/display", json={"FileName": img})
requests.post(f"http://{MISTY_IP}/api/tts/speak", json={"text": msg, "speechLocale": "de-DE"})
print(f"Fertig! Text: {text}")
laeuft_gerade = False
def on_message(ws, message):
global laeuft_gerade
data = json.loads(message)
# Wir prüfen, ob das Event vom Bumper kommt
if "message" in data and "sensor" in data["message"]:
sensor = data["message"]["sensor"]
is_pressed = data["message"]["isPressed"]
# "br" steht für Bumper Right (Rechter Bumper)
if sensor == "br" and is_pressed and not laeuft_gerade:
# Starte den Prozess in einem eigenen Thread, damit die Verbindung nicht blockiert
threading.Thread(target=coaching_prozess).start()
def on_open(ws):
print("Verbindung zu Misty hergestellt. Drücke den RECHTEN BUMPER zum Starten.")
# Wir abonnieren das Bumper-Event
subscribe_msg = {
"Operation": "subscribe",
"Type": "BumpSensor",
"DebounceMs": 50,
"EventName": "BumperPress",
"ReturnProperty": None
}
ws.send(json.dumps(subscribe_msg))
# WebSocket starten
ws = websocket.WebSocketApp(f"ws://{MISTY_IP}/pubsub", on_open=on_open, on_message=on_message)
ws.run_forever()

49
old/detektor.py Normal file
View file

@ -0,0 +1,49 @@
import requests
import base64
import time
from config import MISTY_IP
from analyse import analysiere, gib_feedback
DATEINAME = "aufnahme.wav"
AUFNAHME_DAUER_S = 10
def starte_aufnahme():
print(f"--- Schritt 1: Aufnahme ({AUFNAHME_DAUER_S}s) ---")
try:
requests.delete(f"http://{MISTY_IP}/api/audio?fileName={DATEINAME}", timeout=2)
response = requests.post(f"http://{MISTY_IP}/api/audio/record/start",
json={"fileName": DATEINAME}, timeout=5)
if response.status_code == 200:
print("🔴 Misty hört zu...")
time.sleep(AUFNAHME_DAUER_S)
requests.post(f"http://{MISTY_IP}/api/audio/record/stop", timeout=5)
print("⏹️ Aufnahme beendet.")
time.sleep(3)
return True
except Exception as e:
print(f"❌ Fehler bei Aufnahme: {e}")
return False
def lade_datei():
print("--- Schritt 2: Datei vom Roboter laden ---")
url = f"http://{MISTY_IP}/api/audio?fileName={DATEINAME}&base64=true"
try:
response = requests.get(url, timeout=20)
if response.status_code == 200:
audio_bytes = base64.b64decode(response.json()["result"]["base64"])
with open(DATEINAME, "wb") as f:
f.write(audio_bytes)
print("✅ Datei geladen.")
return True
except Exception as e:
print(f"❌ Fehler beim Laden: {e}")
return False
if __name__ == "__main__":
print("🚀 Rhetorik-Check gestartet")
if starte_aufnahme():
if lade_datei():
anzahl, text = analysiere(DATEINAME)
gib_feedback(anzahl)
print("--- PROGRAMM BEENDET ---")

46
old/live_coach_final.py Normal file
View file

@ -0,0 +1,46 @@
import subprocess
import numpy as np
import whisper
import requests
import time
MISTY_IP = "192.168.68.57"
RTSP_URL = f"rtsp://{MISTY_IP}:1936"
print("Lade Whisper (tiny)...")
model = whisper.load_model("tiny")
def set_misty_led(r, g, b):
try:
requests.post(f"http://{MISTY_IP}/api/led", json={"red": r, "green": g, "blue": b}, timeout=1)
except: pass
ffmpeg_cmd = [
'ffmpeg', '-rtsp_transport', 'tcp', '-i', RTSP_URL,
'-vn', '-f', 's16le', '-ac', '1', '-ar', '16000', '-'
]
process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
set_misty_led(0, 255, 0) # Startet Grün
print(">>> MONITORING LÄUFT. Sprich jetzt!")
try:
# 3-Sekunden-Fenster
chunk_size = 16000 * 2 * 3
while True:
data = process.stdout.read(chunk_size)
if not data: break
audio = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
result = model.transcribe(audio, fp16=False, language="de")
text = result["text"].strip().lower()
if text:
print(f"Erkannt: {text}")
if any(w in text for w in ["äh", "ähm", "halt", "quasi"]):
print("⚠️ FÜLLWORT!")
set_misty_led(255, 0, 0) # Rot bei Fehler
time.sleep(1)
set_misty_led(0, 255, 0) # Zurück zu Grün
except KeyboardInterrupt:
process.terminate()

41
old/start_audio_stream.py Normal file
View file

@ -0,0 +1,41 @@
import requests
import time
from config import MISTY_IP
def setup_misty_audio():
print("--- Hard-Reset der Streaming-Dienste ---")
# 1. Bestehende Streams stoppen
try:
requests.post(f"http://{MISTY_IP}/api/services/avstreaming/stop")
time.sleep(1)
# 2. Audio-Dienst aktivieren
print("1. Aktiviere Dienst...")
requests.post(f"http://{MISTY_IP}/api/services/avstreaming/enable")
# 3. RTSP-Server starten (Port 1936 für Audio)
print("2. Starte RTSP-Server auf Port 1936...")
audio_payload = {
"Port": 1936,
"AudioSource": "Default",
"UserName": None,
"Password": None
}
# Wir nutzen hier wieder den Pfad, der bei dir funktioniert hat
requests.post(f"http://{MISTY_IP}/api/services/avstreaming/audio/start", json=audio_payload)
# 4. Wartezeit für die Hardware
print("3. Warte auf Port-Freigabe (max 10 Sek)...")
for i in range(10):
print(".", end="", flush=True)
time.sleep(1)
print("\nBereit! Der Stream sollte nun unter VLC oder Whisper erreichbar sein.")
except Exception as e:
print(f"\nFehler beim Verbinden mit Misty ({MISTY_IP}): {e}")
if __name__ == "__main__":
setup_misty_audio()

38
old/start_stream.py Normal file
View file

@ -0,0 +1,38 @@
import requests
import json
MISTY_IP = "192.168.68.57"
def try_start(label, payload):
base_url = f"http://{MISTY_IP}/api/avstreaming/start"
print(f"Versuche {label}...")
try:
res = requests.post(base_url, json=payload, timeout=5)
if res.status_code == 200:
print(f"{label} ERFOLGREICH!")
return True
else:
print(f"{label} fehlgeschlagen: {res.status_code} - {res.text}")
except Exception as e:
print(f"Fehler: {e}")
return False
def run_all():
# Reset
requests.post(f"http://{MISTY_IP}/api/avstreaming/stop")
# Variante A: 'URL' großgeschrieben mit null
payload_a = {"URL": None, "Width": 640, "Height": 480, "FrameRate": 15, "VideoBitRate": 1000000, "AudioBitRate": 128000, "AudioSampleRateHz": 16000}
# Variante B: 'Url' kleingeschrieben mit null
payload_b = {"Url": None, "Width": 640, "Height": 480, "FrameRate": 15, "VideoBitRate": 1000000, "AudioBitRate": 128000, "AudioSampleRateHz": 16000}
# Variante C: 'URL' mit lokalem Pfad (Manche Versionen brauchen das)
payload_c = {"URL": "rtsp://127.0.0.1:554/live", "Width": 640, "Height": 480, "FrameRate": 15, "VideoBitRate": 1000000, "AudioBitRate": 128000, "AudioSampleRateHz": 16000}
if not try_start("Variante A (URL: null)", payload_a):
if not try_start("Variante B (Url: null)", payload_b):
try_start("Variante C (Localhost IP)", payload_c)
if __name__ == "__main__":
run_all()

45
old/start_vlc_stream.py Normal file
View file

@ -0,0 +1,45 @@
import requests
import time
from config import MISTY_IP
def start_misty_studio_clone():
print(f"--- Erzwungener Studio-Klon Start ({MISTY_IP}) ---")
# 1. Cleanup: Erst alles stoppen
requests.post(f"http://{MISTY_IP}/api/avstreaming/stop")
time.sleep(2)
# 2. Die exakte Struktur aus deinem Studio-Fund
# WICHTIG: Port muss im Body sein, url muss null sein
payload = {
"url": None,
"width": 0,
"height": 0,
"frameRate": 0,
"videoBitRate": 0,
"audioBitRate": 0,
"audioSampleRateHz": 0,
"userName": None,
"password": None,
"port": 1936
}
# Wir schicken es an den Endpoint, den das Studio nutzt
url = f"http://{MISTY_IP}/api/avstreaming/start"
print("Sende Paket...")
try:
response = requests.post(url, json=payload, timeout=10)
print(f"Status: {response.status_code}")
print(f"Antwort: {response.text}")
if response.status_code == 200 and "Success" in response.text:
print("\n✅ API sagt JA! Teste JETZT VLC.")
else:
print("\n❌ Misty hat das Paket abgelehnt.")
except Exception as e:
print(f"Fehler: {e}")
if __name__ == "__main__":
start_misty_studio_clone()

108
old/test Normal file
View file

@ -0,0 +1,108 @@
import whisper
import numpy as np
import subprocess
import time
import sys
import re
from config import RTSP_URL
# DEFINITION DER FÜLLWÖRTER
FILLER_WORDS = ["äh", "ähhm", "ähm", "mhm", "halt", "quasi", "sozusagen", "eigentlich"]
def analyze_text(text):
"""Sucht nach Füllwörtern im erkannten Text."""
text_clean = re.sub(r'[^\w\s]', '', text.lower())
# Entfernt einzelne Buchstaben/Satzzeichen und splittet in Wörter
words = text_clean.split()
found_fillers = {w: words.count(w) for w in FILLER_WORDS if w in words}
return sum(found_fillers.values()), found_fillers
def run_adaptive_whisper():
print(f"--- M2 Live-Coach: Analyse läuft ---")
# Modell laden
print("Lade Modell (base)...")
model = whisper.load_model("base")
# FFmpeg Befehl mit TCP für stabilere Verbindung
command = [
'ffmpeg',
'-rtsp_transport', 'tcp',
'-i', RTSP_URL,
'-ar', '16000',
'-ac', '1',
'-f', 's16le',
'-'
]
# Startet den FFmpeg-Prozess
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
audio_buffer = []
silence_start = None
# EINSTELLUNGEN FÜR DIE PAUSENERKENNUNG
THRESHOLD = 350 # Empfindlichkeit
SILENCE_DURATION = 2.5 # Sekunden Stille bis zum Ende
MIN_AUDIO_LENGTH = 15 # Mindestmenge an Daten
print(f"\nVerbindung zu: {RTSP_URL}")
print("[MISTY HÖRT ZU] Bitte sprich jetzt (2.5s Pause zum Beenden)...")
try:
while True:
raw_chunk = process.stdout.read(3200)
if not raw_chunk:
break
chunk_np = np.frombuffer(raw_chunk, dtype=np.int16)
if chunk_np.size == 0: continue
audio_buffer.append(chunk_np)
amplitude = np.sqrt(np.mean(chunk_np**2))
if amplitude < THRESHOLD:
if silence_start is None:
silence_start = time.time()
elif time.time() - silence_start > SILENCE_DURATION:
if len(audio_buffer) > MIN_AUDIO_LENGTH:
print("\n[Pause erkannt - Analyse startet]")
break
else:
sys.stdout.write(".")
sys.stdout.flush()
silence_start = None
process.terminate()
if not audio_buffer:
print("\n❌ Fehler: Keine Audiodaten empfangen.")
return
print("Verarbeite Audio...")
full_audio = np.concatenate(audio_buffer).astype(np.float32) / 32768.0
# Transkription mit Füllwort-Support
result = model.transcribe(full_audio, language="de", initial_prompt="Äh, ähm, mhm.")
text = result['text'].strip()
count, details = analyze_text(text)
# --- AUSGABE ---
print("\n" + "═"*45)
print(f"ERKANNT: {text}")
print("─"*45)
print(f"ANALYSE: {count} Füllwörter gefunden.")
if count > 0:
for w, n in details.items():
print(f" -> '{w}': {n}x")
print("═"*45 + "\n")
except Exception as e:
print(f"\nFehler: {e}")
finally:
if process and process.poll() is None:
process.kill()
if __name__ == "__main__":
run_adaptive_whisper()

68
old/test.py Normal file
View file

@ -0,0 +1,68 @@
import whisper
import numpy as np
import subprocess
import time
import sys
from config import RTSP_URL, MISTY_IP
def run_test_inference():
print(f"--- Finaler Test-Lauf (SDK-Struktur) ---")
# Modell laden
print("Lade Whisper-Modell...")
model = whisper.load_model("base")
# FFmpeg-Befehl mit mehr "Geduld" (analyzeduration & probesize)
# Das hilft, wenn Misty den Stream langsam startet
command = [
'ffmpeg',
'-rtsp_transport', 'tcp',
'-analyzeduration', '5000000',
'-probesize', '5000000',
'-i', RTSP_URL,
'-ar', '16000',
'-ac', '1',
'-f', 's16le',
'-'
]
print(f"\nVersuche Verbindung zu: {RTSP_URL}")
# Startet den Prozess
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
audio_buffer = []
print("[MISTY HÖRT ZU] Sprich jetzt... (Sammle 10 Sekunden Audio)")
start_time = time.time()
try:
# Wir sammeln jetzt erst mal stumpf 10 Sekunden, um den Puffer zu füllen
while time.time() - start_time < 10:
raw_chunk = process.stdout.read(3200)
if raw_chunk:
audio_buffer.append(np.frombuffer(raw_chunk, dtype=np.int16))
sys.stdout.write(".")
sys.stdout.flush()
process.terminate()
if not audio_buffer:
# Wenn nichts kam, schauen wir in den Error-Log von FFmpeg
_, stderr = process.communicate()
print(f"\n❌ FFmpeg Fehler-Log:\n{stderr.decode()}")
return
print("\n\nAnalyse startet...")
full_audio = np.concatenate(audio_buffer).astype(np.float32) / 32768.0
result = model.transcribe(full_audio, language="de")
print("\n" + "="*40)
print(f"ERGEBNIS: {result['text'].strip()}")
print("="*40 + "\n")
except Exception as e:
print(f"Fehler: {e}")
finally:
process.kill()
if __name__ == "__main__":
run_test_inference()

View file

@ -0,0 +1,90 @@
import whisper
import numpy as np
import subprocess
import time
import sys
import re
from config import RTSP_URL
FILLER_WORDS = ["äh", "ähhm", "ähm", "mhm", "halt", "quasi", "sozusagen", "eigentlich"]
def analyze_text(text):
text_clean = re.sub(r'[^\w\s]', '', text.lower())
words = text_clean.split()
found_fillers = {w: words.count(w) for w in FILLER_WORDS if w in words}
return sum(found_fillers.values()), found_fillers
def run_adaptive_whisper():
print(f"--- M2 Live-Coach: Analyse läuft ---")
print("Lade KI-Modell...")
model = whisper.load_model("base")
# FFmpeg mit längerer Analysezeit und TCP-Zwang
command = [
'ffmpeg',
'-rtsp_transport', 'tcp',
'-i', RTSP_URL,
'-ar', '16000', '-ac', '1', '-f', 's16le', '-'
]
print(f"Verbinde zu: {RTSP_URL}")
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
audio_buffer = []
silence_start = None
THRESHOLD = 300 # Etwas empfindlicher
SILENCE_DURATION = 2.5
print("[WARTE AUF STREAM...]")
try:
# 10 Versuche, den Stream-Anfang zu finden
for _ in range(100):
raw_chunk = process.stdout.read(3200)
if raw_chunk:
print("[MISTY HÖRT ZU] - Daten fließen!")
audio_buffer.append(np.frombuffer(raw_chunk, dtype=np.int16))
break
time.sleep(0.1)
if not audio_buffer:
print("❌ Fehler: Misty sendet keine Daten auf Port 1936.")
return
while True:
raw_chunk = process.stdout.read(3200)
if not raw_chunk: break
chunk_np = np.frombuffer(raw_chunk, dtype=np.int16)
audio_buffer.append(chunk_np)
amplitude = np.sqrt(np.mean(chunk_np**2)) if chunk_np.size > 0 else 0
if amplitude < THRESHOLD:
if silence_start is None:
silence_start = time.time()
elif time.time() - silence_start > SILENCE_DURATION:
if len(audio_buffer) > 20: break
else:
sys.stdout.write(".")
sys.stdout.flush()
silence_start = None
process.terminate()
full_audio = np.concatenate(audio_buffer).astype(np.float32) / 32768.0
result = model.transcribe(full_audio, language="de", initial_prompt="Äh, ähm, mhm.")
text = result['text'].strip()
count, details = analyze_text(text)
print("\n" + ""*45)
print(f"TEXT: {text}")
print(f"FÜLLWÖRTER: {count}")
print(""*45)
except Exception as e:
print(f"\nFehler: {e}")
finally:
if process: process.kill()
if __name__ == "__main__":
run_adaptive_whisper()

27
session_daten.json Normal file
View file

@ -0,0 +1,27 @@
{
"status": "wartend",
"sessions": [
{
"nummer": 1,
"zeit": "12:36",
"text": "Okay, das heißt, wenn ich jetzt hier rede und eine Präsentation halt, wieder nimmst du das Ganze auch. Okay. Aber dann merke ich gerade, dann ist es doch kein Livestream. Weil er nimmt jetzt wahrscheinlich auf, speichert das Ganze in eine wunderschöne Datei. Und diese Datei, die nennen wir Mississippi und gucken, ob er auch ganz viele verschiedene Worte und schwierige Worte wie Retoreg erkennt und dann wird das Ganze wahrscheinlich mit Whisper analysiert.",
"fuellwoerter_anzahl": 1,
"gefundene_woerter": {
"halt": 1
},
"tempo": 146,
"feedback": "Du hattest ein Füllwort. Du hast das Füllwort halt einmal benutzt. Dein Sprechtempo war sehr angenehm.",
"gesicht": "e_Love.jpg"
},
{
"nummer": 2,
"zeit": "12:37",
"text": "Die mit manch schlecht.",
"fuellwoerter_anzahl": 0,
"gefundene_woerter": {},
"tempo": 266,
"feedback": "Keine Füllwörter gefunden. Hervorragend! Dein Sprechtempo war etwas zu schnell. Versuche dich etwas zu verlangsamen.",
"gesicht": "e_Contempt.jpg"
}
]
}

116
start_coaching.py Normal file
View file

@ -0,0 +1,116 @@
import websocket
import json
import requests
import time
import threading
import os
from config import MISTY_IP
from analyse import analysiere, gib_feedback, setze_status
DATEI_NAME = "aufnahme.wav"
DATEN_DATEI = "session_daten.json"
laeuft_gerade = False
aufnahme_laeuft = False
aufnahme_start = 0
def setze_neutral():
requests.post(f"http://{MISTY_IP}/api/images/display", json={"FileName": "e_DefaultContent.jpg"})
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)
print("--- Session-Daten zurückgesetzt ---")
def coaching_prozess():
global laeuft_gerade, aufnahme_laeuft, aufnahme_start
laeuft_gerade = True
aufnahme_laeuft = True
setze_neutral()
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"
})
time.sleep(4)
print("--- Aufnahme läuft ---")
requests.post(f"http://{MISTY_IP}/api/audio/record/start", json={"FileName": DATEI_NAME})
aufnahme_start = time.time()
while aufnahme_laeuft:
time.sleep(0.1)
echte_dauer = time.time() - aufnahme_start
print(f"--- Aufnahme gestoppt ({round(echte_dauer)} Sekunden) ---")
requests.post(f"http://{MISTY_IP}/api/audio/record/stop")
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!")
setze_status("wartend")
laeuft_gerade = False
return
anzahl, text, gefundene_woerter, tempo = analysiere(DATEI_NAME, echte_dauer)
gib_feedback(anzahl, gefundene_woerter, tempo, text)
setze_status("wartend")
laeuft_gerade = False
def on_message(ws, message):
global laeuft_gerade, aufnahme_laeuft
data = json.loads(message)
if "message" in data and "sensorId" in data["message"]:
sensor = data["message"]["sensorId"]
is_pressed = data["message"]["isContacted"]
if sensor in ["bfr", "bfl"] and is_pressed:
if not laeuft_gerade:
threading.Thread(target=coaching_prozess).start()
elif aufnahme_laeuft:
print("--- Fuß gedrückt! Aufnahme wird beendet ---")
aufnahme_laeuft = False
def on_open(ws):
print("Verbindung zu Misty hergestellt. Drücke einen FUSS zum Starten.")
reset_session_daten()
setze_neutral()
setze_status("wartend")
subscribe_msg = {
"Operation": "subscribe",
"Type": "BumpSensor",
"DebounceMs": 50,
"EventName": "BumperPress",
"ReturnProperty": None
}
ws.send(json.dumps(subscribe_msg))
def on_error(ws, error):
print(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")
ws = websocket.WebSocketApp(
f"ws://{MISTY_IP}/pubsub",
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close
)
try:
ws.run_forever()
except KeyboardInterrupt:
print("\n--- Programm beendet ---")
setze_neutral()
setze_status("wartend")

BIN
static/e_Contempt.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
static/e_DefaultContent.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
static/e_Love.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
static/e_Sadness.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

348
templates/dashboard.html Normal file
View file

@ -0,0 +1,348 @@
<!DOCTYPE html>
<html lang="de">
<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>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #f0f4ff;
--white: #ffffff;
--blue: #1a56db;
--blue-light: #dbeafe;
--blue-mid: #93c5fd;
--dark: #1e2d5a;
--muted: #6b7db3;
--green: #059669;
--green-light: #d1fae5;
--amber: #d97706;
--amber-light: #fef3c7;
--red: #dc2626;
--red-light: #fee2e2;
--border: #e2e8f0;
}
body {
background: var(--bg);
color: var(--dark);
font-family: 'Nunito', sans-serif;
min-height: 100vh;
padding: 28px;
}
header {
background: var(--white);
border-radius: 20px;
padding: 20px 28px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(37,99,235,0.08);
border: 1px solid var(--border);
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-avatar {
width: 48px; height: 48px;
border-radius: 14px;
background: linear-gradient(135deg, var(--blue), #3b82f6);
display: grid; place-items: center;
font-size: 24px;
box-shadow: 0 4px 12px rgba(37,99,235,0.3);
}
.brand-name { font-weight: 900; font-size: 20px; letter-spacing: -0.5px; }
.brand-sub { font-size: 12px; color: var(--muted); margin-top: 2px; font-weight: 500; }
.status-pill {
display: flex; align-items: center; gap: 8px;
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;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px; margin-bottom: 20px;
}
.grid-bottom {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px; margin-bottom: 20px;
}
.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);
}
.card-blue {
background: linear-gradient(135deg, var(--blue) 0%, #3b82f6 100%);
color: white; border: none;
box-shadow: 0 8px 24px rgba(37,99,235,0.3);
position: relative; overflow: hidden;
}
.dots {
position: absolute; top: 16px; right: 16px;
opacity: 0.15; font-size: 20px;
letter-spacing: 3px; color: white;
}
.card-label {
font-size: 11px; font-weight: 700;
letter-spacing: 1.5px; text-transform: uppercase;
color: var(--muted); margin-bottom: 14px;
}
.card-blue .card-label { color: rgba(255,255,255,0.7); }
.metric-num {
font-size: 56px; font-weight: 900;
line-height: 1; letter-spacing: -2px;
}
.metric-unit { font-size: 13px; color: var(--muted); margin-top: 4px; font-weight: 600; }
.card-blue .metric-unit { color: rgba(255,255,255,0.7); }
.badge {
display: inline-flex; align-items: center; gap: 5px;
padding: 5px 12px; border-radius: 100px;
font-size: 12px; font-weight: 700; margin-top: 12px;
}
.badge-green { background: var(--green-light); color: var(--green); }
.badge-amber { background: var(--amber-light); color: var(--amber); }
.badge-red { background: var(--red-light); color: var(--red); }
.badge-blue { background: rgba(255,255,255,0.2); color: white; }
.gauge-wrap { margin-top: 16px; }
.gauge-track {
height: 8px; border-radius: 4px;
background: var(--blue-light);
position: relative; overflow: visible;
}
.gz { position: absolute; height: 100%; border-radius: 4px; }
.gauge-knob {
position: absolute; top: 50%;
transform: translate(-50%, -50%);
width: 18px; height: 18px;
background: white; border-radius: 50%;
border: 3px solid var(--blue);
box-shadow: 0 2px 8px rgba(37,99,235,0.4);
}
.gauge-labels {
display: flex; justify-content: space-between;
margin-top: 8px; font-size: 10px;
color: var(--muted); font-weight: 600;
}
.face-card {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
min-height: 140px; text-align: center;
}
.face-circle {
width: 120px; height: 80px; border-radius: 12px;
background: var(--blue-light);
display: grid; place-items: center;
margin-bottom: 10px;
border: 3px solid var(--blue-mid);
overflow: hidden;
animation: float 3s ease-in-out infinite;
}
@keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-5px)} }
.face-name {
font-size: 11px; color: var(--muted); font-weight: 600;
background: var(--blue-light); padding: 4px 10px; border-radius: 6px;
}
.transcript-box {
background: var(--bg); border-radius: 12px;
padding: 16px 18px; font-size: 14px;
line-height: 1.8; color: #475569;
border: 1px solid var(--border); font-weight: 500;
}
.tags { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.tag {
display: inline-flex; align-items: center; gap: 6px;
background: var(--amber-light); border: 1px solid #fde68a;
color: var(--amber); padding: 6px 12px;
border-radius: 8px; font-size: 12px; font-weight: 700;
}
.tag-n {
background: #fcd34d; color: #92400e;
border-radius: 4px; padding: 1px 6px; font-size: 11px;
}
.feedback-body {
font-size: 14px; line-height: 1.7;
color: #475569; font-weight: 500;
}
.feedback-icon { font-size: 32px; margin-bottom: 10px; }
.verlauf-row {
display: flex; align-items: center; gap: 14px;
padding: 12px 0; border-bottom: 1px solid var(--border);
}
.verlauf-row:last-child { border-bottom: none; padding-bottom: 0; }
.v-num { font-size: 12px; font-weight: 800; color: var(--muted); width: 30px; flex-shrink: 0; }
.v-bar { flex: 1; height: 8px; background: var(--blue-light); border-radius: 4px; overflow: hidden; }
.v-fill { height: 100%; border-radius: 4px; }
.v-meta {
display: flex; align-items: center; gap: 10px;
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; }
.c-green { color: var(--green); }
.c-amber { color: var(--amber); }
.c-red { color: var(--red); }
.c-blue { color: var(--blue); }
.empty-state {
text-align: center; padding: 40px;
color: var(--muted); font-size: 15px; font-weight: 600;
}
</style>
</head>
<body>
<header>
<div class="brand">
<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>
</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>
</header>
{% if daten.sessions %}
{% set letzte = daten.sessions[-1] %}
<div class="grid-top">
<div class="card card-blue">
<span class="dots">· · · · ·<br>· · · · ·<br>· · · · ·</span>
<div class="card-label">Füllwörter</div>
<div class="metric-num" style="color:white;">{{ letzte.fuellwoerter_anzahl }}</div>
<div class="metric-unit">in dieser Session</div>
{% if daten.sessions|length > 1 %}
{% set vorher = daten.sessions[-2] %}
{% if letzte.fuellwoerter_anzahl < vorher.fuellwoerter_anzahl %}
<div class="badge badge-blue">↓ {{ vorher.fuellwoerter_anzahl - letzte.fuellwoerter_anzahl }} weniger als zuvor</div>
{% elif letzte.fuellwoerter_anzahl > vorher.fuellwoerter_anzahl %}
<div class="badge badge-blue">↑ {{ letzte.fuellwoerter_anzahl - vorher.fuellwoerter_anzahl }} mehr als zuvor</div>
{% else %}
<div class="badge badge-blue">= gleich wie zuvor</div>
{% endif %}
{% endif %}
</div>
<div class="card">
<div class="card-label">Sprechtempo</div>
<div class="metric-num c-blue">{{ letzte.tempo }}</div>
<div class="metric-unit">Wörter pro Minute</div>
<div class="gauge-wrap">
<div class="gauge-track">
<div class="gz" style="left:0;width:30%;background:#fde68a;border-radius:4px 0 0 4px;"></div>
<div class="gz" style="left:30%;width:40%;background:#bbf7d0;"></div>
<div class="gz" style="left:70%;width:30%;background:#fecaca;border-radius:0 4px 4px 0;"></div>
{% if letzte.tempo < 90 %}
<div class="gauge-knob" style="left:15%;"></div>
{% elif letzte.tempo <= 150 %}
<div class="gauge-knob" style="left:{{ 30 + ((letzte.tempo - 90) / 60 * 40)|int }}%;"></div>
{% else %}
<div class="gauge-knob" style="left:85%;"></div>
{% endif %}
</div>
<div class="gauge-labels">
<span>zu langsam</span>
<span>optimal</span>
<span>zu schnell</span>
</div>
</div>
{% if letzte.tempo < 90 %}
<div class="badge badge-amber">zu langsam</div>
{% elif letzte.tempo <= 150 %}
<div class="badge badge-green">✓ Sehr angenehm</div>
{% else %}
<div class="badge badge-red">zu schnell</div>
{% endif %}
</div>
<div class="card">
<div class="card-label">Mistys Reaktion</div>
<div class="face-card">
<div class="face-circle">
<img src="/static/{{ letzte.gesicht }}" style="width:100%; height:100%; object-fit:cover;">
</div>
<div class="face-name">{{ letzte.gesicht }}</div>
</div>
</div>
</div>
<div class="grid-bottom">
<div class="card">
<div class="card-label">Erkannter Text</div>
<div class="transcript-box">„{{ letzte.text }}"</div>
{% if letzte.gefundene_woerter %}
<div class="tags">
{% for wort, anzahl in letzte.gefundene_woerter.items() %}
<span class="tag">{{ wort }} <span class="tag-n">{{ anzahl }}×</span></span>
{% endfor %}
</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>
{% for session in daten.sessions|reverse %}
{% if not loop.first %}
<div class="verlauf-row">
<div class="v-num">#{{ session.nummer }}</div>
<div class="v-bar">
<div class="v-fill" style="width:{{ [session.fuellwoerter_anzahl * 10, 100]|min }}%;
background:{% if session.fuellwoerter_anzahl == 0 %}#6ee7b7
{% elif session.fuellwoerter_anzahl <= 2 %}#fcd34d
{% else %}#fca5a5{% endif %};"></div>
</div>
<div class="v-meta">
<b class="{% if session.fuellwoerter_anzahl == 0 %}c-green{% elif session.fuellwoerter_anzahl <= 2 %}c-amber{% else %}c-red{% endif %}">
{{ session.fuellwoerter_anzahl }}
</b> Füllwörter ·
<b>{{ session.tempo }}</b> W/min ·
<img src="/static/{{ session.gesicht }}" class="v-face">
</div>
</div>
{% endif %}
{% 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>
</div>
{% endif %}
</body>
</html>