first commit
This commit is contained in:
commit
a75a9b1177
28 changed files with 1466 additions and 0 deletions
BIN
__pycache__/analyse.cpython-312.pyc
Normal file
BIN
__pycache__/analyse.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
153
analyse.py
Normal file
153
analyse.py
Normal 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
BIN
aufnahme.wav
Normal file
Binary file not shown.
4
config.py
Normal file
4
config.py
Normal 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
25
dashboard.py
Normal 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)
|
||||
38
livemodus/misty_start_av.py
Normal file
38
livemodus/misty_start_av.py
Normal 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}")
|
||||
26
livemodus/misty_stop_av.py
Normal file
26
livemodus/misty_stop_av.py
Normal 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.")
|
||||
58
livemodus/whisper_live_check.py
Normal file
58
livemodus/whisper_live_check.py
Normal 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()
|
||||
BIN
old/__pycache__/config.cpython-312.pyc
Normal file
BIN
old/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
53
old/aufnahme.py
Normal file
53
old/aufnahme.py
Normal 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
36
old/avstream_test.py
Normal 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
97
old/bumper_start.py
Normal 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
49
old/detektor.py
Normal 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
46
old/live_coach_final.py
Normal 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
41
old/start_audio_stream.py
Normal 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
38
old/start_stream.py
Normal 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
45
old/start_vlc_stream.py
Normal 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
108
old/test
Normal 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
68
old/test.py
Normal 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()
|
||||
90
old/whisper_terminal_check.py
Normal file
90
old/whisper_terminal_check.py
Normal 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
27
session_daten.json
Normal 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
116
start_coaching.py
Normal 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
BIN
static/e_Contempt.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
static/e_DefaultContent.jpg
Normal file
BIN
static/e_DefaultContent.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
static/e_Love.jpg
Normal file
BIN
static/e_Love.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
BIN
static/e_Sadness.jpg
Normal file
BIN
static/e_Sadness.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
348
templates/dashboard.html
Normal file
348
templates/dashboard.html
Normal 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>
|
||||
Loading…
Reference in a new issue