commit a75a9b11776c7c37c8e52afa66895f2c0a7ea128 Author: TiffanyBrugger Date: Wed May 6 12:46:05 2026 +0000 first commit diff --git a/__pycache__/analyse.cpython-312.pyc b/__pycache__/analyse.cpython-312.pyc new file mode 100644 index 0000000..1321739 Binary files /dev/null and b/__pycache__/analyse.cpython-312.pyc differ diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..8c3f7a5 Binary files /dev/null and b/__pycache__/config.cpython-312.pyc differ diff --git a/analyse.py b/analyse.py new file mode 100644 index 0000000..29c010e --- /dev/null +++ b/analyse.py @@ -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}") diff --git a/aufnahme.wav b/aufnahme.wav new file mode 100644 index 0000000..78deeef Binary files /dev/null and b/aufnahme.wav differ diff --git a/config.py b/config.py new file mode 100644 index 0000000..a354873 --- /dev/null +++ b/config.py @@ -0,0 +1,4 @@ +# config.py +# Zentrale Stelle für Netzwerkparameter +MISTY_IP = "192.168.68.65" +RTSP_URL = f"rtsp://{MISTY_IP}:1936" diff --git a/dashboard.py b/dashboard.py new file mode 100644 index 0000000..6601f70 --- /dev/null +++ b/dashboard.py @@ -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) diff --git a/livemodus/misty_start_av.py b/livemodus/misty_start_av.py new file mode 100644 index 0000000..ca9b2d4 --- /dev/null +++ b/livemodus/misty_start_av.py @@ -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}") diff --git a/livemodus/misty_stop_av.py b/livemodus/misty_stop_av.py new file mode 100644 index 0000000..7e5eb0f --- /dev/null +++ b/livemodus/misty_stop_av.py @@ -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.") diff --git a/livemodus/whisper_live_check.py b/livemodus/whisper_live_check.py new file mode 100644 index 0000000..18be3c6 --- /dev/null +++ b/livemodus/whisper_live_check.py @@ -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() diff --git a/old/__pycache__/config.cpython-312.pyc b/old/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..fa5d11a Binary files /dev/null and b/old/__pycache__/config.cpython-312.pyc differ diff --git a/old/aufnahme.py b/old/aufnahme.py new file mode 100644 index 0000000..6059a9d --- /dev/null +++ b/old/aufnahme.py @@ -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() diff --git a/old/avstream_test.py b/old/avstream_test.py new file mode 100644 index 0000000..fc0f4eb --- /dev/null +++ b/old/avstream_test.py @@ -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) diff --git a/old/bumper_start.py b/old/bumper_start.py new file mode 100644 index 0000000..53e7255 --- /dev/null +++ b/old/bumper_start.py @@ -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() diff --git a/old/detektor.py b/old/detektor.py new file mode 100644 index 0000000..d1afb88 --- /dev/null +++ b/old/detektor.py @@ -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 ---") diff --git a/old/live_coach_final.py b/old/live_coach_final.py new file mode 100644 index 0000000..c0f131c --- /dev/null +++ b/old/live_coach_final.py @@ -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() diff --git a/old/start_audio_stream.py b/old/start_audio_stream.py new file mode 100644 index 0000000..96e623f --- /dev/null +++ b/old/start_audio_stream.py @@ -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() + diff --git a/old/start_stream.py b/old/start_stream.py new file mode 100644 index 0000000..e8e3f8c --- /dev/null +++ b/old/start_stream.py @@ -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() diff --git a/old/start_vlc_stream.py b/old/start_vlc_stream.py new file mode 100644 index 0000000..f642894 --- /dev/null +++ b/old/start_vlc_stream.py @@ -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() diff --git a/old/test b/old/test new file mode 100644 index 0000000..a78ec5a --- /dev/null +++ b/old/test @@ -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() diff --git a/old/test.py b/old/test.py new file mode 100644 index 0000000..30ccd60 --- /dev/null +++ b/old/test.py @@ -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() diff --git a/old/whisper_terminal_check.py b/old/whisper_terminal_check.py new file mode 100644 index 0000000..017847e --- /dev/null +++ b/old/whisper_terminal_check.py @@ -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() diff --git a/session_daten.json b/session_daten.json new file mode 100644 index 0000000..6069132 --- /dev/null +++ b/session_daten.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/start_coaching.py b/start_coaching.py new file mode 100644 index 0000000..d0037f9 --- /dev/null +++ b/start_coaching.py @@ -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") diff --git a/static/e_Contempt.jpg b/static/e_Contempt.jpg new file mode 100644 index 0000000..a987a8f Binary files /dev/null and b/static/e_Contempt.jpg differ diff --git a/static/e_DefaultContent.jpg b/static/e_DefaultContent.jpg new file mode 100644 index 0000000..99398a7 Binary files /dev/null and b/static/e_DefaultContent.jpg differ diff --git a/static/e_Love.jpg b/static/e_Love.jpg new file mode 100644 index 0000000..ae1c7cd Binary files /dev/null and b/static/e_Love.jpg differ diff --git a/static/e_Sadness.jpg b/static/e_Sadness.jpg new file mode 100644 index 0000000..d7629b6 Binary files /dev/null and b/static/e_Sadness.jpg differ diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..8891e83 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,348 @@ + + + + + + +Misty Rhetorik-Coach + + + + + +
+
+
🤖
+
+
Misty Rhetorik-Coach
+
PH Weingarten · Modul M2 Entwicklung Interaktiver Medien
+
+
+
+
+ {% if daten.status == "wartend" %}Wartet auf Fußdruck + {% elif daten.status == "aufnehmend" %}Aufnahme läuft... + {% elif daten.status == "analysierend" %}Analyse läuft... + {% endif %} +
+
+ +{% if daten.sessions %} + {% set letzte = daten.sessions[-1] %} + +
+
+ · · · · ·
· · · · ·
· · · · ·
+
Füllwörter
+
{{ letzte.fuellwoerter_anzahl }}
+
in dieser Session
+ {% if daten.sessions|length > 1 %} + {% set vorher = daten.sessions[-2] %} + {% if letzte.fuellwoerter_anzahl < vorher.fuellwoerter_anzahl %} +
↓ {{ vorher.fuellwoerter_anzahl - letzte.fuellwoerter_anzahl }} weniger als zuvor
+ {% elif letzte.fuellwoerter_anzahl > vorher.fuellwoerter_anzahl %} +
↑ {{ letzte.fuellwoerter_anzahl - vorher.fuellwoerter_anzahl }} mehr als zuvor
+ {% else %} +
= gleich wie zuvor
+ {% endif %} + {% endif %} +
+ +
+
Sprechtempo
+
{{ letzte.tempo }}
+
Wörter pro Minute
+
+
+
+
+
+ {% if letzte.tempo < 90 %} +
+ {% elif letzte.tempo <= 150 %} +
+ {% else %} +
+ {% endif %} +
+
+ zu langsam + optimal + zu schnell +
+
+ {% if letzte.tempo < 90 %} +
zu langsam
+ {% elif letzte.tempo <= 150 %} +
✓ Sehr angenehm
+ {% else %} +
zu schnell
+ {% endif %} +
+ +
+
Mistys Reaktion
+
+
+ +
+
{{ letzte.gesicht }}
+
+
+
+ +
+
+
Erkannter Text
+
„{{ letzte.text }}"
+ {% if letzte.gefundene_woerter %} +
+ {% for wort, anzahl in letzte.gefundene_woerter.items() %} + {{ wort }} {{ anzahl }}× + {% endfor %} +
+ {% endif %} +
+ +
+
Feedback von Misty
+ + +
+
+ + {% if daten.sessions|length > 1 %} +
+
Sessionverlauf
+ {% for session in daten.sessions|reverse %} + {% if not loop.first %} +
+
#{{ session.nummer }}
+
+
+
+
+ + {{ session.fuellwoerter_anzahl }} + Füllwörter · + {{ session.tempo }} W/min · + +
+
+ {% endif %} + {% endfor %} +
+ {% endif %} + +{% else %} +
+
+
+ 🤖 Willkommen beim Misty Rhetorik-Coach!

+ Hier werden die Ergebnisse jeder Präsentation – Füllwörter, Sprechtempo und Mistys Feedback gezeigt.

+ Sobald der erste Durchgang abgeschlossen ist, erscheinen die Ergebnisse automatisch.

+ Im Sessionverlauf ist zu sehen, wie sich die Leistung über mehrere Durchgänge hinweg verbessert.

+ Drücke Mistys Fuß um die Aufnahme zu starten! +
+
+{% endif %} + + +