Augen
Hände











Pose Preview
No pose selected
Zeit / Status
00:00
3D Zone: Waiting
Pose: Waiting
Hände: Waiting
Gesicht: Waiting
Käfig: Waiting
Touch: Waiting

Lock-In!

10

// ============================================================ // --- 1. FULL MULTI-LANGUAGE DICTIONARY (100% AUSGESCHRIEBEN) --- // ============================================================ const translations = { de: { cam: "Lokale Kamera", lang: "Sprache", pose: "Körper-Pose", p_free: "Freies Spiel", p_cat_std: "--- Standard ---", p_tpose: "T-Pose", p_yoga: "Yoga Baum", p_hands: "Hände Nacken", p_cat_end: "--- Ausdauer ---", p_wall: "Wandsitz (90°)", p_surrender: "Kniend", p_cat_cust: "--- Eigene ---", upload: "📁 Upload", capture: "📷 Capture", btn_delete: "🗑️ Löschen", add_cam: "➕ Add", ip_cam: "IP Kamera (MJPEG)", add_ip: "➕ Add IP", cam_split: "Split-Modus (OBS)", ai_level: "KI Präzision", ai_fast: "Schnell (0)", ai_bal: "Balanced (1)", ai_prec: "Exakt (2)", holo: "Overlay", h_both: "Warn & Strafe", h_start: "Nur Start", h_always: "Dauerhaft", h_warn: "Nur Warnung", h_penalty: "Nur Strafe", h_vector: "Vektor Skelett", zone_check: "3D Zone", z_on: "Aktiv", z_off: "Aus", z_x: "X-Tol %", z_y: "Y-Tol %", z_z: "Z-Tol %", closer: "Näher", warden_draw: "Käfig Zeichnen", w_in: "🟩 IN", w_out: "🟥 OUT", w_clear: "🗑️ Löschen", mouth: "Mund", m_ignore: "Egal", m_open: "Offen", m_closed: "Zu", eyes: "Augen", eye_both: "Augen", eye_l: "L. Auge", eye_r: "R. Auge", sep_eyes: "Getrennt", e_ignore: "Egal", e_open: "Offen", e_closed: "Zu", e_sens: "Sens %", hands: "Hände", hand_both: "Hände", hand_l: "L. Hand", hand_r: "R. Hand", sep_hands: "Getrennt", lh: "L. Hand", rh: "R. Hand", h_ignore: "Egal", h_fist: "Faust", h_spread: "Spreizen", h_together: "Geschlossen", body_tol: "Körper-Tol°", body_sens: "Körper-Sens %", time_target: "Zielzeit", time_grace: "Warn.(s)", time_pen: "Strafe(s)", esc_win: "Esc. Win", esc_add: "Esc. Add", tts_mode: "Ausgabesprache", tts_local: "Lokal", tts_en: "Englisch", tts_orig: "Original", tts_voice: "TTS Stimme", nt_warden: "🚫 No-Touch (True 3D)", nt_face: "Gesicht", nt_chest: "Brust", nt_crotch: "Schritt", btn_audio: "🔊 Audio", btn_start: "START", btn_tg: "Verbinde Remote", prep_time: "Start in(s)", alert_time: "Anz.(s)", msg_warn: "Warn Text", msg_pen: "Straf Text", auto_photo: "Auto-Foto", prev_title: "Pose Vorschau", prev_empty: "Keine Pose gewählt", btn_hide: "Timer verstecken", btn_show: "👀 Timer anzeigen", stat_time: "Zeit / Status", stat_zone: "3D Zone:", stat_pose: "Pose:", stat_hand: "Hände:", stat_face: "Gesicht:", stat_warden: "Käfig/Touch:", stat_cage: "Käfig:", stat_touch: "Touch:", spkStart: "Überwachung gestartet. Bereit machen.", spkPen: "Strafe!", spkSec: "Sekunden.", spkOver: "Erfolg! Die Zeit ist um.", spkErr: "Fehler erkannt!", spkCorr: "Gut gerettet!", err_stop: "Bitte aktives Spiel stoppen!", err_person: "Keine Person gefunden!", prompt_pose: "Name der Pose:", err_tg: "Konnte Telegram nicht erreichen.", err_net: "Bild konnte nicht geladen werden.", err_quota: "Speicher voll! Pose nur temporär.", err_del: "Pose unwiderruflich löschen?", cam_https: "HTTPS benötigt", cam_block: "Blockiert", cam_none: "Keine Kamera", btn_conn: "Verbinde...", btn_act: "Remote Aktiv ✓", btn_err: "Bot Fehler!", btn_net: "Netzwerk Fehler!", st_setup: "Setup", st_lock: "Gesperrt", st_miss: "Fehlt", st_off: "Aus", st_ok: "OK", st_free: "Frei", st_ign: "Egal", st_hid: "Versteckt", st_wait: "Wartet", tg_menu: "🎮 Master Control Panel", tg_status: "📊 STATUS", tg_start: "Start", tg_stop: "Stop", tg_spy: "Spy Foto", tg_photo: "Auto-Foto", tg_hide: "Timer Weg", tg_show: "Timer Da", tg_blind: "Blindflug", tg_ui: "UI Zeigen", tg_saved: "Gespeichert!", tg_fail: "Fehlgeschlagen!", tg_busy: "⏳ Aktion läuft bereits...", err_mouth: "Mund", err_eye_l: "Auge L", err_eye_r: "Auge R", err_hand_l: "Hand L", err_hand_r: "Hand R", err_pose: "Pose", err_cage_in: "IN-Käfig", err_cage_out: "OUT-Käfig", err_touch_f: "Touch Gesicht", err_touch_c: "Touch Brust", err_touch_cr: "Touch Schritt", err_zone_x: "Zone X", err_zone_y: "Zone Y", err_zone_z: "Zone Z" }, en: { cam: "Local Camera", lang: "Language", pose: "Body Pose", p_free: "Free Play", p_cat_std: "--- Standard ---", p_tpose: "T-Pose", p_yoga: "Yoga Tree", p_hands: "Hands Behind Head", p_cat_end: "--- Endurance ---", p_wall: "Wall Sit", p_surrender: "Kneeling", p_cat_cust: "--- Custom ---", upload: "📁 Upload", capture: "📷 Capture", btn_delete: "🗑️ Delete", add_cam: "➕ Add", ip_cam: "IP Camera", add_ip: "➕ Add IP", cam_split: "Split-Mode", ai_level: "AI Precision", ai_fast: "Fast (0)", ai_bal: "Balanced (1)", ai_prec: "Exact (2)", holo: "Overlay", h_both: "Warn & Pen.", h_start: "Start Only", h_always: "Always", h_warn: "Warn Only", h_penalty: "Pen. Only", h_vector: "Vector", zone_check: "3D Zone", z_on: "Active", z_off: "Off", z_x: "X-Tol %", z_y: "Y-Tol %", z_z: "Z-Tol %", closer: "Closer", warden_draw: "Draw Cage", w_in: "🟩 IN", w_out: "🟥 OUT", w_clear: "🗑️ Clear", mouth: "Mouth", m_ignore: "Ignore", m_open: "Open", m_closed: "Closed", eyes: "Eyes", eye_both: "Eyes", eye_l: "L. Eye", eye_r: "R. Eye", sep_eyes: "Separate", e_ignore: "Ignore", e_open: "Open", e_closed: "Closed", e_sens: "Sens %", hands: "Hands", hand_both: "Hands", hand_l: "L. Hand", hand_r: "R. Hand", sep_hands: "Separate", lh: "L. Hand", rh: "R. Hand", h_ignore: "Ignore", h_fist: "Fist", h_spread: "Spread", h_together: "Closed", body_tol: "Body Tol°", body_sens: "Body Sens %", time_target: "Target Time", time_grace: "Grace(s)", time_pen: "Pen.(s)", esc_win: "Esc. Win", esc_add: "Esc. Add", tts_mode: "Voice Lang", tts_local: "Local", tts_en: "English", tts_orig: "Original", tts_voice: "TTS Voice", nt_warden: "🚫 No-Touch", nt_face: "Face", nt_chest: "Chest", nt_crotch: "Crotch", btn_audio: "🔊 Audio", btn_start: "START", btn_tg: "Connect", prep_time: "Start in(s)", alert_time: "Alert(s)", msg_warn: "Warn Text", msg_pen: "Pen Text", auto_photo: "Auto-Photo", prev_title: "Pose Preview", prev_empty: "No pose selected", btn_hide: "Hide Timer", btn_show: "👀 Show Timer", stat_time: "Time / Status", stat_zone: "3D Zone:", stat_pose: "Pose:", stat_hand: "Hands:", stat_face: "Face:", stat_warden: "Cage/Touch:", stat_cage: "Cage:", stat_touch: "Touch:", spkStart: "Monitoring started.", spkPen: "Penalty!", spkSec: "seconds.", spkOver: "Success!", spkErr: "Error!", spkCorr: "Nice save!", err_stop: "Stop game!", err_person: "No person found!", prompt_pose: "Pose name:", err_tg: "Telegram error.", err_net: "Network error.", err_quota: "Storage full!", err_del: "Delete pose?", cam_https: "HTTPS needed", cam_block: "Blocked", cam_none: "No Camera", btn_conn: "Connecting...", btn_act: "Remote Active ✓", btn_err: "Bot Error!", btn_net: "Network Error!", st_setup: "Setup", st_lock: "Locked", st_miss: "Missing", st_off: "Off", st_ok: "OK", st_free: "Free", st_ign: "Ignore", st_hid: "Hidden", st_wait: "Waiting", tg_menu: "🎮 Control Panel", tg_status: "📊 STATUS", tg_start: "Start", tg_stop: "Stop", tg_spy: "Spy Photo", tg_photo: "Auto-Photo", tg_hide: "Hide Timer", tg_show: "Show Timer", tg_blind: "Blind Mode", tg_ui: "Show UI", tg_saved: "Saved!", tg_fail: "Failed!", tg_busy: "⏳ Busy...", err_mouth: "Mouth", err_eye_l: "Eye L", err_eye_r: "Eye R", err_hand_l: "Hand L", err_hand_r: "Hand R", err_pose: "Pose", err_cage_in: "IN-Cage", err_cage_out: "OUT-Cage", err_touch_f: "Touch Face", err_touch_c: "Touch Chest", err_touch_cr: "Touch Crotch", err_zone_x: "Zone X", err_zone_y: "Zone Y", err_zone_z: "Zone Z" }, nl: { cam: "Lokale Camera", lang: "Taal", pose: "Lichaamshouding", p_free: "Vrij Spel", p_cat_std: "--- Standaard ---", p_tpose: "T-Houding", p_yoga: "Yogaboom", p_hands: "Handen Nek", p_cat_end: "--- Uithouding ---", p_wall: "Muurzit", p_surrender: "Knielend", p_cat_cust: "--- Eigen ---", upload: "📁 Upload", capture: "📷 Capture", btn_delete: "🗑️ Verwijderen", add_cam: "➕ Toevoegen", ip_cam: "IP Camera", add_ip: "➕ IP Toevoegen", cam_split: "Split-Modus", ai_level: "AI Precisie", ai_fast: "Snel (0)", ai_bal: "Balans (1)", ai_prec: "Exact (2)", holo: "Overlay", h_both: "Waarsch & Straf", h_start: "Alleen Start", h_always: "Altijd", h_warn: "Waarsch", h_penalty: "Straf", h_vector: "Vector", zone_check: "3D Zone", z_on: "Aan", z_off: "Uit", z_x: "X-Tol %", z_y: "Y-Tol %", z_z: "Z-Tol %", closer: "Dichterbij", warden_draw: "Teken Kooi", w_in: "🟩 IN", w_out: "🟥 UIT", w_clear: "🗑️ Wissen", mouth: "Mond", m_ignore: "Negeer", m_open: "Open", m_closed: "Dicht", eyes: "Ogen", eye_both: "Ogen", eye_l: "L. Oog", eye_r: "R. Oog", sep_eyes: "Gescheiden", e_ignore: "Negeer", e_open: "Open", e_closed: "Dicht", e_sens: "Gev. %", hands: "Handen", hand_both: "Handen", hand_l: "L. Hand", hand_r: "R. Hand", sep_hands: "Gescheiden", lh: "L. Hand", rh: "R. Hand", h_ignore: "Negeer", h_fist: "Vuist", h_spread: "Spreid", h_together: "Gesloten", body_tol: "Lichaam Tol°", body_sens: "Lichaam Gev. %", time_target: "Doeltijd", time_grace: "Waarsch(s)", time_pen: "Straf(s)", esc_win: "Esc. Win", esc_add: "Esc. Toev", tts_mode: "Spraak", tts_local: "Lokaal", tts_en: "Engels", tts_orig: "Origineel", tts_voice: "Stem", nt_warden: "🚫 Geen Aanraking (3D)", nt_face: "Gezicht", nt_chest: "Borst", nt_crotch: "Kruis", btn_audio: "🔊 Audio", btn_start: "START", btn_tg: "Verbind Speler", prep_time: "Start in(s)", alert_time: "Melding(s)", msg_warn: "Waarsch Tekst", msg_pen: "Straf Tekst", auto_photo: "Auto-Foto", prev_title: "Preview", prev_empty: "Geen houding", btn_hide: "Verberg Timer", btn_show: "👀 Toon Timer", stat_time: "Tijd / Status", stat_zone: "3D Zone:", stat_pose: "Pose:", stat_hand: "Handen:", stat_face: "Gezicht:", stat_warden: "Kooi/Raak:", stat_cage: "Kooi:", stat_touch: "Raak:", spkStart: "Gestart. Maak je klaar.", spkPen: "Straf!", spkSec: "seconden.", spkOver: "Klaar! De tijd is om.", spkErr: "Fout gedetecteerd!", spkCorr: "Goed hersteld!", err_stop: "Stop actief spel!", err_person: "Geen persoon gevonden!", prompt_pose: "Naam pose:", err_tg: "Telegram onbereikbaar.", err_net: "Afbeelding niet geladen.", err_quota: "Opslag vol!", err_del: "Pose verwijderen?", cam_https: "HTTPS nodig", cam_block: "Geblokkeerd", cam_none: "Geen Camera", btn_conn: "Verbinden...", btn_act: "Actief ✓", btn_err: "Bot Fout!", btn_net: "Netwerk Fout!", st_setup: "Setup", st_lock: "Gelockt", st_miss: "Weg", st_off: "Uit", st_ok: "OK", st_free: "Vrij", st_ign: "Negeer", st_hid: "Verborgen", st_wait: "Wachten", tg_menu: "🎮 Controlepaneel", tg_status: "📊 STATUS", tg_start: "Start", tg_stop: "Stop", tg_spy: "Spy Foto", tg_photo: "Auto-Foto", tg_hide: "Verberg Timer", tg_show: "Toon Timer", tg_blind: "Blind", tg_ui: "Toon UI", tg_saved: "Opgeslagen!", tg_fail: "Mislukt!", tg_busy: "⏳ Bezig...", err_mouth: "Mond", err_eye_l: "Oog L", err_eye_r: "Oog R", err_hand_l: "Hand L", err_hand_r: "Hand R", err_pose: "Houding", err_cage_in: "IN-Kooi", err_cage_out: "UIT-Kooi", err_touch_f: "Raak Gezicht", err_touch_c: "Raak Borst", err_touch_cr: "Raak Kruis", err_zone_x: "Zone X", err_zone_y: "Zone Y", err_zone_z: "Zone Z" }, es: { cam: "Cámara Local", lang: "Idioma", pose: "Postura", p_free: "Juego Libre", p_cat_std: "--- Estándar ---", p_tpose: "Pose T", p_yoga: "Árbol", p_hands: "Manos Nuca", p_cat_end: "--- Resistencia ---", p_wall: "Silla Pared", p_surrender: "Rodillas", p_cat_cust: "--- Propios ---", upload: "📁 Subir", capture: "📷 Captura", btn_delete: "🗑️ Borrar", add_cam: "➕ Añadir", ip_cam: "Cámara IP", add_ip: "➕ Añadir IP", cam_split: "Modo Split", ai_level: "Precisión IA", ai_fast: "Rápido (0)", ai_bal: "Equilibrio (1)", ai_prec: "Exacto (2)", holo: "Overlay", h_both: "Aviso y Pena", h_start: "Solo Inicio", h_always: "Siempre", h_warn: "Solo Aviso", h_penalty: "Solo Pena", h_vector: "Esqueleto", zone_check: "Zona 3D", z_on: "Activo", z_off: "Inactivo", z_x: "X-Tol %", z_y: "Y-Tol %", z_z: "Z-Tol %", closer: "Cerca", warden_draw: "Dibujar Jaula", w_in: "🟩 DENTRO", w_out: "🟥 FUERA", w_clear: "🗑️ Borrar", mouth: "Boca", m_ignore: "Ignorar", m_open: "Abierta", m_closed: "Cerrada", eyes: "Ojos", eye_both: "Ojos", eye_l: "Ojo Izq", eye_r: "Ojo Der", sep_eyes: "Separado", e_ignore: "Ignorar", e_open: "Abiertos", e_closed: "Cerrados", e_sens: "Sens %", hands: "Manos", hand_both: "Manos", hand_l: "Mano Izq", hand_r: "Mano Der", sep_hands: "Separado", lh: "M. Izq", rh: "M. Der", h_ignore: "Ignorar", h_fist: "Puño", h_spread: "Abierta", h_together: "Cerrada", body_tol: "Tol Cuerpo°", body_sens: "Sens Cuerpo %", time_target: "Meta", time_grace: "Aviso(s)", time_pen: "Pena(s)", esc_win: "Esc Win", esc_add: "Esc Add", tts_mode: "Idioma Voz", tts_local: "Local", tts_en: "Inglés", tts_orig: "Original", tts_voice: "Voz TTS", nt_warden: "🚫 No Tocar (3D)", nt_face: "Cara", nt_chest: "Pecho", nt_crotch: "Ingle", btn_audio: "🔊 Audio", btn_start: "INICIAR", btn_tg: "Conectar Remoto", prep_time: "Inicio en(s)", alert_time: "Duración(s)", msg_warn: "Texto Aviso", msg_pen: "Texto Pena", auto_photo: "Auto-Foto", prev_title: "Vista Previa", prev_empty: "Sin pose", btn_hide: "Ocultar Reloj", btn_show: "👀 Mostrar Reloj", stat_time: "Tiempo / Estado", stat_zone: "Zona 3D:", stat_pose: "Pose:", stat_hand: "Manos:", stat_face: "Cara:", stat_warden: "Jaula/Toque:", stat_cage: "Jaula:", stat_touch: "Toque:", spkStart: "Iniciado. Prepárate.", spkPen: "¡Penalización!", spkSec: "segundos.", spkOver: "¡Éxito! Tiempo terminado.", spkErr: "¡Error detectado!", spkCorr: "¡Bien corregido!", err_stop: "¡Detén el juego activo!", err_person: "¡No se detectó persona!", prompt_pose: "Nombre de pose:", err_tg: "Error de Telegram.", err_net: "No se pudo cargar la imagen.", err_quota: "¡Almacenamiento lleno!", err_del: "¿Borrar pose?", cam_https: "HTTPS necesario", cam_block: "Bloqueada", cam_none: "Sin Cámara", btn_conn: "Conectando...", btn_act: "Remoto Activo ✓", btn_err: "¡Error Bot!", btn_net: "¡Error Red!", st_setup: "Setup", st_lock: "Fijo", st_miss: "Falta", st_off: "Apagado", st_ok: "OK", st_free: "Libre", st_ign: "Ignorar", st_hid: "Oculto", st_wait: "Espera", tg_menu: "🎮 Panel de Control", tg_status: "📊 ESTADO", tg_start: "Iniciar", tg_stop: "Parar", tg_spy: "Foto Espía", tg_photo: "Auto-Foto", tg_hide: "Ocultar", tg_show: "Mostrar", tg_blind: "Modo Ciego", tg_ui: "Mostrar UI", tg_saved: "¡Guardado!", tg_fail: "¡Fallo!", tg_busy: "⏳ Acción en curso...", err_mouth: "Boca", err_eye_l: "Ojo Izq", err_eye_r: "Ojo Der", err_hand_l: "Mano Izq", err_hand_r: "Mano Der", err_pose: "Pose", err_cage_in: "IN-Jaula", err_cage_out: "OUT-Jaula", err_touch_f: "Toque Cara", err_touch_c: "Toque Pecho", err_touch_cr: "Toque Ingle", err_zone_x: "Zona X", err_zone_y: "Zona Y", err_zone_z: "Zona Z" }, fr: { cam: "Caméra Locale", lang: "Langue", pose: "Posture", p_free: "Jeu Libre", p_cat_std: "--- Standard ---", p_tpose: "Pose T", p_yoga: "Arbre", p_hands: "Mains Nuque", p_cat_end: "--- Endurance ---", p_wall: "Chaise Mur", p_surrender: "À genoux", p_cat_cust: "--- Personnalisé ---", upload: "📁 Charger", capture: "📷 Capturer", btn_delete: "🗑️ Supprimer", add_cam: "➕ Ajouter", ip_cam: "Caméra IP", add_ip: "➕ Ajouter IP", cam_split: "Mode Split", ai_level: "Précision IA", ai_fast: "Rapide (0)", ai_bal: "Équilibré (1)", ai_prec: "Exact (2)", holo: "Calque", h_both: "Alerte & Pén", h_start: "Départ Seul", h_always: "Toujours", h_warn: "Alerte Seule", h_penalty: "Pénalité Seule", h_vector: "Squelette", zone_check: "Zone 3D", z_on: "Actif", z_off: "Dés", z_x: "X-Tol %", z_y: "Y-Tol %", z_z: "Z-Tol %", closer: "Plus près", warden_draw: "Dessiner Cage", w_in: "🟩 IN", w_out: "🟥 OUT", w_clear: "🗑️ Effacer", mouth: "Bouche", m_ignore: "Ignorer", m_open: "Ouverte", m_closed: "Fermée", eyes: "Yeux", eye_both: "Yeux", eye_l: "Œil G", eye_r: "Œil D", sep_eyes: "Séparé", e_ignore: "Ignorer", e_open: "Ouverts", e_closed: "Fermés", e_sens: "Sens %", hands: "Mains", hand_both: "Mains", hand_l: "Main G", hand_r: "Main D", sep_hands: "Séparé", lh: "M. Gauche", rh: "M. Droite", h_ignore: "Ignorer", h_fist: "Poing", h_spread: "Écarté", h_together: "Fermée", body_tol: "Tol Corps°", body_sens: "Sens Corps %", time_target: "Cible", time_grace: "Alerte(s)", time_pen: "Pénalité(s)", esc_win: "Esc Win", esc_add: "Esc Add", tts_mode: "Langue Voix", tts_local: "Local", tts_en: "Anglais", tts_orig: "Original", tts_voice: "Voix TTS", nt_warden: "🚫 Ne Pas Toucher (3D)", nt_face: "Visage", nt_chest: "Poitrine", nt_crotch: "Entrejambe", btn_audio: "🔊 Audio", btn_start: "DÉMARRER", btn_tg: "Connecter", prep_time: "Début(s)", alert_time: "Alerte(s)", msg_warn: "Texte Alerte", msg_pen: "Texte Pén", auto_photo: "Auto-Photo", prev_title: "Aperçu", prev_empty: "Aucune pose", btn_hide: "Cacher Timer", btn_show: "👀 Voir Timer", stat_time: "Temps / Statut", stat_zone: "Zone 3D:", stat_pose: "Pose:", stat_hand: "Mains:", stat_face: "Visage:", stat_warden: "Cage/Toucher:", stat_cage: "Cage:", stat_touch: "Toucher:", spkStart: "Démarré. Préparez-vous.", spkPen: "Pénalité!", spkSec: "secondes.", spkOver: "Fini! Le temps est écoulé.", spkErr: "Erreur détectée!", spkCorr: "Bien corrigé!", err_stop: "Arrêtez le jeu!", err_person: "Personne non trouvée!", prompt_pose: "Nom:", err_tg: "Erreur Telegram.", err_net: "Erreur réseau.", err_quota: "Mémoire pleine!", err_del: "Supprimer la pose?", cam_https: "HTTPS requis", cam_block: "Bloqué", cam_none: "Pas de Caméra", btn_conn: "Connexion...", btn_act: "Actif ✓", btn_err: "Erreur Bot!", btn_net: "Erreur Réseau!", st_setup: "Setup", st_lock: "Verrouillé", st_miss: "Absent", st_off: "Off", st_ok: "OK", st_free: "Libre", st_ign: "Ignorer", st_hid: "Caché", st_wait: "Attente", tg_menu: "🎮 Panneau", tg_status: "📊 STATUT", tg_start: "Démarrer", tg_stop: "Stop", tg_spy: "Photo Espion", tg_photo: "Auto-Photo", tg_hide: "Cacher Timer", tg_show: "Voir Timer", tg_blind: "Aveugle", tg_ui: "Montrer UI", tg_saved: "Sauvegardé!", tg_fail: "Échec!", tg_busy: "⏳ En cours...", err_mouth: "Bouche", err_eye_l: "Oeil G", err_eye_r: "Oeil D", err_hand_l: "Main G", err_hand_r: "Main D", err_pose: "Posture", err_cage_in: "IN-Cage", err_cage_out: "OUT-Cage", err_touch_f: "Touche Visage", err_touch_c: "Touche Poitrine", err_touch_cr: "Touche Entrej", err_zone_x: "Zone X", err_zone_y: "Zone Y", err_zone_z: "Zone Z" }, it: { cam: "Camera Locale", lang: "Lingua", pose: "Posa", p_free: "Gioco Libero", p_cat_std: "--- Standard ---", p_tpose: "Posa T", p_yoga: "Albero", p_hands: "Mani Nuca", p_cat_end: "--- Resistenza ---", p_wall: "Sedia", p_surrender: "In ginocchio", p_cat_cust: "--- Pers. ---", upload: "📁 Carica", capture: "📷 Cattura", btn_delete: "🗑️ Cancella", add_cam: "➕ Aggiungi", ip_cam: "IP Camera", add_ip: "➕ Add IP", cam_split: "Split Mode", ai_level: "Precisione IA", ai_fast: "Veloce (0)", ai_bal: "Bilanciata (1)", ai_prec: "Esatta (2)", holo: "Overlay", h_both: "Avviso & Pen", h_start: "Solo Inizio", h_always: "Sempre", h_warn: "Solo Avviso", h_penalty: "Solo Pen", h_vector: "Vettore", zone_check: "Zona 3D", z_on: "On", z_off: "Off", z_x: "X-Tol %", z_y: "Y-Tol %", z_z: "Z-Tol %", closer: "Vicino", warden_draw: "Disegna Gabbia", w_in: "🟩 IN", w_out: "🟥 OUT", w_clear: "🗑️ Canc", mouth: "Bocca", m_ignore: "Ignora", m_open: "Aperta", m_closed: "Chiusa", eyes: "Occhi", eye_both: "Occhi", eye_l: "Occhio S", eye_r: "Occhio D", sep_eyes: "Separati", e_ignore: "Ignora", e_open: "Aperti", e_closed: "Chiusi", e_sens: "Sens %", hands: "Mani", hand_both: "Mani", hand_l: "Mano S", hand_r: "Mano D", sep_hands: "Separati", lh: "M. Sinistra", rh: "M. Destra", h_ignore: "Ignora", h_fist: "Pugno", h_spread: "Aperta", h_together: "Chiusa", body_tol: "Tol Corpo°", body_sens: "Sens Corpo %", time_target: "Target", time_grace: "Avviso(s)", time_pen: "Pen.(s)", esc_win: "Esc Win", esc_add: "Esc Add", tts_mode: "Lingua Voce", tts_local: "Locale", tts_en: "Inglese", tts_orig: "Originale", tts_voice: "Voce TTS", nt_warden: "🚫 Non Toccare", nt_face: "Viso", nt_chest: "Petto", nt_crotch: "Inguine", btn_audio: "🔊 Audio", btn_start: "START", btn_tg: "Connetti Remoto", prep_time: "Inizia in(s)", alert_time: "Durata(s)", msg_warn: "Testo Avviso", msg_pen: "Testo Pen", auto_photo: "Auto-Foto", prev_title: "Anteprima", prev_empty: "Nessuna posa", btn_hide: "Nascondi Timer", btn_show: "👀 Mostra Timer", stat_time: "Tempo / Stato", stat_zone: "Zona 3D:", stat_pose: "Posa:", stat_hand: "Mani:", stat_face: "Viso:", stat_warden: "Gabbia/Tocco:", stat_cage: "Gabbia:", stat_touch: "Tocco:", spkStart: "Iniziato. Preparati.", spkPen: "Penalità!", spkSec: "secondi.", spkOver: "Finito! Tempo scaduto.", spkErr: "Errore rilevato!", spkCorr: "Ben corretto!", err_stop: "Ferma il gioco attivo!", err_person: "Nessuno rilevato!", prompt_pose: "Nome posa:", err_tg: "Errore Telegram.", err_net: "Immagine non caricata.", err_quota: "Memoria piena!", err_del: "Cancellare posa?", cam_https: "Serve HTTPS", cam_block: "Bloccata", cam_none: "No Camera", btn_conn: "Connessione...", btn_act: "Attivo ✓", btn_err: "Errore Bot!", btn_net: "Errore Rete!", st_setup: "Setup", st_lock: "Fisso", st_miss: "Manca", st_off: "Off", st_ok: "OK", st_free: "Libero", st_ign: "Ignora", st_hid: "Nascosto", st_wait: "Attesa", tg_menu: "🎮 Pannello di Controllo", tg_status: "📊 STATO", tg_start: "Inizia", tg_stop: "Ferma", tg_spy: "Foto Spia", tg_photo: "Auto-Foto", tg_hide: "Nascondi Timer", tg_show: "Mostra Timer", tg_blind: "Modalità Cieco", tg_ui: "Mostra UI", tg_saved: "Salvato!", tg_fail: "Fallito!", tg_busy: "⏳ In corso...", err_mouth: "Bocca", err_eye_l: "Occhio S", err_eye_r: "Occhio D", err_hand_l: "Mano S", err_hand_r: "Mano D", err_pose: "Posa", err_cage_in: "IN-Gabbia", err_cage_out: "OUT-Gabbia", err_touch_f: "Tocco Viso", err_touch_c: "Tocco Petto", err_touch_cr: "Tocco Inguine", err_zone_x: "Zona X", err_zone_y: "Zona Y", err_zone_z: "Zona Z" }, pt: { cam: "Câmera Local", lang: "Idioma", pose: "Pose", p_free: "Livre", p_cat_std: "--- Padrão ---", p_tpose: "Pose T", p_yoga: "Árvore", p_hands: "Mãos Nuca", p_cat_end: "--- Resistência ---", p_wall: "Cadeira Parede", p_surrender: "Ajoelhado", p_cat_cust: "--- Pessoal ---", upload: "📁 Enviar", capture: "📷 Capturar", btn_delete: "🗑️ Excluir", add_cam: "➕ Add", ip_cam: "Câmera IP", add_ip: "➕ Add IP", cam_split: "Split Mode", ai_level: "Precisão IA", ai_fast: "Rápido (0)", ai_bal: "Balanço (1)", ai_prec: "Exato (2)", holo: "Overlay", h_both: "Aviso & Pena", h_start: "Só Início", h_always: "Sempre", h_warn: "Só Aviso", h_penalty: "Só Pena", h_vector: "Vetor", zone_check: "Zona 3D", z_on: "On", z_off: "Off", z_x: "X-Tol %", z_y: "Y-Tol %", z_z: "Z-Tol %", closer: "Perto", warden_draw: "Gaiola", w_in: "🟩 DENTRO", w_out: "🟥 FORA", w_clear: "🗑️ Limpar", mouth: "Boca", m_ignore: "Ignorar", m_open: "Aberta", m_closed: "Fechada", eyes: "Olhos", eye_both: "Olhos", eye_l: "Olho Esq", eye_r: "Olho Dir", sep_eyes: "Separar", e_ignore: "Ignorar", e_open: "Abertos", e_closed: "Fechados", e_sens: "Sens %", hands: "Mãos", hand_both: "Mãos", hand_l: "Mão Esq", hand_r: "Mão Dir", sep_hands: "Separar", lh: "M. Esq", rh: "M. Dir", h_ignore: "Ignorar", h_fist: "Punho", h_spread: "Aberta", h_together: "Fechada", body_tol: "Tol Corpo°", body_sens: "Sens Corpo %", time_target: "Meta", time_grace: "Aviso(s)", time_pen: "Pena(s)", esc_win: "Esc Win", esc_add: "Esc Add", tts_mode: "Idioma Voz", tts_local: "Local", tts_en: "Inglês", tts_orig: "Original", tts_voice: "Voz TTS", nt_warden: "🚫 Não Tocar", nt_face: "Rosto", nt_chest: "Peito", nt_crotch: "Virilha", btn_audio: "🔊 Áudio", btn_start: "INICIAR", btn_tg: "Conectar Remoto", prep_time: "Início(s)", alert_time: "Duração(s)", msg_warn: "Texto Aviso", msg_pen: "Texto Pena", auto_photo: "Auto-Foto", prev_title: "Prévia", prev_empty: "Nenhuma", btn_hide: "Ocultar Timer", btn_show: "👀 Mostrar Timer", stat_time: "Tempo / Status", stat_zone: "Zona 3D:", stat_pose: "Pose:", stat_hand: "Mãos:", stat_face: "Rosto:", stat_warden: "Gaiola/Tocar:", stat_cage: "Gaiola:", stat_touch: "Tocar:", spkStart: "Iniciado. Prepare-se.", spkPen: "Pena!", spkSec: "segundos.", spkOver: "Fim! O tempo acabou.", spkErr: "Erro detectado!", spkCorr: "Bem corrigido!", err_stop: "Pare o jogo ativo!", err_person: "Nenhuma pessoa!", prompt_pose: "Nome da pose:", err_tg: "Erro Telegram.", err_net: "Erro na rede.", err_quota: "Memória cheia!", err_del: "Excluir pose?", cam_https: "Requer HTTPS", cam_block: "Bloqueada", cam_none: "Sem Câmera", btn_conn: "Conectando...", btn_act: "Ativo ✓", btn_err: "Erro Bot!", btn_net: "Erro Rede!", st_setup: "Setup", st_lock: "Fixo", st_miss: "Falta", st_off: "Off", st_ok: "OK", st_free: "Livre", st_ign: "Ignorar", st_hid: "Oculto", st_wait: "Espera", tg_menu: "🎮 Painel de Controle", tg_status: "📊 STATUS", tg_start: "Iniciar", tg_stop: "Parar", tg_spy: "Foto Espiã", tg_photo: "Auto-Foto", tg_hide: "Ocultar Timer", tg_show: "Mostrar Timer", tg_blind: "Modo Cego", tg_ui: "Mostrar UI", tg_saved: "Salvo!", tg_fail: "Falhou!", tg_busy: "⏳ Ocupado...", err_mouth: "Boca", err_eye_l: "Olho Esq", err_eye_r: "Olho Dir", err_hand_l: "Mão Esq", err_hand_r: "Mão Dir", err_pose: "Pose", err_cage_in: "IN-Gaiola", err_cage_out: "OUT-Gaiola", err_touch_f: "Toque Rosto", err_touch_c: "Toque Peito", err_touch_cr: "Toque Virilha", err_zone_x: "Zona X", err_zone_y: "Zona Y", err_zone_z: "Zona Z" }, ru: { cam: "Камера", lang: "Язык", pose: "Поза", p_free: "Свободно", p_cat_std: "--- Стандарт ---", p_tpose: "Т-Поза", p_yoga: "Дерево", p_hands: "Руки за голову", p_cat_end: "--- Выносливость ---", p_wall: "Стул", p_surrender: "На коленях", p_cat_cust: "--- Свои ---", upload: "📁 Загрузка", capture: "📷 Снимок", btn_delete: "🗑️ Удалить", add_cam: "➕ Добавить", ip_cam: "IP Камера", add_ip: "➕ Добавить IP", cam_split: "Сплит Режим", ai_level: "Точность ИИ", ai_fast: "Быстро (0)", ai_bal: "Баланс (1)", ai_prec: "Точно (2)", holo: "Оверлей", h_both: "Пред & Штраф", h_start: "Только Старт", h_always: "Всегда", h_warn: "Только Пред", h_penalty: "Только Штраф", h_vector: "Вектор", zone_check: "Зона 3D", z_on: "Вкл", z_off: "Выкл", z_x: "X-Допуск %", z_y: "Y-Допуск %", z_z: "Z-Допуск %", closer: "Ближе", warden_draw: "Зоны", w_in: "🟩 ВНУТРИ", w_out: "🟥 СНАРУЖИ", w_clear: "🗑️ Очистить", mouth: "Рот", m_ignore: "Неважно", m_open: "Открыт", m_closed: "Закрыт", eyes: "Глаза", eye_both: "Глаза", eye_l: "Л. Глаз", eye_r: "П. Глаз", sep_eyes: "Раздельно", e_ignore: "Неважно", e_open: "Открыты", e_closed: "Закрыты", e_sens: "Чувств. %", hands: "Руки", hand_both: "Руки", hand_l: "Л. Рука", hand_r: "П. Рука", sep_hands: "Раздельно", lh: "Л. Рука", rh: "П. Рука", h_ignore: "Неважно", h_fist: "Кулак", h_spread: "Раскрыта", h_together: "Сомкнута", body_tol: "Допуск°", body_sens: "Чувств. Позы %", time_target: "Цель", time_grace: "Пред(с)", time_pen: "Штраф(с)", esc_win: "Окно Эск", esc_add: "Штраф Эск", tts_mode: "Язык", tts_local: "Местный", tts_en: "Английский", tts_orig: "Оригинал", tts_voice: "Голос", nt_warden: "🚫 Без касаний (3D)", nt_face: "Лицо", nt_chest: "Грудь", nt_crotch: "Пах", btn_audio: "🔊 Звук", btn_start: "СТАРТ", btn_tg: "Подключить", prep_time: "Старт(с)", alert_time: "Длит.(с)", msg_warn: "Текст Пред", msg_pen: "Текст Штраф", auto_photo: "Авто-Фото", prev_title: "Превью", prev_empty: "Нет позы", btn_hide: "Скрыть Таймер", btn_show: "👀 Показать Таймер", stat_time: "Время / Статус", stat_zone: "Зона 3D:", stat_pose: "Поза:", stat_hand: "Руки:", stat_face: "Лицо:", stat_warden: "Клетка/Касание:", stat_cage: "Клетка:", stat_touch: "Касание:", spkStart: "Началось. Приготовьтесь.", spkPen: "Штраф!", spkSec: "секунд.", spkOver: "Успех! Время вышло.", spkErr: "Обнаружена ошибка!", spkCorr: "Исправлено!", err_stop: "Остановите игру!", err_person: "Никого нет!", prompt_pose: "Имя позы:", err_tg: "Ошибка Telegram.", err_net: "Ошибка загрузки изображения.", err_quota: "Память заполнена!", err_del: "Удалить позу?", cam_https: "Нужен HTTPS", cam_block: "Блок", cam_none: "Нет Камеры", btn_conn: "Подключение...", btn_act: "Активно ✓", btn_err: "Ошибка Бота!", btn_net: "Ошибка Сети!", st_setup: "Setup", st_lock: "Захвачено", st_miss: "Нет", st_off: "Выкл", st_ok: "OK", st_free: "Свободно", st_ign: "Игнор", st_hid: "Скрыто", st_wait: "Ожидание", tg_menu: "🎮 Панель Управления", tg_status: "📊 СТАТУС", tg_start: "Старт", tg_stop: "Стоп", tg_spy: "Шпионское Фото", tg_photo: "Авто-Фото", tg_hide: "Скрыть Таймер", tg_show: "Показать Таймер", tg_blind: "Вслепую", tg_ui: "Показать UI", tg_saved: "Сохранено!", tg_fail: "Ошибка!", tg_busy: "⏳ Занято...", err_mouth: "Рот", err_eye_l: "Л. Глаз", err_eye_r: "П. Глаз", err_hand_l: "Л. Рука", err_hand_r: "П. Рука", err_pose: "Поза", err_cage_in: "IN-Клетка", err_cage_out: "OUT-Клетка", err_touch_f: "Касание Лицо", err_touch_c: "Касание Грудь", err_touch_cr: "Касание Пах", err_zone_x: "Зона X", err_zone_y: "Зона Y", err_zone_z: "Зона Z" }, zh: { cam: "本地相机", lang: "语言", pose: "姿势", p_free: "自由活动", p_cat_std: "--- 标准 ---", p_tpose: "T字姿势", p_yoga: "树式", p_hands: "抱头", p_cat_end: "--- 耐力 ---", p_wall: "靠墙坐", p_surrender: "跪姿", p_cat_cust: "--- 自定义 ---", upload: "📁 上传", capture: "📷 拍照", btn_delete: "🗑️ 删除", add_cam: "➕ 添加", ip_cam: "IP 相机", add_ip: "➕ 添加IP", cam_split: "分屏模式", ai_level: "AI精度", ai_fast: "快速 (0)", ai_bal: "平衡 (1)", ai_prec: "精确 (2)", holo: "叠加", h_both: "警告与惩罚", h_start: "仅开始", h_always: "总是", h_warn: "仅警告", h_penalty: "仅惩罚", h_vector: "矢量", zone_check: "3D区域", z_on: "开", z_off: "关", z_x: "X-容差 %", z_y: "Y-容差 %", z_z: "Z-容差 %", closer: "靠近", warden_draw: "绘制区域", w_in: "🟩 内", w_out: "🟥 外", w_clear: "🗑️ 清除", mouth: "嘴", m_ignore: "忽略", m_open: "张开", m_closed: "闭合", eyes: "眼睛", eye_both: "眼睛", eye_l: "左眼", eye_r: "右眼", sep_eyes: "分开", e_ignore: "忽略", e_open: "睁开", e_closed: "闭合", e_sens: "灵敏度 %", hands: "手", hand_both: "双手", hand_l: "左手", hand_r: "右手", sep_hands: "分开", lh: "左手", rh: "右手", h_ignore: "忽略", h_fist: "拳头", h_spread: "张开", h_together: "并拢", body_tol: "容差°", body_sens: "姿势灵敏度 %", time_target: "目标", time_grace: "警告(秒)", time_pen: "惩罚(秒)", esc_win: "升级窗", esc_add: "升级加", tts_mode: "语音", tts_local: "本地", tts_en: "英语", tts_orig: "原文", tts_voice: "声音", nt_warden: "🚫 禁触区 (3D)", nt_face: "脸", nt_chest: "胸", nt_crotch: "裆", btn_audio: "🔊 音频", btn_start: "开始", btn_tg: "连接", prep_time: "开始(秒)", alert_time: "显示(秒)", msg_warn: "警告文本", msg_pen: "惩罚文本", auto_photo: "自动拍照", prev_title: "预览", prev_empty: "未选择", btn_hide: "隐藏计时器", btn_show: "👀 显示计时器", stat_time: "时间 / 状态", stat_zone: "3D区域:", stat_pose: "姿势:", stat_hand: "手:", stat_face: "脸:", stat_warden: "禁区/触摸:", stat_cage: "禁区:", stat_touch: "触摸:", spkStart: "开始了。 准备。", spkPen: "惩罚!", spkSec: "秒。", spkOver: "成功! 时间到。", spkErr: "检测到错误!", spkCorr: "已纠正!", err_stop: "请停止游戏!", err_person: "未检测到人!", prompt_pose: "名称:", err_tg: "Telegram错误", err_net: "网络错误", err_quota: "存储满!", err_del: "确认删除?", cam_https: "需要HTTPS", cam_block: "被阻止", cam_none: "无相机", btn_conn: "连接中...", btn_act: "已连接 ✓", btn_err: "Bot错误!", btn_net: "网络错误!", st_setup: "设置中", st_lock: "已锁定", st_miss: "丢失", st_off: "关闭", st_ok: "正常", st_free: "自由", st_ign: "忽略", st_hid: "隐藏", st_wait: "等待", tg_menu: "🎮 控制面板", tg_status: "📊 状态", tg_start: "开始", tg_stop: "停止", tg_spy: "间谍照片", tg_photo: "自动拍照", tg_hide: "隐藏计时器", tg_show: "显示计时器", tg_blind: "盲区模式", tg_ui: "显示UI", tg_saved: "已保存!", tg_fail: "失败!", tg_busy: "⏳ 忙碌中...", err_mouth: "嘴巴", err_eye_l: "左眼", err_eye_r: "右眼", err_hand_l: "左手", err_hand_r: "右手", err_pose: "姿势", err_cage_in: "内-禁区", err_cage_out: "外-禁区", err_touch_f: "触摸脸", err_touch_c: "触摸胸", err_touch_cr: "触摸裆", err_zone_x: "区域 X", err_zone_y: "区域 Y", err_zone_z: "区域 Z" }, ja: { cam: "カメラ", lang: "言語", pose: "ポーズ", p_free: "フリー", p_cat_std: "--- 標準 ---", p_tpose: "Tポーズ", p_yoga: "木", p_hands: "頭の後ろで手", p_cat_end: "--- 耐久 ---", p_wall: "空気椅子", p_surrender: "膝立ち", p_cat_cust: "--- カスタム ---", upload: "📁 アップ", capture: "📷 撮影", btn_delete: "🗑️ 削除", add_cam: "➕ 追加", ip_cam: "IP カメラ", add_ip: "➕ IP追加", cam_split: "スプリットモード", ai_level: "AI 精度", ai_fast: "高速 (0)", ai_bal: "バランス (1)", ai_prec: "正確 (2)", holo: "オーバーレイ", h_both: "警告と罰", h_start: "開始のみ", h_always: "常に", h_warn: "警告のみ", h_penalty: "罰のみ", h_vector: "ベクター", zone_check: "3Dゾーン", z_on: "オン", z_off: "オフ", z_x: "X-許容 %", z_y: "Y-許容 %", z_z: "Z-許容 %", closer: "接近", warden_draw: "ケージ描画", w_in: "🟩 IN", w_out: "🟥 OUT", w_clear: "🗑️ クリア", mouth: "口", m_ignore: "無視", m_open: "開く", m_closed: "閉じる", eyes: "目", eye_both: "目", eye_l: "左目", eye_r: "右目", sep_eyes: "分割", e_ignore: "無視", e_open: "開く", e_closed: "閉じる", e_sens: "感度 %", hands: "手", hand_both: "両手", hand_l: "左手", hand_r: "右手", sep_hands: "分割", lh: "左手", rh: "右手", h_ignore: "無視", h_fist: "拳", h_spread: "開く", h_together: "閉じる", body_tol: "許容°", body_sens: "ポーズ感度 %", time_target: "目標", time_grace: "警告(秒)", time_pen: "罰(秒)", esc_win: "Esc枠", esc_add: "Esc追加", tts_mode: "音声", tts_local: "ローカル", tts_en: "英語", tts_orig: "オリジナル", tts_voice: "声", nt_warden: "🚫 接触禁止 (3D)", nt_face: "顔", nt_chest: "胸", nt_crotch: "股間", btn_audio: "🔊 音声", btn_start: "スタート", btn_tg: "リモート接続", prep_time: "開始(秒)", alert_time: "表示(秒)", msg_warn: "警告テキスト", msg_pen: "罰テキスト", auto_photo: "自動写真", prev_title: "プレビュー", prev_empty: "未選択", btn_hide: "タイマー隠す", btn_show: "👀 タイマー表示", stat_time: "時間 / 状態", stat_zone: "3Dゾーン:", stat_pose: "ポーズ:", stat_hand: "手:", stat_face: "顔:", stat_warden: "ケージ/タッチ:", stat_cage: "ケージ:", stat_touch: "タッチ:", spkStart: "開始しました。準備して。", spkPen: "ペナルティ!", spkSec: "秒。", spkOver: "終了!時間が来ました。", spkErr: "エラーを検出しました!", spkCorr: "修正完了!", err_stop: "ゲームを停止して!", err_person: "人がいません!", prompt_pose: "名前:", err_tg: "Telegramエラー", err_net: "通信エラー", err_quota: "容量不足!", err_del: "削除しますか?", cam_https: "HTTPSが必要", cam_block: "ブロック", cam_none: "カメラなし", btn_conn: "接続中...", btn_act: "接続済 ✓", btn_err: "Botエラー!", btn_net: "通信エラー!", st_setup: "設定中", st_lock: "ロック", st_miss: "見失う", st_off: "オフ", st_ok: "OK", st_free: "フリー", st_ign: "無視", st_hid: "非表示", st_wait: "待機中", tg_menu: "🎮 コントロールパネル", tg_status: "📊 ステータス", tg_start: "スタート", tg_stop: "ストップ", tg_spy: "スパイ写真", tg_photo: "自動写真", tg_hide: "タイマー隠す", tg_show: "タイマー表示", tg_blind: "ブラインド", tg_ui: "UI表示", tg_saved: "保存しました!", tg_fail: "失敗!", tg_busy: "⏳ 処理中...", err_mouth: "口", err_eye_l: "左目", err_eye_r: "右目", err_hand_l: "左手", err_hand_r: "右手", err_pose: "ポーズ", err_cage_in: "IN-ケージ", err_cage_out: "OUT-ケージ", err_touch_f: "タッチ 顔", err_touch_c: "タッチ 胸", err_touch_cr: "タッチ 股間", err_zone_x: "ゾーン X", err_zone_y: "ゾーン Y", err_zone_z: "ゾーン Z" }, pl: { cam: "Kamera", lang: "Język", pose: "Poza", p_free: "Dowolnie", p_cat_std: "--- Standard ---", p_tpose: "Poza T", p_yoga: "Drzewo", p_hands: "Ręce za głową", p_cat_end: "--- Wytrzymałość ---", p_wall: "Krzesełko", p_surrender: "Klęczący", p_cat_cust: "--- Własne ---", upload: "📁 Wgraj", capture: "📷 Zrób", btn_delete: "🗑️ Usuń", add_cam: "➕ Dodaj", ip_cam: "Kamera IP", add_ip: "➕ Dodaj IP", cam_split: "Tryb Split", ai_level: "Precyzja AI", ai_fast: "Szybko (0)", ai_bal: "Balans (1)", ai_prec: "Dokładnie (2)", holo: "Nakładka", h_both: "Ostrz & Kara", h_start: "Tylko Start", h_always: "Zawsze", h_warn: "Tylko Ostrz", h_penalty: "Tylko Kara", h_vector: "Wektor", zone_check: "Strefa 3D", z_on: "Wł", z_off: "Wył", z_x: "X-Tol %", z_y: "Y-Tol %", z_z: "Z-Tol %", closer: "Bliżej", warden_draw: "Rysuj Klatkę", w_in: "🟩 WEW", w_out: "🟥 ZEW", w_clear: "🗑️ Usuń", mouth: "Usta", m_ignore: "Ignoruj", m_open: "Otwarte", m_closed: "Zamknięte", eyes: "Oczy", eye_both: "Oczy", eye_l: "L. Oko", eye_r: "P. Oko", sep_eyes: "Osobno", e_ignore: "Ignoruj", e_open: "Otwarte", e_closed: "Zamknięte", e_sens: "Czułość %", hands: "Dłonie", hand_both: "Dłonie", hand_l: "L. Ręka", hand_r: "P. Ręka", sep_hands: "Osobno", lh: "L. Ręka", rh: "P. Ręka", h_ignore: "Ignoruj", h_fist: "Pięść", h_spread: "Rozchylone", h_together: "Złączone", body_tol: "Tol Ciała°", body_sens: "Czułość Pozy %", time_target: "Cel", time_grace: "Ostrz(s)", time_pen: "Kara(s)", esc_win: "Okno Esc", esc_add: "Dod Esc", tts_mode: "Głos", tts_local: "Lokalny", tts_en: "Angielski", tts_orig: "Oryginał", tts_voice: "Mowa", nt_warden: "🚫 Bez Dotyku (3D)", nt_face: "Twarz", nt_chest: "Klatka", nt_crotch: "Krocze", btn_audio: "🔊 Dźwięk", btn_start: "START", btn_tg: "Połącz", prep_time: "Start(s)", alert_time: "Czas(s)", msg_warn: "Tekst Ostrz", msg_pen: "Tekst Kary", auto_photo: "Auto-Zdjęcie", prev_title: "Podgląd", prev_empty: "Brak pozy", btn_hide: "Ukryj Zegar", btn_show: "👀 Pokaż Zegar", stat_time: "Czas / Status", stat_zone: "Strefa 3D:", stat_pose: "Poza:", stat_hand: "Dłonie:", stat_face: "Twarz:", stat_warden: "Klatka/Dotyk:", stat_cage: "Klatka:", stat_touch: "Dotyk:", spkStart: "Rozpoczęto. Przygotuj się.", spkPen: "Kara!", spkSec: "sekund.", spkOver: "Koniec! Czas minął.", spkErr: "Wykryto błąd!", spkCorr: "Korekta!", err_stop: "Zatrzymaj grę!", err_person: "Brak osoby!", prompt_pose: "Nazwa:", err_tg: "Błąd Telegram.", err_net: "Błąd sieci.", err_quota: "Pamięć pełna!", err_del: "Usunąć?", cam_https: "Wymagane HTTPS", cam_block: "Zablokowana", cam_none: "Brak Kamery", btn_conn: "Łączenie...", btn_act: "Aktywny ✓", btn_err: "Błąd Bota!", btn_net: "Błąd Sieci!", st_setup: "Setup", st_lock: "Zablok.", st_miss: "Brak", st_off: "Wył", st_ok: "OK", st_free: "Wolne", st_ign: "Ignoruj", st_hid: "Ukryte", st_wait: "Czekam", tg_menu: "🎮 Panel Sterowania", tg_status: "📊 STATUS", tg_start: "Start", tg_stop: "Stop", tg_spy: "Zdjęcie Szpieg", tg_photo: "Auto-Foto", tg_hide: "Ukryj Zegar", tg_show: "Pokaż Zegar", tg_blind: "W ciemno", tg_ui: "Pokaż UI", tg_saved: "Zapisano!", tg_fail: "Błąd!", tg_busy: "⏳ Trwa akcja...", err_mouth: "Usta", err_eye_l: "Oko L", err_eye_r: "Oko P", err_hand_l: "Ręka L", err_hand_r: "Ręka P", err_pose: "Poza", err_cage_in: "IN-Klatka", err_cage_out: "OUT-Klatka", err_touch_f: "Dotyk Twarz", err_touch_c: "Dotyk Klatka", err_touch_cr: "Dotyk Krocze", err_zone_x: "Strefa X", err_zone_y: "Strefa Y", err_zone_z: "Strefa Z" } }; let curLang = "de"; const sysLang = (navigator.language || navigator.userLanguage || "de").split('-')[0].toLowerCase(); if (translations[sysLang]) { curLang = sysLang; } function t(key) { return (translations[curLang] && translations[curLang][key]) ? translations[curLang][key] : (translations['en'][key] || key); } let tgLang = "de"; function t_tg(key) { return (translations[tgLang] && translations[tgLang][key]) ? translations[tgLang][key] : (translations['en'][key] || key); } function updateUI_Language() { document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); if (el.tagName === 'OPTION') { el.text = t(key); } else if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') { el.placeholder = t(key); } else { el.innerHTML = t(key); } }); populateVoices(); } // ============================================================ // --- 2. AUDIO & SPEECH ENGINE --- // ============================================================ const AudioContext = window.AudioContext || window.webkitAudioContext; const audioCtx = new AudioContext(); const unlockAudio = () => { if (audioCtx.state === 'suspended') { audioCtx.resume().catch(()=>{}); } }; ['click', 'touchstart', 'keydown'].forEach(evt => document.body.addEventListener(evt, unlockAudio, { once: true })); function isAudioOn() { return !document.getElementById('btn-voice').classList.contains('off'); } function playTone(type, freq, duration, vol = 0.1) { if (!isAudioOn()) return; if (audioCtx.state === 'suspended') { audioCtx.resume().catch(() => {}); } try { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, audioCtx.currentTime); gain.gain.setValueAtTime(vol, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + duration); } catch (e) {} } const Sounds = { tick: () => playTone('sine', 800, 0.1, 0.15), start: () => { playTone('sine', 1046.50, 0.2, 0.2); setTimeout(() => playTone('sine', 1318.51, 0.4, 0.2), 100); }, warning: () => playTone('sawtooth', 150, 0.4, 0.3), penalty: () => { playTone('sawtooth', 100, 0.6, 0.4); playTone('square', 105, 0.6, 0.4); }, success: () => { playTone('sine', 523.25, 1.5, 0.2); playTone('sine', 659.25, 1.5, 0.2); playTone('sine', 783.99, 1.5, 0.2); } }; let availableVoices = []; const voiceSelect = document.getElementById('voice-select'); function populateVoices() { if (!window.speechSynthesis) return; availableVoices = window.speechSynthesis.getVoices(); const prevVal = voiceSelect.value; voiceSelect.innerHTML = ``; availableVoices.forEach((voice, index) => { const option = document.createElement('option'); option.value = index; option.textContent = voice.name.substring(0, 22) + ` (${voice.lang})`; voiceSelect.appendChild(option); }); if (prevVal) voiceSelect.value = prevVal; } if (window.speechSynthesis) window.speechSynthesis.onvoiceschanged = populateVoices; document.getElementById('btn-voice').onclick = () => { unlockAudio(); const btn = document.getElementById('btn-voice'); const isOff = btn.classList.toggle('off'); btn.innerHTML = isOff ? "🔇 " + t('btn_audio') : "🔊 " + t('btn_audio'); }; function speakNow(msg, forceLang = null) { if (!isAudioOn() || !window.speechSynthesis) return; window.speechSynthesis.cancel(); const u = new SpeechSynthesisUtterance(msg); if (voiceSelect.value !== "") { const idx = parseInt(voiceSelect.value); if (availableVoices[idx]) u.voice = availableVoices[idx]; } else { let targetLang = forceLang || (document.getElementById('tts-mode').value === 'en' ? 'en' : curLang); if (document.getElementById('tts-mode').value !== 'orig') u.lang = targetLang; if (availableVoices.length > 0) { let voice = availableVoices.find(v => v.lang.toLowerCase().startsWith(targetLang.toLowerCase())); if (voice) u.voice = voice; } } try { window.speechSynthesis.speak(u); } catch(e) {} } // ============================================================ // --- 3. HELPER MATH & PARSER (100% LERP Toleranzen) --- // ============================================================ function parseTimeStr(str) { if (!str) return 0; str = str.toString().trim(); if (/^\d+$/.test(str)) return parseInt(str); let total = 0; const hMatch = str.match(/(\d+)\s*[hH]/); if (hMatch) total += parseInt(hMatch[1]) * 3600; const mMatch = str.match(/(\d+)\s*[mM]/); if (mMatch) total += parseInt(mMatch[1]) * 60; const sMatch = str.match(/(\d+)\s*[sS]/); if (sMatch) total += parseInt(sMatch[1]); return total > 0 ? total : (parseInt(str) || 0); } function getDistance3D(p1, p2) { return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2) + Math.pow(p1.z - p2.z, 2)); } function getDistance2D(p1, p2) { return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); } function calculateEAR2D(eyePoints, landmarks) { const v1 = getDistance2D(landmarks[eyePoints[1]], landmarks[eyePoints[5]]); const v2 = getDistance2D(landmarks[eyePoints[2]], landmarks[eyePoints[4]]); const h = getDistance2D(landmarks[eyePoints[0]], landmarks[eyePoints[3]]); return h <= 0.001 ? 0 : (v1 + v2) / (2.0 * h); } function lerp(start, end, amt) { return (1 - amt) * start + amt * end; } function extractFaceState(lm, mouthSens, eyeSensL, eyeSensR, currentFaceState) { if (!lm) return currentFaceState; const fH = getDistance3D(lm[10], lm[152]); const mR = fH <= 0.001 ? 0 : getDistance3D(lm[13], lm[14]) / fH; const mouthOpenThresh = lerp(0.01, 0.08, mouthSens / 100); const mouthClosedThresh = lerp(0.15, 0.02, mouthSens / 100); const eL_val = calculateEAR2D([33, 160, 158, 133, 153, 144], lm); const eR_val = calculateEAR2D([362, 385, 387, 263, 373, 380], lm); let maxEarL = currentFaceState.maxEarL || 0.25; let maxEarR = currentFaceState.maxEarR || 0.25; if (eL_val > maxEarL) { maxEarL = maxEarL * 0.6 + eL_val * 0.4; } else if (maxEarL > 0.25) { maxEarL -= 0.0001; } if (eR_val > maxEarR) { maxEarR = maxEarR * 0.6 + eR_val * 0.4; } else if (maxEarR > 0.25) { maxEarR -= 0.0001; } maxEarL = Math.max(0.20, Math.min(maxEarL, 0.40)); maxEarR = Math.max(0.20, Math.min(maxEarR, 0.40)); const eyeOpenThreshL = lerp(0.15, 0.28, eyeSensL / 100); const eyeClosedThreshL = lerp(0.35, 0.15, eyeSensL / 100); const eyeOpenThreshR = lerp(0.15, 0.28, eyeSensR / 100); const eyeClosedThreshR = lerp(0.35, 0.15, eyeSensR / 100); return { mouth: { raw: mR, open: mR > mouthOpenThresh, closed: mR < mouthClosedThresh }, eyeL: { raw: eL_val, open: eL_val > eyeOpenThreshL, closed: eL_val < eyeClosedThreshL }, eyeR: { raw: eR_val, open: eR_val > eyeOpenThreshR, closed: eR_val < eyeClosedThreshR }, maxEarL: maxEarL, maxEarR: maxEarR }; } function getSecureAngle(wa, wb, wc, a, b, c) { if (!a || !b || !c || !wa || !wb || !wc || a.visibility < 0.05 || b.visibility < 0.05 || c.visibility < 0.05) return undefined; const ba = { x: wa.x - wb.x, y: wa.y - wb.y, z: wa.z - wb.z }; const bc = { x: wc.x - wb.x, y: wc.y - wb.y, z: wc.z - wb.z }; const dot = ba.x * bc.x + ba.y * bc.y + ba.z * bc.z; const magA = Math.sqrt(ba.x ** 2 + ba.y ** 2 + ba.z ** 2); const magC = Math.sqrt(bc.x ** 2 + bc.y ** 2 + bc.z ** 2); if (magA <= 0.001 || magC <= 0.001) return 0; let val = Math.max(-1, Math.min(1, dot / (magA * magC))); return Math.acos(val) * (180 / Math.PI); } function extractBodyAngles(lm, wLm) { if (!wLm) wLm = lm; const angles = { ls: getSecureAngle(wLm[23], wLm[11], wLm[13], lm[23], lm[11], lm[13]), rs: getSecureAngle(wLm[24], wLm[12], wLm[14], lm[24], lm[12], lm[14]), le: getSecureAngle(wLm[11], wLm[13], wLm[15], lm[11], lm[13], lm[15]), re: getSecureAngle(wLm[12], wLm[14], wLm[16], lm[12], lm[14], lm[16]), lhip: getSecureAngle(wLm[11], wLm[23], wLm[25], lm[11], lm[23], lm[25]), rhip: getSecureAngle(wLm[12], wLm[24], wLm[26], lm[12], lm[24], lm[26]), lk: getSecureAngle(wLm[23], wLm[25], wLm[27], lm[23], lm[25], lm[27]), rk: getSecureAngle(wLm[24], wLm[26], wLm[28], lm[24], lm[26], lm[28]), lfoot: getSecureAngle(wLm[25], wLm[27], wLm[31], lm[25], lm[27], lm[31]), rfoot: getSecureAngle(wLm[26], wLm[28], wLm[32], lm[26], lm[28], lm[32]) }; Object.keys(angles).forEach(k => { if (angles[k] === undefined || isNaN(angles[k])) { delete angles[k]; } else { angles[k] = Math.round(angles[k]); } }); return angles; } function checkHandState(lm, rule, sens) { if (!lm) return "hidden"; if (rule === "ignore") return "ok"; if (sens === 0) return "ok"; let ext = 0; if (getDistance2D(lm[0], lm[8]) > getDistance2D(lm[0], lm[6])) ext++; if (getDistance2D(lm[0], lm[12]) > getDistance2D(lm[0], lm[10])) ext++; if (getDistance2D(lm[0], lm[16]) > getDistance2D(lm[0], lm[14])) ext++; if (getDistance2D(lm[0], lm[20]) > getDistance2D(lm[0], lm[18])) ext++; const spreadDist = getDistance2D(lm[8], lm[12]) + getDistance2D(lm[12], lm[16]) + getDistance2D(lm[16], lm[20]); const palmSize = getDistance2D(lm[0], lm[9]); const s = sens / 100; if (rule === "fist") { const maxExt = Math.floor(lerp(4, 0, s)); return ext <= maxExt ? "ok" : "err"; } if (rule === "spread") { const minExt = Math.floor(lerp(1, 4, s)); const minSpread = lerp(0.1, 1.2, s) * palmSize; return (ext >= minExt && spreadDist > minSpread) ? "ok" : "err"; } if (rule === "together") { const minExt = Math.floor(lerp(1, 4, s)); const maxSpread = lerp(3.0, 0.5, s) * palmSize; return (ext >= minExt && spreadDist < maxSpread) ? "ok" : "err"; } return "err"; } function drawVectorSkeleton(ctx, landmarks, w, h) { ctx.clearRect(0, 0, w, h); if (!landmarks) return; ctx.strokeStyle = '#00FF00'; ctx.lineWidth = 2; const conn = window.POSE_CONNECTIONS || []; conn.forEach(c => { const p1 = landmarks[c[0]]; const p2 = landmarks[c[1]]; if (p1 && p2 && p1.visibility > 0.1 && p2.visibility > 0.1) { ctx.beginPath(); ctx.moveTo(p1.x * w, p1.y * h); ctx.lineTo(p2.x * w, p2.y * h); ctx.stroke(); } }); ctx.fillStyle = '#FF0000'; landmarks.forEach(p => { if (p && p.visibility > 0.1) { ctx.beginPath(); ctx.arc(p.x * w, p.y * h, 3, 0, 2 * Math.PI); ctx.fill(); } }); } function generateDummyVector(poseType) { let lm = Array(33).fill(null).map(() => ({ x: 0.5, y: 0.5, z: 0, visibility: 0 })); const setP = (i, x, y) => { lm[i] = { x, y, z: 0, visibility: 0.9 }; }; setP(0, 0.5, 0.12); setP(7, 0.44, 0.13); setP(8, 0.56, 0.13); setP(11, 0.38, 0.28); setP(12, 0.62, 0.28); setP(23, 0.44, 0.52); setP(24, 0.56, 0.52); if (poseType === 't_pose') { setP(13, 0.18, 0.28); setP(14, 0.82, 0.28); setP(15, 0.04, 0.28); setP(16, 0.96, 0.28); setP(25, 0.44, 0.72); setP(26, 0.56, 0.72); setP(27, 0.44, 0.92); setP(28, 0.56, 0.92); } else if (poseType === 'hands_behind_head') { setP(13, 0.32, 0.12); setP(14, 0.68, 0.12); setP(15, 0.44, 0.06); setP(16, 0.56, 0.06); setP(25, 0.44, 0.72); setP(26, 0.56, 0.72); setP(27, 0.44, 0.92); setP(28, 0.56, 0.92); } else if (poseType === 'yoga_tree') { setP(13, 0.32, 0.42); setP(14, 0.68, 0.42); setP(15, 0.5, 0.55); setP(16, 0.5, 0.55); setP(25, 0.44, 0.72); setP(26, 0.72, 0.60); setP(27, 0.44, 0.92); setP(28, 0.56, 0.72); } else if (poseType === 'wall_sit') { setP(13, 0.38, 0.42); setP(14, 0.62, 0.42); setP(15, 0.38, 0.56); setP(16, 0.62, 0.56); setP(25, 0.34, 0.52); setP(26, 0.66, 0.52); setP(27, 0.34, 0.78); setP(28, 0.66, 0.78); } else if (poseType === 'surrender') { setP(13, 0.28, 0.04); setP(14, 0.72, 0.04); setP(15, 0.28, -0.08); setP(16, 0.72, -0.08); setP(25, 0.44, 0.78); setP(26, 0.56, 0.78); setP(27, 0.44, 0.94); setP(28, 0.56, 0.94); } return lm; } const STANDARD_POSES = { "t_pose": { desc: "T-Pose", img: "", rules: { ls: 90, rs: 90, le: 180, re: 180 }, mouth: "ignore", eyeL: "ignore", eyeR: "ignore", lh_rule: "ignore", rh_rule: "ignore", savedLandmarks: generateDummyVector('t_pose') }, "hands_behind_head": { desc: "Hands Behind Head", img: "", rules: { le: 40, re: 40, ls: 155, rs: 155 }, mouth: "ignore", eyeL: "ignore", eyeR: "ignore", lh_rule: "ignore", rh_rule: "ignore", savedLandmarks: generateDummyVector('hands_behind_head') }, "yoga_tree": { desc: "Yoga Tree", img: "", rules: { ls: 170, rs: 170 }, mouth: "ignore", eyeL: "ignore", eyeR: "ignore", lh_rule: "ignore", rh_rule: "ignore", savedLandmarks: generateDummyVector('yoga_tree') }, "wall_sit": { desc: "Wall Sit", img: "", rules: { lhip: 90, rhip: 90, lk: 90, rk: 90 }, mouth: "ignore", eyeL: "ignore", eyeR: "ignore", lh_rule: "ignore", rh_rule: "ignore", savedLandmarks: generateDummyVector('wall_sit') }, "surrender": { desc: "Surrender", img: "", rules: { ls: 160, rs: 160, lk: 90, rk: 90 }, mouth: "ignore", eyeL: "ignore", eyeR: "ignore", lh_rule: "ignore", rh_rule: "ignore", savedLandmarks: generateDummyVector('surrender') } }; let customPoses = {}; try { const stored = localStorage.getItem('custom_poses_v115_full'); if (stored) { customPoses = JSON.parse(stored); } } catch (e) {} let ALL_POSES = { ...STANDARD_POSES, ...customPoses }; let drawableZones = []; let isDrawing = false; let drawStartX = 0; let drawStartY = 0; let currentDrawRect = null; let drawMode = null; let activeDrawCanvas = null; let visualCageImageBase64 = null; const limbNames = { ls: "L.Schulter", rs: "R.Schulter", le: "L.Ellbow", re: "R.Ellbow", lhip: "L.Hüfte", rhip: "R.Hüfte", lk: "L.Knie", rk: "R.Knie", lfoot: "L.Fuß", rfoot: "R.Fuß" }; // ============================================================ // --- 4. MULTI-CAM ARCHITECTURE --- // ============================================================ let visionNodes = []; let camCounter = 0; function getNormalizedCoords(e, canvas) { const rect = canvas.getBoundingClientRect(); const canvasRatio = canvas.width / canvas.height; const rectRatio = rect.width / rect.height; let renderWidth = rect.width; let renderHeight = rect.height; let offsetX = 0; let offsetY = 0; if (canvasRatio > rectRatio) { renderHeight = rect.width / canvasRatio; offsetY = (rect.height - renderHeight) / 2; } else { renderWidth = rect.height * canvasRatio; offsetX = (rect.width - renderWidth) / 2; } const x = (e.clientX - rect.left - offsetX) / renderWidth; const y = (e.clientY - rect.top - offsetY) / renderHeight; return { x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)) }; } class VisionNode { constructor(type, source, labelText, crop = null) { this.id = 'cam_' + camCounter++; this.type = type; this.source = source; this.crop = crop; this.isActive = true; this.isProcessing = false; this.landmarks = null; this.worldLandmarks = null; this.faceLandmarks = null; this.leftHandLandmarks = null; this.rightHandLandmarks = null; this.angles = {}; this.faceState = { mouth: "ignore", eyeL: "ignore", eyeR: "ignore" }; this.hasPerson = false; this.startZoneX = null; this.startZoneY = null; this.startZoneDepth = null; this.isPresent = true; this.isZoneOk = true; this.isTouchOk = true; this.isPoseHeld = true; this.isFaceHeld = true; this.isHandHeld = true; this.cageErrors = []; this.touchErrors = []; this.faceErrors = []; this.handErrors = []; this.poseErrors = []; this.createDOM(labelText); this.initHolistic(); if(type === 'local') { this.startLocalStream(); } else if(type === 'ip') { this.startIPStream(); } } createDOM(labelText) { const grid = document.getElementById('vision-grid'); this.container = document.createElement('div'); this.container.className = 'vision-node'; this.container.id = `node_${this.id}`; this.label = document.createElement('div'); this.label.className = 'node-label'; this.label.innerText = labelText; const btnRemove = document.createElement('button'); btnRemove.className = 'btn-remove-node'; btnRemove.innerText = '🗑️'; btnRemove.onclick = () => { this.destroy(); }; this.canvas = document.createElement('canvas'); this.canvas.width = 640; this.canvas.height = 480; this.ctx = this.canvas.getContext('2d'); this.tempCanvas = document.createElement('canvas'); this.tempCanvas.width = 640; this.tempCanvas.height = 480; this.tempCtx = this.tempCanvas.getContext('2d'); this.ovPose = document.createElement('img'); this.ovPose.className = 'vision-overlay ov-pose'; this.ovVector = document.createElement('canvas'); this.ovVector.width = 640; this.ovVector.height = 480; this.ovVector.className = 'vision-overlay ov-vector'; this.vCtx = this.ovVector.getContext('2d'); this.ovCage = document.createElement('img'); this.ovCage.className = 'vision-overlay ov-cage'; if(this.type === 'local') { this.video = document.createElement('video'); this.video.autoplay = true; this.video.playsInline = true; this.video.muted = true; this.container.appendChild(this.video); } else { this.imgStream = document.createElement('img'); this.imgStream.className = 'ip-stream'; this.imgStream.crossOrigin = "anonymous"; this.container.appendChild(this.imgStream); } this.container.appendChild(this.label); this.container.appendChild(btnRemove); this.container.appendChild(this.canvas); this.container.appendChild(this.ovPose); this.container.appendChild(this.ovVector); this.container.appendChild(this.ovCage); grid.appendChild(this.container); this.canvas.addEventListener('mousedown', (e) => { if (!drawMode) return; activeDrawCanvas = this.canvas; const norm = getNormalizedCoords(e, this.canvas); drawStartX = norm.x; drawStartY = norm.y; isDrawing = true; }); this.canvas.addEventListener('mousemove', (e) => { if (!isDrawing || activeDrawCanvas !== this.canvas) return; const norm = getNormalizedCoords(e, this.canvas); currentDrawRect = { x: Math.min(drawStartX, norm.x), y: Math.min(drawStartY, norm.y), w: Math.abs(norm.x - drawStartX), h: Math.abs(norm.y - drawStartY) }; }); this.canvas.addEventListener('mouseup', () => { if (!isDrawing || activeDrawCanvas !== this.canvas) return; isDrawing = false; activeDrawCanvas = null; if (currentDrawRect && currentDrawRect.w > 0.02 && currentDrawRect.h > 0.02) { drawableZones.push({ type: drawMode, rect: currentDrawRect }); } currentDrawRect = null; }); this.canvas.addEventListener('mouseleave', () => { if(activeDrawCanvas === this.canvas) { isDrawing = false; activeDrawCanvas = null; currentDrawRect = null; } }); } initHolistic() { const complexity = parseInt(document.getElementById('ai-complexity').value) || 1; const minConf = complexity === 0 ? 0.5 : (complexity === 1 ? 0.6 : 0.7); this.holistic = new Holistic({ locateFile: (f) => `https://cdn.jsdelivr.net/npm/@mediapipe/holistic/${f}` }); this.holistic.setOptions({ modelComplexity: complexity, smoothLandmarks: true, enableSegmentation: false, refineFaceLandmarks: true, minDetectionConfidence: minConf, minTrackingConfidence: minConf }); this.holistic.onResults((res) => { this.onResults(res); }); } async startLocalStream() { try { this.stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: this.source }, width: { ideal: 1920 }, height: { ideal: 1080 } } }); this.video.srcObject = this.stream; await new Promise((resolve) => { this.video.onloadedmetadata = () => { this.video.play().then(resolve).catch(()=>resolve()); }; }); this.loop(); } catch (e) { this.label.innerText = "Error Local Cam"; } } startIPStream() { this.imgStream.onload = () => { this.ipLoaded = true; }; this.imgStream.onerror = () => { this.label.innerText = "IP Stream Error"; }; this.imgStream.src = this.source; this.loop(); } async loop() { if (!this.isActive) return; if (!this.isProcessing && !pendingImagePose) { this.isProcessing = true; try { let sourceElement = this.type === 'local' && this.video.readyState >= 2 ? this.video : (this.type === 'ip' && this.ipLoaded ? this.imgStream : null); if (sourceElement) { const vw = sourceElement.videoWidth || sourceElement.width || 640; const vh = sourceElement.videoHeight || sourceElement.height || 480; let sx = 0, sy = 0, sw = vw, sh = vh; if (this.crop) { sx = this.crop.x * vw; sy = this.crop.y * vh; sw = this.crop.w * vw; sh = this.crop.h * vh; } if (this.tempCanvas.width !== sw || this.tempCanvas.height !== sh) { this.tempCanvas.width = sw; this.tempCanvas.height = sh; this.canvas.width = sw; this.canvas.height = sh; this.ovVector.width = sw; this.ovVector.height = sh; } this.tempCtx.drawImage(sourceElement, sx, sy, sw, sh, 0, 0, sw, sh); await this.holistic.send({ image: this.tempCanvas }); } } catch(e) { if(e.name === 'SecurityError') { this.label.innerText = "IP CORS Error"; this.isActive = false; } } finally { this.isProcessing = false; } } if(this.isActive) { requestAnimationFrame(() => this.loop()); } } onResults(results) { if (!this.isActive) return; if (pendingImagePose && visionNodes.find(n => n.hasPerson) === this) { const cImg = pendingImagePose.imgData; const cSrc = pendingImagePose.source; const cName = pendingImagePose.name; if (results.poseLandmarks) { const extR = extractBodyAngles(results.poseLandmarks, results.poseWorldLandmarks); if (Object.keys(extR).length === 0) { if (cSrc === 'telegram') { sendTelegram(`❌ ${t_tg('err_person')} (Zu wenig Körper sichtbar)`); } else { alert(t('err_person')); } } else { const fs = extractFaceState(results.faceLandmarks, 50, 65, 65, this.faceState); if (cSrc === 'telegram') { saveCustomPose(cName, cImg, extR, fs.mouth.open ? 'open' : 'closed', fs.eyeL.open ? 'open' : 'closed', fs.eyeR.open ? 'open' : 'closed', results.poseLandmarks); sendTelegram(`✅ Pose gespeichert.`); } else if (cSrc === 'local') { setTimeout(() => { const name = prompt(t('prompt_pose')); if (name) { saveCustomPose(name, cImg, extR, fs.mouth.open ? 'open' : 'closed', fs.eyeL.open ? 'open' : 'closed', fs.eyeR.open ? 'open' : 'closed', results.poseLandmarks); } }, 100); } } } else { if (cSrc === 'telegram') { sendTelegram(`❌ ${t_tg('err_person')}`); } else { setTimeout(() => alert(t('err_person')), 100); } } pendingImagePose = null; return; } else if (pendingImagePose) { return; } this.landmarks = results.poseLandmarks || null; this.worldLandmarks = results.poseWorldLandmarks || null; this.faceLandmarks = results.faceLandmarks || null; this.leftHandLandmarks = results.leftHandLandmarks || null; this.rightHandLandmarks = results.rightHandLandmarks || null; this.hasPerson = !!this.landmarks; this.ctx.save(); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); if(results.image) { this.ctx.drawImage(results.image, 0, 0, this.canvas.width, this.canvas.height); } this.nodeErrors = []; this.cageErrors = []; this.touchErrors = []; this.faceErrors = []; this.handErrors = []; this.poseErrors = []; this.isZoneOk = true; this.isTouchOk = true; this.isPoseHeld = true; this.isFaceHeld = true; this.isHandHeld = true; if (this.hasPerson) { if (window.POSE_CONNECTIONS) { drawConnectors(this.ctx, this.landmarks, window.POSE_CONNECTIONS, { color: '#00FF00', lineWidth: 3 }); } if (window.HAND_CONNECTIONS) { if (this.leftHandLandmarks) { drawConnectors(this.ctx, this.leftHandLandmarks, window.HAND_CONNECTIONS, { color: '#00CC00', lineWidth: 2 }); } if (this.rightHandLandmarks) { drawConnectors(this.ctx, this.rightHandLandmarks, window.HAND_CONNECTIONS, { color: '#00CC00', lineWidth: 2 }); } } if (this.faceLandmarks && window.FACEMESH_TESSELATION) { drawConnectors(this.ctx, this.faceLandmarks, window.FACEMESH_TESSELATION, { color: '#C0C0C030', lineWidth: 1 }); } let tX = this.landmarks[0].x; let tY = this.landmarks[0].y; drawableZones.forEach(z => { let r = z.rect; let pX = r.x * this.canvas.width; let pY = r.y * this.canvas.height; let pW = r.w * this.canvas.width; let pH = r.h * this.canvas.height; if (z.type === 'in') { this.ctx.strokeStyle = "rgba(76,175,80,0.8)"; this.ctx.lineWidth = 3; this.ctx.strokeRect(pX, pY, pW, pH); if (tX < r.x || tX > r.x + r.w || tY < r.y || tY > r.y + r.h) { this.isZoneOk = false; this.cageErrors.push(t('err_cage_in')); } } else { this.ctx.fillStyle = "rgba(244,67,54,0.15)"; this.ctx.fillRect(pX, pY, pW, pH); this.ctx.strokeStyle = "rgba(244,67,54,0.8)"; this.ctx.lineWidth = 2; this.ctx.strokeRect(pX, pY, pW, pH); if (tX > r.x && tX < r.x + r.w && tY > r.y && tY < r.y + r.h) { this.isZoneOk = false; this.cageErrors.push(t('err_cage_out')); } } }); // TRUE 3D NO-TOUCH const ntF = document.getElementById('nt-face').checked; const ntC = document.getElementById('nt-chest').checked; const ntCr = document.getElementById('nt-crotch').checked; if (ntF || ntC || ntCr) { const handIndices = [15, 17, 19, 21, 16, 18, 20, 22]; const activeHands = handIndices.map(i => this.landmarks[i]).filter(p => p && p.visibility > 0.4); if (activeHands.length > 0) { const check3DTouch = (targetCenter, radius2D, zTol, name, errString) => { let touched = false; for (let h of activeHands) { const aspect = this.canvas.width / this.canvas.height; const dx = (h.x - targetCenter.x) * aspect; const dy = (h.y - targetCenter.y); const dist2D = Math.sqrt(dx * dx + dy * dy); const zDiff = Math.abs(h.z - targetCenter.z); if (dist2D < radius2D && zDiff < zTol) { touched = true; this.ctx.fillStyle = "rgba(255,255,0,0.9)"; this.ctx.beginPath(); this.ctx.arc(h.x * this.canvas.width, h.y * this.canvas.height, 15, 0, 2 * Math.PI); this.ctx.fill(); break; } } if (touched) { this.isTouchOk = false; this.touchErrors.push(t(errString)); this.ctx.fillStyle = "rgba(255,0,0,0.4)"; this.ctx.beginPath(); const aspect = this.canvas.width / this.canvas.height; this.ctx.ellipse(targetCenter.x * this.canvas.width, targetCenter.y * this.canvas.height, radius2D * this.canvas.height * aspect, radius2D * this.canvas.height, 0, 0, 2 * Math.PI); this.ctx.fill(); } }; const ls = this.landmarks[11]; const rs = this.landmarks[12]; const lhip = this.landmarks[23]; const rhip = this.landmarks[24]; if (ntF && this.landmarks[0] && this.landmarks[0].visibility > 0.5) { check3DTouch(this.landmarks[0], 0.08, 0.15, "Face", 'err_touch_f'); } if (ntC && ls && rs && lhip && rhip && ls.visibility > 0.5) { const chestCenter = { x: (ls.x + rs.x) / 2, y: ls.y + (lhip.y - ls.y) * 0.3, z: (ls.z + rs.z) / 2 }; check3DTouch(chestCenter, 0.15, 0.20, "Chest", 'err_touch_c'); } if (ntCr && lhip && rhip && lhip.visibility > 0.5) { const crotchCenter = { x: (lhip.x + rhip.x) / 2, y: ((lhip.y + rhip.y) / 2) + 0.05, z: (lhip.z + rhip.z) / 2 }; check3DTouch(crotchCenter, 0.10, 0.15, "Crotch", 'err_touch_cr'); } } } if (document.getElementById('rule-presence').value === 'on') { const ls = this.landmarks[11]; const rs = this.landmarks[12]; const lhip = this.landmarks[23]; const rhip = this.landmarks[24]; const cX = (ls.x + rs.x) / 2; const cY = (ls.y + rs.y) / 2; const mH = { x: (lhip.x + rhip.x) / 2, y: (lhip.y + rhip.y) / 2 }; const cD = getDistance2D({ x: cX, y: cY }, mH); if (!gameActive) { this.startZoneX = cX; this.startZoneY = cY; this.startZoneDepth = cD; this.isPresent = true; } else if (this.startZoneX !== null && this.startZoneDepth > 0) { const xT = parseFloat(document.getElementById('input-zone-x').value)/100||0; const yT = parseFloat(document.getElementById('input-zone-y').value)/100||0; const zT = parseFloat(document.getElementById('input-zone-z').value)/100||0; const dX = Math.abs(cX - this.startZoneX); const dY = Math.abs(cY - this.startZoneY); const dRatio = cD / this.startZoneDepth; let dZ = 0; if (document.getElementById('check-allow-closer').checked && dRatio > 1) { dZ = 0; } else { dZ = Math.abs(1 - dRatio); } if (dX > xT) { this.isPresent = false; this.cageErrors.push(t('err_zone_x')); } if (dY > yT) { this.isPresent = false; this.cageErrors.push(t('err_zone_y')); } if (dZ > zT) { this.isPresent = false; this.cageErrors.push(t('err_zone_z')); } } } else { this.isPresent = true; } this.angles = extractBodyAngles(this.landmarks, this.worldLandmarks); const p = document.getElementById('pose-select').value; if (p !== "none" && ALL_POSES[p]) { const r = ALL_POSES[p].rules; const bodySens = parseFloat(document.getElementById('input-body-sens').value) || 50; let aTol = lerp(90, 5, bodySens / 100); if (!gameActive) aTol += 15; for (const [lK, tA] of Object.entries(r)) { if (limbNames[lK] && this.angles[lK] !== undefined) { if (Math.abs(this.angles[lK] - tA) > aTol) { this.isPoseHeld = false; this.poseErrors.push(t('err_pose')); break; } } } } else { this.isPoseHeld = true; } const isSepHands = document.getElementById('check-sep-hands').checked; const sensL = parseFloat(document.getElementById('input-hand-sens-l').value) || 50; const sensR = isSepHands ? (parseFloat(document.getElementById('input-hand-sens-r').value) || 50) : sensL; const ruleL = document.getElementById('rule-lh').value; const ruleR = isSepHands ? document.getElementById('rule-rh').value : ruleL; const lH = checkHandState(this.leftHandLandmarks, ruleL, sensL); const rH = checkHandState(this.rightHandLandmarks, ruleR, sensR); if (lH === "err") { this.isHandHeld = false; this.handErrors.push(t('err_hand_l')); } if (rH === "err") { this.isHandHeld = false; this.handErrors.push(t('err_hand_r')); } const mSens = parseFloat(document.getElementById('input-mouth-sens').value) || 50; const eSensL = parseFloat(document.getElementById('input-eye-sens-l').value) || 65; const isSepEyes = document.getElementById('check-sep-eyes').checked; const eSensR = isSepEyes ? (parseFloat(document.getElementById('input-eye-sens-r').value) || 65) : eSensL; const reqM = document.getElementById('rule-mouth').value; const reqEL = document.getElementById('rule-eye-l').value; const reqER = isSepEyes ? document.getElementById('rule-eye-r').value : reqEL; this.faceState = extractFaceState(this.faceLandmarks, mSens, eSensL, eSensR, this.faceState); if (reqM !== "ignore") { if ((reqM === "open" && !this.faceState.mouth.open) || (reqM === "closed" && !this.faceState.mouth.closed)) { this.isFaceHeld = false; this.faceErrors.push(t('err_mouth')); } } if (reqEL !== "ignore") { if ((reqEL === "open" && !this.faceState.eyeL.open) || (reqEL === "closed" && !this.faceState.eyeL.closed)) { this.isFaceHeld = false; this.faceErrors.push(t('err_eye_l')); } } if (reqER !== "ignore") { if ((reqER === "open" && !this.faceState.eyeR.open) || (reqER === "closed" && !this.faceState.eyeR.closed)) { this.isFaceHeld = false; this.faceErrors.push(t('err_eye_r')); } } } else { this.isPresent = false; } if (isDrawing && currentDrawRect && activeDrawCanvas === this.canvas) { let r = currentDrawRect; this.ctx.strokeStyle = drawMode === 'in' ? "lime" : "red"; this.ctx.setLineDash([5, 5]); this.ctx.lineWidth = 2; this.ctx.strokeRect(r.x * this.canvas.width, r.y * this.canvas.height, r.w * this.canvas.width, r.h * this.canvas.height); this.ctx.setLineDash([]); } const allErrs = [...this.cageErrors, ...this.touchErrors, ...this.faceErrors, ...this.handErrors, ...this.poseErrors]; if (allErrs.length > 0 && gameActive) { this.ctx.fillStyle = "rgba(180,0,0,0.8)"; this.ctx.fillRect(0, 0, this.canvas.width, 30); this.ctx.fillStyle = "white"; this.ctx.font = "bold 14px Arial"; this.ctx.textAlign = "center"; this.ctx.textBaseline = "middle"; this.ctx.fillText("⚠️ " + allErrs.join(" | "), this.canvas.width / 2, 15); } this.ctx.restore(); aggregateGlobalState(); } updateOverlays() { const p = document.getElementById('pose-select').value; const mode = document.getElementById('setting-overlay').value; const hasImg = p !== "none" && ALL_POSES[p] && ALL_POSES[p].img && ALL_POSES[p].img !== ""; const hasVector = p !== "none" && ALL_POSES[p] && ALL_POSES[p].savedLandmarks; this.ovPose.style.display = "none"; this.ovVector.style.display = "none"; this.ovCage.style.display = "none"; if (visualCageImageBase64) { this.ovCage.src = visualCageImageBase64; this.ovCage.style.display = "block"; } if (captureInterval) return; if (prepInterval) { try { if (mode === 'vector' && hasVector) { this.ovVector.style.display = "block"; drawVectorSkeleton(this.vCtx, ALL_POSES[p].savedLandmarks, this.ovVector.width, this.ovVector.height); } else if (hasImg) { this.ovPose.src = ALL_POSES[p].img; this.ovPose.style.display = "block"; } } catch(e) {} return; } if (!gameActive) return; let show = false; if (mode === 'always') { show = true; } else if (mode === 'warn' && inGracePeriod) { show = true; } else if (mode === 'penalty' && showPenaltyOverlayFlag) { show = true; } else if (mode === 'both' && (inGracePeriod || showPenaltyOverlayFlag)) { show = true; } else if (mode === 'vector') { show = true; } if (show) { try { if (mode === 'vector' && hasVector) { this.ovVector.style.display = "block"; drawVectorSkeleton(this.vCtx, ALL_POSES[p].savedLandmarks, this.ovVector.width, this.ovVector.height); } else if (mode !== 'vector' && hasImg) { this.ovPose.src = ALL_POSES[p].img; this.ovPose.style.display = "block"; } } catch(e) {} } } destroy() { this.isActive = false; if (this.stream) { this.stream.getTracks().forEach(t => t.stop()); } if (this.holistic) { this.holistic.close(); } this.container.remove(); visionNodes = visionNodes.filter(n => n.id !== this.id); aggregateGlobalState(); } } // ============================================================ // --- 5. GLOBAL AGGREGATION & MULTI-CAM MANAGER --- // ============================================================ let lastAggregateTime = 0; function aggregateGlobalState() { const now = Date.now(); if (now - lastAggregateTime < 100) return; lastAggregateTime = now; if (visionNodes.length === 0) { globalIsPresent = false; globalIsZoneOk = true; globalIsTouchOk = true; globalIsPoseHeld = true; globalIsFaceHeld = true; globalIsHandHeld = true; globalActiveErrors = ["No Camera"]; updateStatusUI(); return; } globalActiveErrors = []; globalFaceErrs = []; globalHandErrs = []; globalPoseErrs = []; globalCageErrs = []; globalTouchErrs = []; if (document.getElementById('rule-presence').value === 'on') { globalIsPresent = visionNodes.some(n => n.hasPerson && n.isPresent); if(!globalIsPresent) globalActiveErrors.push(t('err_zone_x')); // Fallback text } else { globalIsPresent = visionNodes.some(n => n.hasPerson); if(!globalIsPresent) globalActiveErrors.push("Missing"); } const aN = visionNodes.filter(n => n.hasPerson); if (aN.length > 0) { globalIsZoneOk = aN.every(n => n.isZoneOk); globalIsTouchOk = aN.every(n => n.isTouchOk); globalIsPoseHeld = aN.every(n => n.isPoseHeld); globalIsFaceHeld = aN.every(n => n.isFaceHeld); globalIsHandHeld = aN.every(n => n.isHandHeld); aN.forEach(n => { globalFaceErrs.push(...n.faceErrors); globalHandErrs.push(...n.handErrors); globalPoseErrs.push(...n.poseErrors); globalCageErrs.push(...n.cageErrors); globalTouchErrs.push(...n.touchErrors); }); // Deduplicate (Verhindert mehrfache UI-Meldungen bei OBS Split) globalFaceErrs = [...new Set(globalFaceErrs)]; globalHandErrs = [...new Set(globalHandErrs)]; globalPoseErrs = [...new Set(globalPoseErrs)]; globalCageErrs = [...new Set(globalCageErrs)]; globalTouchErrs = [...new Set(globalTouchErrs)]; globalActiveErrors = [...globalFaceErrs, ...globalHandErrs, ...globalPoseErrs, ...globalCageErrs, ...globalTouchErrs]; } else { globalIsZoneOk = true; globalIsTouchOk = true; globalIsPoseHeld = true; globalIsFaceHeld = true; globalIsHandHeld = true; } updateStatusUI(); } function updateStatusUI() { const stZ = document.getElementById('status-zone'); const stP = document.getElementById('status-pose'); const stH = document.getElementById('status-hand'); const stF = document.getElementById('status-face'); const stCage = document.getElementById('status-cage'); const stTouch = document.getElementById('status-touch'); if (globalIsPresent) { stZ.innerText = gameActive ? t('st_lock') : t('st_ok'); stZ.className = "status-badge bg-ok"; } else { stZ.innerText = t('st_miss'); stZ.className = "status-badge bg-alert"; } if (globalIsPoseHeld) { stP.innerText = t('st_ok'); stP.className = "status-badge bg-ok"; } else { stP.innerText = globalPoseErrs.join(', ') || "ERR"; stP.className = "status-badge bg-alert"; } if (globalIsHandHeld) { stH.innerText = t('st_ok'); stH.className = "status-badge bg-ok"; } else { stH.innerText = globalHandErrs.join(', ') || "ERR"; stH.className = "status-badge bg-alert"; } if (globalIsFaceHeld) { stF.innerText = t('st_ok'); stF.className = "status-badge bg-ok"; } else { stF.innerText = globalFaceErrs.join(', ') || "ERR"; stF.className = "status-badge bg-alert"; } if (globalIsZoneOk) { stCage.innerText = t('st_ok'); stCage.className = "status-badge bg-ok"; } else { stCage.innerText = globalCageErrs.join(', ') || "ERR"; stCage.className = "status-badge bg-alert"; } if (globalIsTouchOk) { stTouch.innerText = t('st_ok'); stTouch.className = "status-badge bg-ok"; } else { stTouch.innerText = globalTouchErrs.join(', ') || "ERR"; stTouch.className = "status-badge bg-alert"; } } function updateAllOverlays() { visionNodes.forEach(n => { n.updateOverlays(); }); } function addCameraNode(type, source, labelText) { const splitMode = document.getElementById('camera-split').value; if (splitMode === '2x1') { visionNodes.push(new VisionNode(type, source, labelText + ' (Links)', {x:0, y:0, w:0.5, h:1})); visionNodes.push(new VisionNode(type, source, labelText + ' (Rechts)', {x:0.5, y:0, w:0.5, h:1})); } else if (splitMode === '1x2') { visionNodes.push(new VisionNode(type, source, labelText + ' (Oben)', {x:0, y:0, w:1, h:0.5})); visionNodes.push(new VisionNode(type, source, labelText + ' (Unten)', {x:0, y:0.5, w:1, h:0.5})); } else if (splitMode === '2x2') { visionNodes.push(new VisionNode(type, source, labelText + ' (OL)', {x:0, y:0, w:0.5, h:0.5})); visionNodes.push(new VisionNode(type, source, labelText + ' (OR)', {x:0.5, y:0, w:0.5, h:0.5})); visionNodes.push(new VisionNode(type, source, labelText + ' (UL)', {x:0, y:0.5, w:0.5, h:0.5})); visionNodes.push(new VisionNode(type, source, labelText + ' (UR)', {x:0.5, y:0.5, w:0.5, h:0.5})); } else { visionNodes.push(new VisionNode(type, source, labelText)); } } async function initCameraManager() { const cS = document.getElementById('camera-select'); if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { cS.innerHTML = ``; return; } try { // PRE-BOOT Sequenz (Erzwingt Berechtigungs-Popup im Browser) const tempStream = await navigator.mediaDevices.getUserMedia({ video: true }); const devs = await navigator.mediaDevices.enumerateDevices(); const vds = devs.filter(d => d.kind === 'videoinput'); tempStream.getTracks().forEach(t => t.stop()); cS.innerHTML = ''; if (vds.length === 0) { cS.innerHTML = ``; return; } vds.forEach((d, index) => { const o = document.createElement('option'); o.value = d.deviceId; o.text = d.label || ('Camera ' + (index + 1)); cS.add(o); }); const firstCamId = vds[0].deviceId; const firstCamLabel = vds[0].label || 'Camera 1'; addCameraNode('local', firstCamId, firstCamLabel); } catch(e) { console.warn("Boot Sequence Interrupted:", e); cS.innerHTML = ``; } } document.getElementById('btn-add-local').onclick = () => { const devId = document.getElementById('camera-select').value; const selectElem = document.getElementById('camera-select'); const text = selectElem.options[selectElem.selectedIndex].text; if(devId) { addCameraNode('local', devId, text); } }; document.getElementById('btn-add-ip').onclick = () => { const url = document.getElementById('ip-cam-url').value.trim(); if(url) { addCameraNode('ip', url, "IP: " + url.substring(0,15)); } }; // ============================================================ // --- 6. GAME LOOP & UI UPDATE --- // ============================================================ function updateTimerUI() { if (prepInterval || captureInterval) return; if (!uiTimer) return; if (gameActive && inGracePeriod) { uiTimer.innerText = `WARN: ${Math.max(0, graceTimeRemaining)}`; if (!uiTimer.classList.contains('hidden-timer')) { uiTimer.className = "timer-box violation"; } return; } const dT = Math.max(0, timeRemaining); const mm = Math.floor(dT / 60).toString().padStart(2, '0'); const ss = (dT % 60).toString().padStart(2, '0'); uiTimer.innerText = `${mm}:${ss}`; const isAllOk = globalIsPoseHeld && globalIsFaceHeld && globalIsHandHeld && globalIsPresent && globalIsZoneOk && globalIsTouchOk; if (!uiTimer.classList.contains("penalty-hit")) { if (isTimerHidden) { uiTimer.className = "timer-box hidden-timer"; } else { uiTimer.className = "timer-box " + (isAllOk ? "active" : "violation"); } } } function startGame(isRemote = false) { if (gameActive || prepInterval || captureInterval) return; if (visionNodes.length === 0) { alert(t('cam_none')); return; } if (!uiTimer) { console.error("uiTimer global is missing!"); return; } unlockAudio(); timeRemaining = parseTimeStr(document.getElementById('input-time').value) || 60; timeElapsed = 0; inGracePeriod = false; penaltyHistory = []; showPenaltyOverlayFlag = false; globalActiveErrors = []; let prepTime = parseTimeStr(document.getElementById('input-prep-time').value) || 10; if (isRemote && prepTime > 10) prepTime = 5; const p = document.getElementById('pose-select').value; if (p !== "none" && ALL_POSES[p]) { document.getElementById('prep-title').innerText = ALL_POSES[p].desc; } else { document.getElementById('prep-title').innerText = t("p_free"); } document.getElementById('prep-countdown').innerText = prepTime; document.getElementById('prep-overlay').style.display = "flex"; uiTimer.innerText = `READY: ${prepTime}`; if (!isTimerHidden) { uiTimer.className = "timer-box prep-time"; } if (isRemote) { sendTelegram(`⚡ Start in ${prepTime}s...`); } Sounds.tick(); visionNodes.forEach(n => { n.startZoneX = null; n.startZoneDepth = null; }); prepInterval = setInterval(() => { try { updateAllOverlays(); prepTime--; if (prepTime > 0) { Sounds.tick(); uiTimer.innerText = `READY: ${prepTime}`; document.getElementById('prep-countdown').innerText = prepTime; } else { Sounds.start(); clearInterval(prepInterval); prepInterval = null; document.getElementById('prep-overlay').style.display = "none"; speakNow(t("spkStart")); startMainGameLoop(); } } catch(e) { console.error("Fehler im Prep-Intervall", e); clearInterval(prepInterval); prepInterval = null; document.getElementById('prep-overlay').style.display = "none"; startMainGameLoop(); } }, 1000); } function startMainGameLoop() { gameActive = true; if (timerInterval) clearInterval(timerInterval); timerInterval = setInterval(() => { try { if (!gameActive) return; timeElapsed++; const isAllOk = globalIsPoseHeld && globalIsFaceHeld && globalIsHandHeld && globalIsPresent && globalIsZoneOk && globalIsTouchOk; const graceSecs = parseTimeStr(document.getElementById('input-grace').value) || 0; const basePenalty = parseTimeStr(document.getElementById('input-penalty').value) || 5; const escWindow = parseTimeStr(document.getElementById('input-esc-window').value) || 0; const escAdd = parseTimeStr(document.getElementById('input-esc-add').value) || 0; const alertDuration = (parseTimeStr(document.getElementById('input-alert-time').value) || 3) * 1000; const warnText = document.getElementById('msg-warn').value || t("spkErr"); const penText = document.getElementById('msg-pen').value || t("spkPen"); if (!isAllOk) { let reason = globalActiveErrors.length > 0 ? globalActiveErrors.join(", ") : "Fehler"; if (!inGracePeriod) { inGracePeriod = true; graceTimeRemaining = graceSecs; if (graceTimeRemaining > 0) { Sounds.warning(); speakNow(warnText); sendTelegram(`⚠️ *${sanitizeTG(warnText)}* – ${sanitizeTG(reason)} (${graceTimeRemaining}s)`); } } if (inGracePeriod) { if (graceTimeRemaining <= 0) { const now = Date.now(); if (escWindow > 0) { penaltyHistory = penaltyHistory.filter(time => (now - time) <= (escWindow * 1000)); } else { penaltyHistory = []; } let currentStrikes = penaltyHistory.length; let extraTime = currentStrikes * escAdd; let totalPenalty = basePenalty + extraTime; timeRemaining += totalPenalty; penaltyHistory.push(now); if (!isTimerHidden) { uiTimer.className = "timer-box penalty-hit"; } Sounds.penalty(); speakNow(penText + " " + totalPenalty + " " + t("spkSec")); showPenaltyOverlayFlag = true; if (penaltyOverlayTimeout) { clearTimeout(penaltyOverlayTimeout); } penaltyOverlayTimeout = setTimeout(() => { showPenaltyOverlayFlag = false; updateAllOverlays(); }, alertDuration); setTimeout(() => { if (gameActive && uiTimer.className.includes("penalty-hit")) { updateTimerUI(); } }, 500); let escMsg = extraTime > 0 ? ` (+${extraTime}s Esc)` : ""; sendTelegram(`🚨 *${sanitizeTG(penText)}* +${totalPenalty}s${escMsg}\nCause: ${sanitizeTG(reason)} | Left: ${timeRemaining}s`); if (document.getElementById('check-auto-photo').checked) { sendSpyPhoto(`🚨 Beweisfoto: ${reason}`); } inGracePeriod = false; } else { graceTimeRemaining--; } } } else if (timeRemaining > 0) { if (inGracePeriod) { inGracePeriod = false; speakNow(t("spkCorr")); sendTelegram("✅ Korrigiert!"); } timeRemaining--; } else { gameActive = false; clearInterval(timerInterval); toggleUI('timer', true); // Force Show uiTimer.innerText = "SUCCESS!"; uiTimer.className = "timer-box active"; Sounds.success(); speakNow(t("spkOver")); sendTelegram(`🎉 *${t_tg('spkOver')}*`); updateAllOverlays(); return; } updateAllOverlays(); updateTimerUI(); } catch (e) { console.error("Fehler in der Main Game Loop", e); } }, 1000); } function stopGame(silent = false) { gameActive = false; inGracePeriod = false; penaltyHistory = []; showPenaltyOverlayFlag = false; globalActiveErrors = []; if (timerInterval) clearInterval(timerInterval); if (prepInterval) { clearInterval(prepInterval); prepInterval = null; } if (captureInterval) { clearInterval(captureInterval); captureInterval = null; document.getElementById('btn-capture').disabled = false; } if (penaltyOverlayTimeout) { clearTimeout(penaltyOverlayTimeout); penaltyOverlayTimeout = null; } document.getElementById('prep-overlay').style.display = "none"; timeRemaining = parseTimeStr(document.getElementById('input-time').value) || 60; updateAllOverlays(); updateTimerUI(); if (!silent) { sendTelegram(`🛑 ${t_tg('tg_stop')}`); } } document.getElementById('btn-start').onclick = () => { startGame(false); }; // ============================================================ // --- 7. UI EVENTS & TELEGRAM INTEGRATION --- // ============================================================ const poseSelect = document.getElementById('pose-select'); const btnDeletePose = document.getElementById('btn-delete-pose'); const previewImg = document.getElementById('preview-img'); const previewText = document.getElementById('preview-text'); const previewVectorCanvas = document.getElementById('preview-vector'); const previewVectorCtx = previewVectorCanvas.getContext('2d'); function loadCustomPosesToUI() { Object.keys(customPoses).forEach(id => { const opt = document.createElement('option'); opt.value = id; opt.text = "✏️ " + customPoses[id].desc; poseSelect.appendChild(opt); }); } loadCustomPosesToUI(); poseSelect.addEventListener('change', () => { const val = poseSelect.value; const pose = ALL_POSES[val]; const hasImg = val !== "none" && pose && pose.img && pose.img !== ""; if (val.startsWith("custom_")) { btnDeletePose.style.display = "inline-block"; } else { btnDeletePose.style.display = "none"; } if (pose) { previewImg.src = pose.img || ""; previewImg.style.display = hasImg ? "block" : "none"; previewText.innerText = pose.desc; document.getElementById('rule-mouth').value = pose.mouth || "ignore"; document.getElementById('rule-eye-l').value = pose.eyeL || "ignore"; document.getElementById('rule-eye-r').value = pose.eyeR || "ignore"; document.getElementById('rule-lh').value = pose.lh_rule || "ignore"; document.getElementById('rule-rh').value = pose.rh_rule || "ignore"; if (pose.savedLandmarks) { previewVectorCanvas.style.display = "block"; drawVectorSkeleton(previewVectorCtx, pose.savedLandmarks, previewVectorCanvas.width, previewVectorCanvas.height); } else { previewVectorCanvas.style.display = "none"; previewVectorCtx.clearRect(0, 0, previewVectorCanvas.width, previewVectorCanvas.height); } } else { previewImg.style.display = "none"; previewVectorCanvas.style.display = "none"; previewText.innerText = t("p_free"); } updateAllOverlays(); }); document.getElementById('btn-hide-local').addEventListener('click', () => { toggleUI('timer'); }); btnDeletePose.addEventListener('click', () => { const val = poseSelect.value; if (val.startsWith("custom_") && confirm(t('err_del'))) { delete customPoses[val]; delete ALL_POSES[val]; try { localStorage.setItem('custom_poses_v115_full', JSON.stringify(customPoses)); } catch (e) {} const optToRemove = poseSelect.querySelector(`option[value="${val}"]`); if (optToRemove) { optToRemove.remove(); } poseSelect.value = "none"; poseSelect.dispatchEvent(new Event('change')); } }); document.getElementById('btn-capture').addEventListener('click', () => { if (gameActive || prepInterval || captureInterval) { alert(t('err_stop')); return; } if (visionNodes.length === 0) { alert(t('cam_none')); return; } unlockAudio(); const btn = document.getElementById('btn-capture'); btn.disabled = true; let captureTime = 5; document.getElementById('prep-title').innerText = "Scan..."; document.getElementById('prep-countdown').innerText = captureTime; document.getElementById('prep-overlay').style.display = "flex"; uiTimer.innerText = `SNAP: ${captureTime}`; uiTimer.className = "timer-box prep-time"; Sounds.tick(); captureInterval = setInterval(() => { try { captureTime--; if (captureTime > 0) { Sounds.tick(); uiTimer.innerText = `SNAP: ${captureTime}`; document.getElementById('prep-countdown').innerText = captureTime; } else { Sounds.start(); clearInterval(captureInterval); captureInterval = null; btn.disabled = false; document.getElementById('prep-overlay').style.display = "none"; const primary = visionNodes.find(n => n.hasPerson) || visionNodes[0]; if (!primary || !primary.angles || Object.keys(primary.angles).length === 0) { uiTimer.innerText = "ERROR"; uiTimer.className = "timer-box violation"; alert(t('err_person')); setTimeout(updateTimerUI, 2000); return; } const tempCanvas = document.createElement('canvas'); tempCanvas.width = 320; tempCanvas.height = 240; tempCanvas.getContext('2d').drawImage(primary.canvas, 0, 0, 320, 240); const thumbnailBase64 = tempCanvas.toDataURL('image/jpeg', 0.6); uiTimer.innerText = "SAVED"; uiTimer.className = "timer-box active"; setTimeout(() => { const name = prompt(t('prompt_pose')); if (name && name.trim() !== "") { saveCustomPose( name, thumbnailBase64, primary.angles, primary.faceState.mouth.open ? 'open' : 'closed', primary.faceState.eyeL.open ? 'open' : 'closed', primary.faceState.eyeR.open ? 'open' : 'closed', primary.landmarks ); } updateTimerUI(); }, 100); } } catch(e) { console.error("Capture Fehler", e); clearInterval(captureInterval); captureInterval = null; btn.disabled = false; document.getElementById('prep-overlay').style.display = "none"; updateTimerUI(); } }, 1000); }); document.getElementById('btn-upload').addEventListener('click', () => { if (gameActive || prepInterval || captureInterval) return; document.getElementById('image-upload').click(); }); document.getElementById('image-upload').addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { if(visionNodes.length === 0) { alert(t('cam_none')); return; } const img = new Image(); img.crossOrigin = "anonymous"; img.onload = async () => { let thumb = ""; try { const tc = document.createElement('canvas'); tc.width = 320; tc.height = Math.round(320 / (img.width/img.height||1)); tc.getContext('2d').drawImage(img, 0, 0, tc.width, tc.height); thumb = tc.toDataURL('image/jpeg', 0.6); } catch(err) { console.warn("Upload Thumbnail Error", err); } pendingImagePose = { name: null, imgData: thumb, source: 'local' }; await visionNodes[0].holistic.send({image: img}); }; img.onerror = () => { alert(t('err_net')); }; img.src = event.target.result; }; reader.readAsDataURL(file); }); function saveCustomPose(name, imgData, rulesObj, forcedMouth = null, forcedEyeL = null, forcedEyeR = null, landmarks = null) { const newId = "custom_" + Date.now(); const reqEL = forcedEyeL || document.getElementById('rule-eye-l').value; let reqER = reqEL; if (document.getElementById('check-sep-eyes').checked) { reqER = forcedEyeR || document.getElementById('rule-eye-r').value; } const newPose = { desc: name, img: imgData, rules: JSON.parse(JSON.stringify(rulesObj)), mouth: forcedMouth || document.getElementById('rule-mouth').value, eyeL: reqEL, eyeR: reqER, lh_rule: document.getElementById('rule-lh').value, rh_rule: document.getElementById('rule-rh').value, savedLandmarks: landmarks ? JSON.parse(JSON.stringify(landmarks)) : null }; customPoses[newId] = newPose; ALL_POSES[newId] = newPose; try { localStorage.setItem('custom_poses_v115_full', JSON.stringify(customPoses)); } catch (e) { alert("⚠️ " + t('err_quota')); } const opt = document.createElement('option'); opt.value = newId; opt.text = "✏️ " + name; poseSelect.appendChild(opt); poseSelect.value = newId; poseSelect.dispatchEvent(new Event('change')); } // ============================================================ // --- TELEGRAM LOGIC --- // ============================================================ function sanitizeTG(str) { if (!str) return ""; return str.replace(/[_*[\]()~`>#+\-=|{}.!]/g, ' '); } async function sendTelegram(msg, customMarkup = null) { const token = document.getElementById('tg-token').value.trim(); const chat = document.getElementById('tg-chat').value.trim(); if (!token || !chat) return; try { let url = `https://api.telegram.org/bot${token}/sendMessage?chat_id=${chat}&text=${encodeURIComponent(msg)}&parse_mode=Markdown`; if (customMarkup) { url += `&reply_markup=${encodeURIComponent(JSON.stringify(customMarkup))}`; } await fetch(url); } catch (e) {} } async function sendSpyPhoto(caption) { if(visionNodes.length === 0) { sendTelegram(`❌ ${t_tg('cam_none')}`); return; } const token = document.getElementById('tg-token').value.trim(); const chat = document.getElementById('tg-chat').value.trim(); if (!token || !chat) return; const c = document.createElement('canvas'); c.width = 640 * visionNodes.length; c.height = 480; const ctx = c.getContext('2d'); visionNodes.forEach((node, i) => { ctx.drawImage(node.canvas, i * 640, 0, 640, 480); }); return new Promise((resolve) => { c.toBlob(async (blob) => { if (!blob) { resolve(false); return; } const formData = new FormData(); formData.append('chat_id', chat); formData.append('photo', blob, 'snapshot.jpg'); if (caption) { formData.append('caption', caption); } try { const res = await fetch(`https://api.telegram.org/bot${token}/sendPhoto`, { method: 'POST', body: formData }); const data = await res.json(); resolve(data.ok); } catch (e) { resolve(false); } }, 'image/jpeg', 0.75); }); } function remoteCapture(countdown, autoStart) { if (gameActive) { stopGame(true); } if (prepInterval) { clearInterval(prepInterval); prepInterval = null; } if (captureInterval) { sendTelegram(`❌ ${t_tg('tg_busy')}`); return; } if (visionNodes.length === 0) { sendTelegram(`❌ ${t_tg('cam_none')}`); return; } unlockAudio(); let captureTime = countdown; document.getElementById('prep-title').innerText = "Bot Freeze..."; document.getElementById('prep-countdown').innerText = captureTime; document.getElementById('prep-overlay').style.display = "flex"; uiTimer.innerText = `SNAP: ${captureTime}`; uiTimer.className = "timer-box prep-time"; const doCapture = () => { if (captureInterval) { clearInterval(captureInterval); captureInterval = null; } document.getElementById('prep-overlay').style.display = "none"; Sounds.start(); const primary = visionNodes.find(n => n.hasPerson) || visionNodes[0]; if (!primary || !primary.angles || Object.keys(primary.angles).length === 0) { uiTimer.innerText = "ERROR"; uiTimer.className = "timer-box violation"; sendTelegram(`❌ ${t_tg('err_person')}`); setTimeout(updateTimerUI, 2000); return; } const tempCanvas = document.createElement('canvas'); tempCanvas.width = 320; tempCanvas.height = 240; tempCanvas.getContext('2d').drawImage(primary.canvas, 0, 0, 320, 240); const thumbnailBase64 = tempCanvas.toDataURL('image/jpeg', 0.6); const d = new Date(); const timeStr = d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0') + ':' + d.getSeconds().toString().padStart(2, '0'); const name = "Bot Freeze " + timeStr; saveCustomPose( name, thumbnailBase64, primary.angles, primary.faceState.mouth.open ? 'open' : 'closed', primary.faceState.eyeL.open ? 'open' : 'closed', primary.faceState.eyeR.open ? 'open' : 'closed', primary.landmarks ); uiTimer.innerText = "SAVED"; uiTimer.className = "timer-box active"; sendTelegram(`✅ ${t_tg('tg_saved')} /pose [Nr]`); updateTimerUI(); if (autoStart) { setTimeout(() => { startGame(true); }, 500); } }; if (captureTime <= 0) { doCapture(); } else { Sounds.tick(); captureInterval = setInterval(() => { try { captureTime--; if (captureTime > 0) { Sounds.tick(); uiTimer.innerText = `SNAP: ${captureTime}`; document.getElementById('prep-countdown').innerText = captureTime; } else { doCapture(); } } catch(e) { clearInterval(captureInterval); captureInterval = null; } }, 1000); } } async function processTelegramPhotoUpload(fileId, commandName, poseName = "") { const token = document.getElementById('tg-token').value.trim(); sendTelegram(`📸 Load...`); let objectUrl = null; try { const res = await fetch(`https://api.telegram.org/bot${token}/getFile?file_id=${fileId}`); const data = await res.json(); if (!data.ok) throw new Error("File fetch failed"); const fileUrl = `https://api.telegram.org/file/bot${token}/${data.result.file_path}`; const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(fileUrl)}`; const tryFetch = async (url) => { const r = await fetch(url); if (!r.ok) throw new Error("Fetch failed"); return r.blob(); }; let blob; try { blob = await tryFetch(proxyUrl); } catch (e) { try { blob = await tryFetch(`https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(fileUrl)}`); } catch (e2) { sendTelegram(`❌ ${t_tg('err_net')}`); return; } } objectUrl = URL.createObjectURL(blob); if (commandName === '/addpose') { if(visionNodes.length === 0) { sendTelegram(`❌ ${t_tg('cam_none')}`); return; } const img = new Image(); img.crossOrigin = "anonymous"; img.onload = async () => { let thumb = ""; try { const tc = document.createElement('canvas'); tc.width = 320; tc.height = Math.round(320 / (img.width/img.height||1)); tc.getContext('2d').drawImage(img, 0, 0, tc.width, tc.height); thumb = tc.toDataURL('image/jpeg', 0.6); } catch(e){} pendingImagePose = { name: poseName || "Chat Pose", imgData: thumb, source: 'telegram' }; await visionNodes[0].holistic.send({image: img}); }; img.onerror = () => { sendTelegram(`❌ ${t_tg('err_net')}`); }; img.src = objectUrl; } else if (commandName === '/setcagephoto') { const reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = () => { visualCageImageBase64 = reader.result; visionNodes.forEach(n => { n.ovCage.src = visualCageImageBase64; n.ovCage.style.display = "block"; }); sendTelegram(`✅ ${t_tg('tg_saved')}`); }; } } catch (e) { sendTelegram(`❌ ${t_tg('err_tg')}`); } finally { if (objectUrl) { setTimeout(() => URL.revokeObjectURL(objectUrl), 5000); } } } const getTgReplyKeyboard = () => ({ keyboard: [ [{ text: "/start" }, { text: "/stop" }, { text: "/status" }], [{ text: "/capture 5" }, { text: "/spy" }, { text: "/autophoto" }], [{ text: "/hide" }, { text: "/show" }, { text: "/showall" }], [{ text: "/menu" }, { text: "/helpme" }, { text: "/ping" }] ], resize_keyboard: true, is_persistent: true }); document.getElementById('btn-tg-connect').onclick = async () => { unlockAudio(); const btn = document.getElementById('btn-tg-connect'); const token = document.getElementById('tg-token').value.trim(); if (!token) { alert("Token fehlt!"); return; } btn.innerText = t('btn_conn'); btn.disabled = true; try { const res = await fetch(`https://api.telegram.org/bot${token}/getMe`); const data = await res.json(); if (data.ok) { if (pollingInterval) { clearInterval(pollingInterval); } pollingInterval = setInterval(pollTelegram, 2500); btn.innerText = t('btn_act'); btn.style.background = "#4caf50"; sendTelegram(`🟢 *System Online!* (${data.result.first_name})`, getTgReplyKeyboard()); } else { btn.innerText = t('btn_err'); btn.style.background = "#f44336"; alert("Telegram Error: " + data.description); } } catch (e) { btn.innerText = t('btn_net'); btn.style.background = "#f44336"; alert(t('err_tg')); } finally { btn.disabled = false; } }; async function pollTelegram() { if (isPolling) return; const token = document.getElementById('tg-token').value.trim(); if (!token) return; isPolling = true; try { const r = await fetch(`https://api.telegram.org/bot${token}/getUpdates?offset=${lastUpdateId + 1}&timeout=1`); const d = await r.json(); if (d.ok && d.result.length > 0) { for (let m of d.result) { lastUpdateId = m.update_id; let chat_id = null; let rawText = ""; let isCallback = false; let callbackId = null; let userLangCode = "en"; if (m.message && m.message.chat) { chat_id = m.message.chat.id.toString(); if (m.message.text) { rawText = m.message.text.trim(); } if(m.message.from && m.message.from.language_code) { userLangCode = m.message.from.language_code; } } else if (m.callback_query) { chat_id = m.callback_query.message.chat.id.toString(); rawText = m.callback_query.data.trim(); isCallback = true; callbackId = m.callback_query.id; if(m.callback_query.from && m.callback_query.from.language_code) { userLangCode = m.callback_query.from.language_code; } } if (chat_id && document.getElementById('tg-chat').value !== chat_id) { document.getElementById('tg-chat').value = chat_id; } let shortLang = userLangCode.split('-')[0].toLowerCase(); tgLang = translations[shortLang] ? shortLang : "en"; if (rawText !== "") { let parts = rawText.split(/\s+/); let cmd = parts[0].split('@')[0].toLowerCase(); let arg1 = parts.length > 1 ? parts[1].toLowerCase() : ""; let arg2 = parts.length > 2 ? parts[2].toLowerCase() : ""; let fullArgs = parts.slice(1).join(" "); if (cmd === '/ping') { const audioState = isAudioOn() ? "ON" : "BLOCKIERT"; sendTelegram(`🏓 *Pong!*\nSystem läuft einwandfrei.\nKameras: ${visionNodes.length}\nAudio: ${audioState}`); } else if (cmd === '/helpme') { const h1 = `🛠 *HANDBUCH (1/3)*\n\n🎮 *GRUNDLAGEN*\n\`/menu\` - Inline Menü\n\`/start\` - Überwachung an\n\`/stop\` - Überwachung aus\n\`/status\` - Alle Parameter zeigen\n\n📸 *KONTROLLE*\n\`/spy\` - Heimliches Foto (Multi-Cam)\n\`/capture [s]\` - Pose einscannen\n\`/capturestart [s]\` - Scannen und starten\n\`/autophoto\` - Auto-Beweisfoto AN/AUS\n\n🤸 *POSEN*\n\`/listpose\` - Alle Posen zeigen\n\`/pose [Nr]\` - Pose aktivieren\n_Eigenes Bild:_ Sende Bild mit Text \`/addpose [Name]\``; const h2 = `🛠 *HANDBUCH (2/3)*\n\n⏱ *ZEITEN*\n\`/settime [s]\`, \`/setgrace [s]\`, \`/setpenalty [s]\`\n\`/setescwindow [s]\`, \`/setescadd [s]\`\n\n🛡️ *WARDEN (NO-TOUCH)*\n\`/notouch [face/chest/crotch/all] [on/off]\`\n\`/setcage [in/out] [x y w h]\`\n\`/clearcages\`\n_Foto-Käfig:_ Bild mit Text \`/setcagephoto\`\n\n💬 *TEXTE*\n\`/say [Text]\`, \`/msgwarn [Text]\`, \`/msgpen [Text]\`, \`/setaudio [on/off]\``; const h3 = `🛠 *HANDBUCH (3/3)*\n\n🎭 *REGELN*\n\`/setpresence [on/off]\`\n\`/setmouth [ignore/open/closed]\`\n\`/setmouthsens [0-100]\`\n\`/seteyel [ignore/open/closed]\`\n\`/seteyer [ignore/open/closed]\`\n\`/seteyesensl [0-100]\`, \`/seteyesensr [0-100]\`\n\`/setlh [ignore/fist/spread/together]\`\n\`/setrh [ignore/fist/spread/together]\`\n\`/sethandsensl [0-100]\`, \`/sethandsensr [0-100]\`\n\`/setbodysens [0-100]\`\n\n👁️ *UI*\n\`/setoverlay [start/always/warn/penalty/vector]\`\n\`/hide\` / \`/show\`, \`/hidesettings\`, \`/hideall\``; await sendTelegram(h1); setTimeout(() => sendTelegram(h2), 400); setTimeout(() => sendTelegram(h3), 800); } else if (cmd === '/menu') { const kb = { inline_keyboard: [ [{ text: "🟢 " + t_tg('tg_start'), callback_data: "/start" }, { text: "🔴 " + t_tg('tg_stop'), callback_data: "/stop" }, { text: "📊 " + t_tg('tg_status'), callback_data: "/status" }], [{ text: "📸 " + t_tg('tg_spy'), callback_data: "/spy" }, { text: "📷 " + t_tg('tg_photo'), callback_data: "/autophoto" }, { text: "📸 Capture 5s", callback_data: "/capture 5" }], [{ text: "⏱️ Zeit 60s", callback_data: "/settime 60" }, { text: "⏱️ Zeit 300s", callback_data: "/settime 300" }, { text: "⚖️ Strafe 10s", callback_data: "/setpenalty 10" }], [{ text: "👄 Mund Offen", callback_data: "/setmouth open" }, { text: "👄 Mund Zu", callback_data: "/setmouth closed" }, { text: "👄 Egal", callback_data: "/setmouth ignore" }], [{ text: "👁️ L Offen", callback_data: "/seteyel open" }, { text: "👁️ R Zu", callback_data: "/seteyer closed" }, { text: "👁️ Egal", callback_data: "/seteyel ignore\n/seteyer ignore" }], [{ text: "🖐 L Faust", callback_data: "/setlh fist" }, { text: "🖐 R Faust", callback_data: "/setrh fist" }, { text: "🖐 Egal", callback_data: "/setlh ignore\n/setrh ignore" }], [{ text: "🚫 Touch Face", callback_data: "/notouch face on" }, { text: "🚫 Touch Chest", callback_data: "/notouch chest on" }, { text: "🚫 Touch Crotch", callback_data: "/notouch crotch on" }], [{ text: "🚫 Touch OFF", callback_data: "/notouch all off" }, { text: "🙈 " + t_tg('tg_blind'), callback_data: "/hideall" }, { text: "☀️ " + t_tg('tg_ui'), callback_data: "/showall" }] ] }; sendTelegram(`🎮 *${t_tg('tg_menu')}*`, kb); } else if (cmd === '/status' || cmd === '/settings') { const rawName = ALL_POSES[document.getElementById('pose-select').value] ? ALL_POSES[document.getElementById('pose-select').value].desc : "Freies Spiel"; const pName = sanitizeTG(rawName); const msg = `📊 *${t_tg('tg_status')}*\n\n🎮 ${gameActive ? '🟢 LÄUFT' : '🔴 GESTOPPT'} | ⏱ ${timeElapsed}s / ${timeRemaining}s übrig\n🤸 Pose: ${pName} (${document.getElementById('input-body-sens').value}%)\n🎥 Kameras: ${visionNodes.length}\n\n⚙️ *Einstellungen:*\n• Zielzeit: ${document.getElementById('input-time').value} | Grace: ${document.getElementById('input-grace').value} | Strafe: ${document.getElementById('input-penalty').value}\n• Esc-Win: ${document.getElementById('input-esc-window').value} | Esc-Add: ${document.getElementById('input-esc-add').value}\n• Start-CD: ${document.getElementById('input-prep-time').value}s | Audio: ${isAudioOn() ? "ON" : "OFF"}\n\n🛡️ *Warden:*\n• Käfige: ${drawableZones.length} | Foto-Käfig: ${visualCageImageBase64 ? 'Aktiv' : 'Leer'}\n• No-Touch: Face(${document.getElementById('nt-face').checked ? "ON" : "OFF"}) Chest(${document.getElementById('nt-chest').checked ? "ON" : "OFF"}) Crotch(${document.getElementById('nt-crotch').checked ? "ON" : "OFF"})\n\n📍 3D-Zone: ${document.getElementById('rule-presence').value}\n🎭 Mund: ${document.getElementById('rule-mouth').value} (${document.getElementById('input-mouth-sens').value}%)\n👁️ Auge L: ${document.getElementById('rule-eye-l').value} (${document.getElementById('input-eye-sens-l').value}%) | Auge R: ${document.getElementById('rule-eye-r').value} (${document.getElementById('input-eye-sens-r').value}%)\n🖐 Hand L: ${document.getElementById('rule-lh').value} (${document.getElementById('input-hand-sens-l').value}%) | Hand R: ${document.getElementById('rule-rh').value} (${document.getElementById('input-hand-sens-r').value}%)`; sendTelegram(msg); } else if (cmd === '/start') { if (gameActive || prepInterval) { stopGame(true); setTimeout(() => startGame(true), 200); } else { startGame(true); } } else if (cmd === '/stop') { stopGame(false); } else if (cmd === '/spy') { sendTelegram(`📸 ${t_tg('tg_spy')}...`); sendSpyPhoto(`🕵️ ${t_tg('tg_spy')}`).then(ok => { if(!ok) { sendTelegram(`❌ ${t_tg('tg_fail')}`); } }); } else if (cmd === '/autophoto') { const box = document.getElementById('check-auto-photo'); box.checked = !box.checked; sendTelegram(`📸 Auto-Foto: *${box.checked ? 'AKTIV ✅' : 'AUS ❌'}*`); } else if (cmd === '/say') { const txt = rawText.substring(rawText.indexOf(' ') + 1); if (txt && txt !== '/say') { speakNow(txt, document.getElementById('tts-mode').value === 'en' ? 'en' : (document.getElementById('tts-mode').value === 'orig' ? null : curLang)); sendTelegram(`🗣️: "${sanitizeTG(txt)}"`); } } else if (cmd === '/capture') { let sec = parseTimeStr(fullArgs) || 5; remoteCapture(sec, false); sendTelegram(`📸 Freeze in ${sec}s...`); } else if (cmd === '/capturestart') { let sec = parseTimeStr(fullArgs) || 5; remoteCapture(sec, true); sendTelegram(`📸⚡ Shock-Freeze in ${sec}s...`); } else if (cmd === '/listpose' || cmd === '/listposes') { let msg = "📜 *POSEN*\n\n*0.* Freies Spiel\n"; let counter = 1; for (let key in ALL_POSES) { msg += `*${counter}.* ${sanitizeTG(ALL_POSES[key].desc)}\n`; counter++; } msg += `\n➡️ Aktivieren: \`/pose [Nummer]\``; sendTelegram(msg); } else if (cmd === '/pose') { let targetId = arg1; let poseIndex = parseInt(arg1); if (!isNaN(poseIndex)) { if (poseIndex === 0) { targetId = 'none'; } else if (poseIndex > 0 && poseIndex <= Object.keys(ALL_POSES).length) { targetId = Object.keys(ALL_POSES)[poseIndex - 1]; } } if (ALL_POSES[targetId] || targetId === 'none') { poseSelect.value = targetId; poseSelect.dispatchEvent(new Event('change')); const pName = targetId === 'none' ? 'Freies Spiel' : sanitizeTG(ALL_POSES[targetId].desc); sendTelegram(`🤸 Pose: ${pName}`); } } else if (cmd === '/settime') { let v = parseTimeStr(fullArgs); if (v > 0) { timeRemaining = v; document.getElementById('input-time').value = v; sendTelegram(`⏱️ Ziel: ${v}s`); if (gameActive) updateTimerUI(); } } else if (cmd === '/setpenalty') { let v = parseTimeStr(fullArgs); if (v >= 0) { document.getElementById('input-penalty').value = v; sendTelegram(`⚖️ Strafe: ${v}s`); } } else if (cmd === '/setgrace') { let v = parseTimeStr(fullArgs); if (v >= 0) { document.getElementById('input-grace').value = v; sendTelegram(`⚠️ Warn-Zeit: ${v}s`); } } else if (cmd === '/setescwindow') { let v = parseTimeStr(fullArgs); if (v >= 0) { document.getElementById('input-esc-window').value = v; sendTelegram(`⏰ Esc-Fenster: ${v}s`); } } else if (cmd === '/setescadd') { let v = parseTimeStr(fullArgs); if (v >= 0) { document.getElementById('input-esc-add').value = v; sendTelegram(`📈 Esc-Zusatz: ${v}s`); } } else if (cmd === '/setpreptime') { let v = parseTimeStr(fullArgs); if (v >= 0) { document.getElementById('input-prep-time').value = v; sendTelegram(`🕐 Start-CD: ${v}s`); } } else if (cmd === '/msgwarn') { document.getElementById('msg-warn').value = fullArgs; sendTelegram(`💬 Warn-Text: "${sanitizeTG(fullArgs)}"`); } else if (cmd === '/msgpen') { document.getElementById('msg-pen').value = fullArgs; sendTelegram(`💬 Straf-Text: "${sanitizeTG(fullArgs)}"`); } else if (cmd === '/hide') { toggleUI('timer', false); sendTelegram("Timer versteckt."); } else if (cmd === '/show') { toggleUI('timer', true); sendTelegram("Timer sichtbar."); } else if (cmd === '/hidesettings') { toggleUI('settings', false); sendTelegram("🥷 Settings ausgeblendet."); } else if (cmd === '/showsettings') { toggleUI('settings', true); sendTelegram("👁️ Settings eingeblendet."); } else if (cmd === '/hideall') { toggleUI('all', false); sendTelegram("🌑 BLINDFLUG AKTIV!"); } else if (cmd === '/showall') { toggleUI('all', true); sendTelegram("☀️ Alles wieder sichtbar."); } else if (cmd === '/setaudio') { const btn = document.getElementById('btn-voice'); const currently = !btn.classList.contains('off'); if (arg1 === 'on' && !currently) { btn.click(); } else if (arg1 === 'off' && currently) { btn.click(); } else if (arg1 === 'toggle' || arg1 === '') { btn.click(); } sendTelegram(`🔊 Audio: *${isAudioOn() ? 'AN' : 'AUS'}*`); } else if (cmd === '/notouch') { if (arg1 === 'all' && ['on', 'off'].includes(arg2)) { document.getElementById('nt-face').checked = (arg2 === 'on'); document.getElementById('nt-chest').checked = (arg2 === 'on'); document.getElementById('nt-crotch').checked = (arg2 === 'on'); sendTelegram(`🚫 No-Touch ALLE Zonen: ${arg2.toUpperCase()}`); } else if (['face', 'chest', 'crotch'].includes(arg1) && ['on', 'off'].includes(arg2)) { document.getElementById(`nt-${arg1}`).checked = (arg2 === 'on'); sendTelegram(`🚫 No-Touch ${arg1.toUpperCase()}: ${arg2.toUpperCase()}`); } } else if (cmd === '/clearcages') { drawableZones = []; sendTelegram("🗑️ Maus-Käfige gelöscht."); } else if (cmd === '/clearcagephoto') { visualCageImageBase64 = null; visionNodes.forEach(n => { n.ovCage.style.display = "none"; n.ovCage.src = ""; }); sendTelegram("🗑️ Foto-Käfig gelöscht."); } else if (cmd === '/setcage') { if (parts.length === 6 && (arg1 === 'in' || arg1 === 'out')) { let x = parseInt(parts[2]) / 100; let y = parseInt(parts[3]) / 100; let w = parseInt(parts[4]) / 100; let h = parseInt(parts[5]) / 100; if (!isNaN(x) && !isNaN(y) && !isNaN(w) && !isNaN(h)) { drawableZones.push({ type: arg1, rect: { x, y, w, h } }); sendTelegram(`✅ Käfig (${arg1.toUpperCase()}) gesetzt.`); } } } else if (cmd === '/setoverlay') { if (['start', 'always', 'warn', 'penalty', 'both', 'vector'].includes(arg1)) { document.getElementById('setting-overlay').value = arg1; sendTelegram(`👻 Overlay: ${arg1}`); updateAllOverlays(); } } else if (cmd === '/setpresence') { if (['on', 'off'].includes(arg1)) { document.getElementById('rule-presence').value = arg1; sendTelegram(`📍 3D-Zone: ${arg1}`); } } else if (cmd === '/setmouth') { if (['ignore', 'open', 'closed'].includes(arg1)) { document.getElementById('rule-mouth').value = arg1; sendTelegram(`👄 Mund: ${arg1}`); } } else if (cmd === '/setmouthsens') { let v = parseInt(arg1); if (!isNaN(v) && v >= 0 && v <= 100) { document.getElementById('input-mouth-sens').value = v; sendTelegram(`👄 Mund-Sens: ${v}%`); } } else if (cmd === '/setbodysens') { let v = parseInt(arg1); if (!isNaN(v) && v >= 0 && v <= 100) { document.getElementById('input-body-sens').value = v; sendTelegram(`🤸 Körper-Sens: ${v}%`); } } else if (cmd === '/seteyel') { if (['ignore', 'open', 'closed'].includes(arg1)) { document.getElementById('rule-eye-l').value = arg1; sendTelegram(`👁️ Auge L: ${arg1}`); } } else if (cmd === '/seteyer') { if (['ignore', 'open', 'closed'].includes(arg1)) { document.getElementById('rule-eye-r').value = arg1; document.getElementById('check-sep-eyes').checked = true; document.getElementById('check-sep-eyes').dispatchEvent(new Event('change')); sendTelegram(`👁️ Auge R: ${arg1}`); } } else if (cmd === '/seteyesensl') { let v = parseInt(arg1); if (!isNaN(v) && v > 0 && v <= 100) { document.getElementById('input-eye-sens-l').value = v; sendTelegram(`👁️ Auge L Sens: ${v}%`); } } else if (cmd === '/seteyesensr') { let v = parseInt(arg1); if (!isNaN(v) && v > 0 && v <= 100) { document.getElementById('input-eye-sens-r').value = v; document.getElementById('check-sep-eyes').checked = true; document.getElementById('check-sep-eyes').dispatchEvent(new Event('change')); sendTelegram(`👁️ Auge R Sens: ${v}%`); } } else if (cmd === '/setlh') { if (['ignore', 'fist', 'spread', 'together'].includes(arg1)) { document.getElementById('rule-lh').value = arg1; sendTelegram(`🖐 L.Hand: ${arg1}`); } } else if (cmd === '/setrh') { if (['ignore', 'fist', 'spread', 'together'].includes(arg1)) { document.getElementById('rule-rh').value = arg1; document.getElementById('check-sep-hands').checked = true; document.getElementById('check-sep-hands').dispatchEvent(new Event('change')); sendTelegram(`🖐 R.Hand: ${arg1}`); } } else if (cmd === '/sethandsensl') { let v = parseInt(arg1); if (!isNaN(v) && v >= 0 && v <= 100) { document.getElementById('input-hand-sens-l').value = v; sendTelegram(`🖐 L.Hand Sens: ${v}%`); } } else if (cmd === '/sethandsensr') { let v = parseInt(arg1); if (!isNaN(v) && v >= 0 && v <= 100) { document.getElementById('input-hand-sens-r').value = v; document.getElementById('check-sep-hands').checked = true; document.getElementById('check-sep-hands').dispatchEvent(new Event('change')); sendTelegram(`🖐 R.Hand Sens: ${v}%`); } } else if (cmd === '/setzonex') { let v = parseInt(arg1); if (!isNaN(v)) { document.getElementById('input-zone-x').value = v; sendTelegram(`📍 Zone X: ${v}%`); } } else if (cmd === '/setzoney') { let v = parseInt(arg1); if (!isNaN(v)) { document.getElementById('input-zone-y').value = v; sendTelegram(`📍 Zone Y: ${v}%`); } } else if (cmd === '/setzonez') { let v = parseInt(arg1); if (!isNaN(v)) { document.getElementById('input-zone-z').value = v; sendTelegram(`📍 Zone Z: ${v}%`); } } else if (cmd === '/setallowcloser') { if (['on', 'off'].includes(arg1)) { document.getElementById('check-allow-closer').checked = (arg1 === 'on'); sendTelegram(`🔎 Closer: ${arg1}`); } } } if (isCallback && callbackId) { try { await fetch(`https://api.telegram.org/bot${token}/answerCallbackQuery?callback_query_id=${callbackId}`); } catch (e) {} } if (m.message && m.message.photo) { let caption = (m.message.caption || "").trim(); let captionParts = caption.split(/\s+/); let capCmd = (captionParts[0] || "").split('@')[0].toLowerCase(); if (capCmd === '/addpose' || capCmd === '/setcagephoto') { const poseName = captionParts.slice(1).join(' ').trim() || "Chat Pose"; const fileId = m.message.photo[m.message.photo.length - 1].file_id; processTelegramPhotoUpload(fileId, capCmd, poseName); } } } } } catch (e) { console.warn("Telegram Polling Error", e); } finally { isPolling = false; } }