From a17c3cb614f1889b91ab72d64b2c6ee82f1d0ba3 Mon Sep 17 00:00:00 2001 From: pporcheret Date: Wed, 28 Oct 2026 21:45:40 +0100 Subject: [PATCH] v2 - new interface, many new page for fine tuning --- .../20261028_162544_119463/manifest.json | 61 ++ .../20261028_162544_119463/original/app.py | 498 ++++++++++++++++ .../original/static/js/alfred-main-v1.js | 318 +++++++++++ .../original/templates/config.html | 75 +++ .../original/templates/index.html | 190 ++++++ .../original/templates/surveillance.html | 44 ++ .../20261028_205954_153666/manifest.json | 55 ++ .../20261028_205954_153666/original/app.py | 533 +++++++++++++++++ .../original/templates/index.html | 195 +++++++ .../20261028_214221_179952/manifest.json | 102 ++++ .../20261028_214221_179952/original/app.py | 539 ++++++++++++++++++ .../original/services/admin_console.py | 482 ++++++++++++++++ .../original/services/alfred_dashboard.py | 208 +++++++ .../original/services/camera.py | 71 +++ .../original/services/hermes_client.py | 474 +++++++++++++++ .../original/static/js/alfred-admin.js | 187 ++++++ .../original/templates/alfred_logs_v2.html | 31 + .../original/templates/config.html | 78 +++ .../original/templates/configuration.html | 130 +++++ .../original/templates/index.html | 198 +++++++ .../original/templates/surveillance.html | 47 ++ .../20261028_172354_094558/manifest.json | 49 ++ .../20261028_172354_094558/original/app.py | 504 ++++++++++++++++ .../original/templates/index.html | 192 +++++++ alfred/app/.alfred_lists_v1_hermes_installed | 0 .../20261028_160727_232470/manifest.json | 43 ++ .../20261028_160727_232470/original/app.py | 451 +++++++++++++++ .../original/templates/index.html | 187 ++++++ alfred/app/__pycache__/app.cpython-313.pyc | Bin 33879 -> 38491 bytes alfred/app/app.py | 93 +++ alfred/app/cache/weather_outfit_cache.json | 116 ++++ ...2de6ac77c840557f2f04499a53e92990377d00.jpg | Bin 0 -> 26206 bytes ...f443460d642ad9789c9a4bee3b006a1515a01e.jpg | Bin 0 -> 25697 bytes ...783fb9d9f1ec25d2b8163b105015ad320703da.jpg | Bin 0 -> 24752 bytes ...37fbd24e829921ec8e73f13addf8a1647a9cf9.jpg | Bin 0 -> 25455 bytes alfred/app/config/control_center.env | 11 +- alfred/app/config/home_card.json | 17 + alfred/app/config/identity.md | 10 +- .../20261028_214226_191641_avant_nom.md | 31 + .../20261028_214309_009521_avant_nom.md | 31 + alfred/app/config/identity_name_state.json | 1 + alfred/app/data/alfred_features.sqlite3 | Bin 32768 -> 36864 bytes alfred/app/data/alfred_features.sqlite3-shm | Bin 32768 -> 32768 bytes alfred/app/data/alfred_features.sqlite3-wal | Bin 24752 -> 0 bytes alfred/app/data/alfred_lists.sqlite3 | Bin 0 -> 16384 bytes alfred/app/data/timers.json | 11 + .../alfred_listes_mcp.cpython-313.pyc | Bin 0 -> 8388 bytes alfred/app/mcp/alfred_listes_mcp.py | 87 +++ .../__pycache__/admin_console.cpython-313.pyc | Bin 33503 -> 34108 bytes .../alfred_dashboard.cpython-313.pyc | Bin 0 -> 14902 bytes .../alfred_home_glass.cpython-313.pyc | Bin 0 -> 14977 bytes .../alfred_identity_page.cpython-313.pyc | Bin 0 -> 14135 bytes .../__pycache__/alfred_lists.cpython-313.pyc | Bin 0 -> 17242 bytes .../alfred_logs_page.cpython-313.pyc | Bin 0 -> 7809 bytes .../__pycache__/camera.cpython-313.pyc | Bin 4111 -> 4210 bytes .../__pycache__/hermes_client.cpython-313.pyc | Bin 12060 -> 12189 bytes alfred/app/services/admin_console.py | 12 +- alfred/app/services/alfred_dashboard.py | 209 +++++++ alfred/app/services/alfred_home_glass.py | 194 +++++++ alfred/app/services/alfred_identity_page.py | 204 +++++++ alfred/app/services/alfred_lists.py | 249 ++++++++ alfred/app/services/alfred_logs_page.py | 115 ++++ alfred/app/services/camera.py | 2 +- alfred/app/services/hermes_client.py | 4 + alfred/app/skills/alfred-listes/SKILL.md | 20 + alfred/app/static/css/alfred-admin-glass.css | 71 +++ .../app/static/css/alfred-identity-glass.css | 1 + alfred/app/static/css/alfred-listes-glass.css | 40 ++ alfred/app/static/css/alfred-logs-v2.css | 16 + alfred/app/static/css/alfred-main-v1.css | 156 +++++ alfred/app/static/css/alfred-main-v2.css | 72 +++ alfred/app/static/img/alfred-ambience.webp | Bin 0 -> 8616 bytes alfred/app/static/js/alfred-admin-glass.js | 137 +++++ alfred/app/static/js/alfred-admin.js | 5 +- alfred/app/static/js/alfred-identity-glass.js | 60 ++ alfred/app/static/js/alfred-listes-glass.js | 125 ++++ alfred/app/static/js/alfred-logs-v2.js | 45 ++ alfred/app/static/js/alfred-main-v1.js | 319 +++++++++++ alfred/app/static/js/alfred-main-v2.js | 71 +++ alfred/app/static/js/alfred-maison-card.js | 31 + alfred/app/static/js/alfred-name-dynamic.js | 93 +++ .../app/templates/alfred_identity_glass.html | 41 ++ alfred/app/templates/alfred_logs_v2.html | 33 ++ alfred/app/templates/config.html | 5 + alfred/app/templates/configuration.html | 2 + alfred/app/templates/index.html | 13 + alfred/app/templates/surveillance.html | 7 +- .../__pycache__/proxmox_mcp.cpython-313.pyc | Bin 0 -> 10310 bytes hermes/MCP_proxmox/proxmox_mcp.py | 2 +- 89 files changed, 8691 insertions(+), 13 deletions(-) create mode 100644 alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/manifest.json create mode 100644 alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/app.py create mode 100644 alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/static/js/alfred-main-v1.js create mode 100644 alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/config.html create mode 100644 alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/index.html create mode 100644 alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/surveillance.html create mode 100644 alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/manifest.json create mode 100644 alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/original/app.py create mode 100644 alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/original/templates/index.html create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/manifest.json create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/app.py create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/admin_console.py create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/alfred_dashboard.py create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/camera.py create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/hermes_client.py create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/static/js/alfred-admin.js create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/alfred_logs_v2.html create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/config.html create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/configuration.html create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/index.html create mode 100644 alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/surveillance.html create mode 100644 alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/manifest.json create mode 100644 alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/original/app.py create mode 100644 alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/original/templates/index.html create mode 100644 alfred/app/.alfred_lists_v1_hermes_installed create mode 100644 alfred/app/.alfred_main_v1_backups/20261028_160727_232470/manifest.json create mode 100644 alfred/app/.alfred_main_v1_backups/20261028_160727_232470/original/app.py create mode 100644 alfred/app/.alfred_main_v1_backups/20261028_160727_232470/original/templates/index.html create mode 100644 alfred/app/cache/weather_outfit_images/7bd7476257fbcb9a6432c69e9f2de6ac77c840557f2f04499a53e92990377d00.jpg create mode 100644 alfred/app/cache/weather_outfit_images/7f4ab9e5b21e1d02bb502799f4f443460d642ad9789c9a4bee3b006a1515a01e.jpg create mode 100644 alfred/app/cache/weather_outfit_images/abc6e852574fa821f2b272ba2a783fb9d9f1ec25d2b8163b105015ad320703da.jpg create mode 100644 alfred/app/cache/weather_outfit_images/c482b5a8e1fff3a0d6589fdb1f37fbd24e829921ec8e73f13addf8a1647a9cf9.jpg create mode 100644 alfred/app/config/home_card.json create mode 100644 alfred/app/config/identity_history/20261028_214226_191641_avant_nom.md create mode 100644 alfred/app/config/identity_history/20261028_214309_009521_avant_nom.md create mode 100644 alfred/app/config/identity_name_state.json create mode 100644 alfred/app/data/alfred_lists.sqlite3 create mode 100644 alfred/app/mcp/__pycache__/alfred_listes_mcp.cpython-313.pyc create mode 100644 alfred/app/mcp/alfred_listes_mcp.py create mode 100644 alfred/app/services/__pycache__/alfred_dashboard.cpython-313.pyc create mode 100644 alfred/app/services/__pycache__/alfred_home_glass.cpython-313.pyc create mode 100644 alfred/app/services/__pycache__/alfred_identity_page.cpython-313.pyc create mode 100644 alfred/app/services/__pycache__/alfred_lists.cpython-313.pyc create mode 100644 alfred/app/services/__pycache__/alfred_logs_page.cpython-313.pyc create mode 100644 alfred/app/services/alfred_dashboard.py create mode 100644 alfred/app/services/alfred_home_glass.py create mode 100644 alfred/app/services/alfred_identity_page.py create mode 100644 alfred/app/services/alfred_lists.py create mode 100644 alfred/app/services/alfred_logs_page.py create mode 100644 alfred/app/skills/alfred-listes/SKILL.md create mode 100644 alfred/app/static/css/alfred-admin-glass.css create mode 100644 alfred/app/static/css/alfred-identity-glass.css create mode 100644 alfred/app/static/css/alfred-listes-glass.css create mode 100644 alfred/app/static/css/alfred-logs-v2.css create mode 100644 alfred/app/static/css/alfred-main-v1.css create mode 100644 alfred/app/static/css/alfred-main-v2.css create mode 100644 alfred/app/static/img/alfred-ambience.webp create mode 100644 alfred/app/static/js/alfred-admin-glass.js create mode 100644 alfred/app/static/js/alfred-identity-glass.js create mode 100644 alfred/app/static/js/alfred-listes-glass.js create mode 100644 alfred/app/static/js/alfred-logs-v2.js create mode 100644 alfred/app/static/js/alfred-main-v1.js create mode 100644 alfred/app/static/js/alfred-main-v2.js create mode 100644 alfred/app/static/js/alfred-maison-card.js create mode 100644 alfred/app/static/js/alfred-name-dynamic.js create mode 100644 alfred/app/templates/alfred_identity_glass.html create mode 100644 alfred/app/templates/alfred_logs_v2.html create mode 100644 hermes/MCP_proxmox/__pycache__/proxmox_mcp.cpython-313.pyc diff --git a/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/manifest.json b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/manifest.json new file mode 100644 index 0000000..a8aef9d --- /dev/null +++ b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/manifest.json @@ -0,0 +1,61 @@ +{ + "version": 1, + "created_at": "2026-10-28T16:25:44.119642", + "installed": true, + "files": { + "templates/config.html": { + "existed": true, + "before_sha256": "d6fd60c8034688019c4501869dfd1dd61aad31bfee6e020bbe0e00743ac964b1", + "after_sha256": "4e6300fcf4b0dafcfc1e196d219734bb35263572c316ad7d010b66d0f6915762", + "mode": 420 + }, + "templates/surveillance.html": { + "existed": true, + "before_sha256": "c603647ee11dddb771bf588b8469774a012c9aa0bf6af302859df80dfdf00add", + "after_sha256": "0bd5a9a17b613c6b02e53146f4ffeb92b8bb94c9e2979d460d2d96de4ecb96cb", + "mode": 420 + }, + "templates/index.html": { + "existed": true, + "before_sha256": "cc1fbc7cab0c80d7f2799ac565c45e94b8fddb0af26e67172afde8ffd39a6231", + "after_sha256": "73a4f976e301da24f0a59b8866be4f74e001931a641e0483d0b6d1cae133a971", + "mode": 420 + }, + "static/js/alfred-main-v1.js": { + "existed": true, + "before_sha256": "5552cc33cfb1c236cb474a75d305a6df8302ef70051c057f8e8175352d014fc2", + "after_sha256": "0bef31b1e156de0a11141f79895013bb498507d9aec6e5c18546660abdba1691", + "mode": 420 + }, + "app.py": { + "existed": true, + "before_sha256": "631ad0886437b59d4b8573ffd83bf2330511dfee92dc767baa5d42ac7d9c246e", + "after_sha256": "e4fe3af1db2e0d39231f0a3ae341818b2765a14c102f827dc7c6772bbdbc572b", + "mode": 420 + }, + "services/alfred_home_glass.py": { + "existed": false, + "before_sha256": null, + "after_sha256": "672c2c83eb40a74e41ae8c349301d474e7c3130b3db2459401a6150d9201f9cd", + "mode": 420 + }, + "static/css/alfred-admin-glass.css": { + "existed": false, + "before_sha256": null, + "after_sha256": "2de6d0934aeb790fd8fce6287109ed52a63a6e16358ff8b837051b249adfaa9c", + "mode": 420 + }, + "static/js/alfred-admin-glass.js": { + "existed": false, + "before_sha256": null, + "after_sha256": "73b6c7d4186e918bea83edb8308e47bbfe2ff90acd5d189f89d482dd5770223f", + "mode": 420 + }, + "static/js/alfred-maison-card.js": { + "existed": false, + "before_sha256": null, + "after_sha256": "4eb6ba8c1c81145770328d66df34336a873151f1cf07ffd975886013730b617a", + "mode": 420 + } + } +} \ No newline at end of file diff --git a/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/app.py b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/app.py new file mode 100644 index 0000000..78efeef --- /dev/null +++ b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/app.py @@ -0,0 +1,498 @@ +import os, threading, hmac +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from flask import Flask, render_template, request, jsonify, Response +from flask_socketio import SocketIO +from dotenv import load_dotenv +from services.audio import listen_once, speak_async, tts_backend_detect +from services.wakeword import WakeWordListener +from services import admin_console # ALFRED_ADMIN_PAGES_V1 +from services.hermes_client import ask_hermes +from services import alfred_timers, alfred_context_images # ALFRED_TIMERS_CONTEXT_V1 +from services import alfred_features # ALFRED_FEATURES_V45 +from services import audio_settings # ALFRED_AUDIO_SETTINGS_V1 +from services.camera import video_stream +from services import ha_views, alfred_views_controller +from services import camera_settings, ha_weather, weather_outfits + +load_dotenv() + +load_dotenv(os.path.join(os.path.dirname(__file__), "config", "control_center.env"), override=True) # ALFRED_ADMIN_PAGES_V1 +## +from services.audio import get_vosk_model +print("[ALFRED] Préchargement des modèles vocaux...", flush=True) +get_vosk_model(os.environ["WAKE_VOSK_MODEL_PATH"]) +get_vosk_model(os.environ["VOSK_MODEL_PATH"]) +print("[ALFRED] Modèles vocaux prêts.", flush=True) +## +app=Flask(__name__); socketio=SocketIO(app,cors_allowed_origins="*") +STATE={"mode":"idle","title":"Alfred","content":{"type":"text","text":"À votre écoute."},"partial":""} +STATE_LOCK = threading.RLock() +IMAGE_REVISION = 0 # Incrémenté à chaque image envoyée par ComfyUI. +conversation_lock=threading.Lock() +mic_stop_event=threading.Event() +# Service temps autonome (survit aux conversations Hermes) +alfred_timer_store = alfred_timers.register(app, socketio, speak_async, audio_settings) # ALFRED_TIMERS_CONTEXT_V1 +audio_settings.initialize() +audio_settings.update_settings({"mic_gain": 30}) +# Recharge les réglages V4L2 enregistrés, sans interrompre Alfred si la webcam est absente. +camera_settings.restore_saved_settings() + +def emit_state(mode=None, **extra): + with STATE_LOCK: + if mode: STATE["mode"] = mode + STATE.update(extra) + socketio.emit("alfred_state", dict(STATE)) + +def audio_level(level): socketio.emit("audio_level",{"level":max(0,min(float(level),1))}) + +def conversation(score=1.0): + if not conversation_lock.acquire(blocking=False): + return + + try: + emit_state("listening", partial="", transcript="", wake_score=score) + + print("[ALFRED] Début écoute STT", flush=True) + + r = listen_once( + stop_event=mic_stop_event, + on_partial=lambda p: emit_state("listening", partial=p), + on_level=audio_level + ) + + print(f"[ALFRED] Résultat STT : {r.text!r}, erreur : {r.error!r}", flush=True) + + if audio_settings.mic_muted(): + emit_state("idle", partial="") + return + + if r.error: + emit_state( + "error", + content={"type": "text", "text": r.error} + ) + return + + if not r.text: + emit_state("idle", partial="") + return + + emit_state( + "thinking", + partial="", + transcript=r.text, + content={"type": "text", "text": r.text} + ) + + # Vue dynamique : commandes déterministes exécutées sans faire + # dépendre l'affichage des capacités ou sessions Hermes. + visual = None + try: + visual = alfred_views_controller.route(r.text, (STATE.get('content') or {}).get('type')) + except (ha_views.HAError, ValueError) as exc: + print(f"[ALFRED] Vue indisponible : {exc}", flush=True) + visual = ({"type":"text", "text":str(exc)}, str(exc)) + + if visual is not None: + content, speech = visual + with STATE_LOCK: + STATE["content"] = content + socketio.emit("alfred_render", content) + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + emit_state("idle", partial="") + return + + print("[ALFRED] Envoi à Hermes", flush=True) + # ComfyUI publie l'image pendant cet appel Hermes. + with STATE_LOCK: + image_revision_before = IMAGE_REVISION + # ALFRED_FEATURES_V45 voice opt-in background mode + if alfred_features.should_run_in_background(r.text): + try: + alfred_features.submit(r.text, source='voice') + emit_state('idle', partial='', content={ + 'type': 'text', + 'text': 'Hermes travaille sur la demande. Le résultat apparaîtra dans les cartes.' + }) + except ValueError as exc: + emit_state('error', content={'type': 'text', 'text': str(exc)}) + return + answer = ask_hermes(r.text) + print(f"[ALFRED] Réponse Hermes : {answer!r}", flush=True) + if isinstance(answer, dict): + speech = str(answer.get("speech") or answer.get("text") or "") + display = answer.get("display") or { + "type": "text", + "text": speech + } + else: + speech = str(answer or "") + display = {"type": "text", "text": speech} + + # Une illustration contextuelle (Wikipédia) peut compléter la réponse Hermes. + # Elle n'interrompt pas la lecture TTS et ne remplace pas la scène centrale. + alfred_context_images.schedule_context_image( + r.text, speech, socketio, + lambda: STATE.get("transcript") == r.text, + ) # ALFRED_TIMERS_CONTEXT_V1 + + # Si le MCP ComfyUI a affiché une image pendant la requête, + # ne pas l'écraser par la réponse textuelle d'Hermes. + with STATE_LOCK: + if IMAGE_REVISION == image_revision_before: + STATE["content"] = display + alfred_features.publish_voice_display(display) + socketio.emit("alfred_render", display) + else: + print("[ALFRED] Image ComfyUI conservée à l'écran", flush=True) + + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + + emit_state("idle", partial="") + + except Exception as exc: + print(f"[ALFRED] Erreur conversation : {exc}", flush=True) + emit_state( + "error", + content={"type": "text", "text": str(exc)} + ) + + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + print("[ALFRED] Wake word réactivé", flush=True) + + + +def on_wake(score): + if audio_settings.mic_muted(): + return + socketio.emit("wake_detected",{"word":os.getenv("WAKE_WORD","alfred"),"score":score}) + threading.Thread(target=conversation,args=(score,),daemon=True).start() + +wake=WakeWordListener(on_wake=on_wake,on_level=lambda l: audio_level(l if STATE["mode"]!="idle" else l*.18)) + +admin_console.register_admin_console(app, wake, audio_settings) # ALFRED_ADMIN_PAGES_V1 +@app.get('/api/audio/settings') +def get_audio_settings(): + return jsonify(audio_settings.get_settings()) + +@app.post('/api/audio/settings') +def set_audio_settings(): + data = request.get_json(silent=True) + try: + old = audio_settings.get_settings() + new = audio_settings.update_settings(data) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except Exception as exc: + return jsonify({'error': f'ALSA : {exc}'}), 500 + if new['mic_muted'] != old['mic_muted']: + if new['mic_muted']: + mic_stop_event.set() + wake.pause() + else: + mic_stop_event.clear() + if not conversation_lock.locked(): + wake.resume() + socketio.emit('audio_settings', new) + return jsonify(new) + +@app.get('/api/camera/settings') +def get_camera_settings(): + try: + return jsonify(camera_settings.get_settings()) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/settings') +def set_camera_settings(): + # Même garde-fou que les commandes calendrier existantes. + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({'error': 'JSON invalide.'}), 400 + try: + result = camera_settings.update_setting(data.get('control'), data.get('value')) + socketio.emit('camera_settings', result) + return jsonify(result) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/reset') +def reset_camera_settings(): + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + try: + result = camera_settings.reset_defaults() + socketio.emit('camera_settings', result) + return jsonify(result) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.get('/api/camera/stream') +def camera_stream(): + # Flux MJPEG à la demande ; ne démarre jamais la webcam en arrière-plan. + try: + frames = video_stream(index=int(os.getenv('CAMERA_INDEX', '0'))) + except (ImportError, ValueError, RuntimeError, OSError) as exc: + return jsonify({'error': str(exc)}), 503 + return Response( + frames, + mimetype='multipart/x-mixed-replace; boundary=frame', + headers={'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff'} + ) + +# ALFRED_FEATURES_V45 init +alfred_features.init_app(app, ask_hermes, socketio=socketio) + +@app.get("/") +def index(): return render_template("index.html",name=os.getenv("ALFRED_NAME","Alfred")) +@app.get("/api/state") +def state(): + with STATE_LOCK: + snapshot = dict(STATE) + return jsonify({**snapshot, "wake": wake.status(), "tts": tts_backend_detect()}) + +@app.post("/api/state") +def set_state(): + data = request.get_json(force=True) + with STATE_LOCK: + STATE.update(data) + socketio.emit("alfred_state", dict(STATE)) + return jsonify({"ok": True}) +@app.post("/api/render") +def render_payload(): + global IMAGE_REVISION + token = os.getenv("ALFRED_RENDER_TOKEN", "").strip() + if token and not hmac.compare_digest( + request.headers.get("X-Alfred-Render-Token", ""), token + ): + return jsonify({"ok": False, "error": "Non autorisé"}), 401 + + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({"ok": False, "error": "JSON objet attendu"}), 400 + if data.get("type") == "image": + image_url = data.get("url") + if not isinstance(image_url, str) or not ( + image_url.startswith(("http://", "https://", "/")) + and not image_url.startswith("//") + ): + return jsonify({"ok": False, "error": "URL image invalide"}), 400 + + with STATE_LOCK: + if data.get("type") == "image": + IMAGE_REVISION += 1 + STATE["content"] = data + socketio.emit("alfred_render", data) + return jsonify({"ok": True, "image_revision": IMAGE_REVISION}) +# --- Agenda / capteurs / caméras Home Assistant --- +def _calendar_range(): + tz = ZoneInfo(os.getenv('ALFRED_TIMEZONE', 'Europe/Paris')) + now = datetime.now(tz) + def parse(name, default): + raw = request.args.get(name) + if not raw:return default + value = datetime.fromisoformat(raw.replace('Z','+00:00')) + if value.tzinfo is None: value = value.replace(tzinfo=tz) + return value.astimezone(tz) + start = parse('start',now.replace(hour=0,minute=0,second=0,microsecond=0)) + end = parse('end',start+timedelta(days=7)) + return start,end + +def _json_error(exc): + code = 400 if isinstance(exc,ValueError) else 502 + return jsonify({'error':str(exc)}),code + +def _writes_enabled(): + return os.getenv('ALFRED_ALLOW_CALENDAR_WRITES','false').lower() in ('1','true','yes') + +def _same_origin_json(): + # POSTs require a custom header and same-origin browser requests. + if request.headers.get('X-Alfred-Request') != '1':return False + origin=request.headers.get('Origin') + if origin and origin.rstrip('/') != request.host_url.rstrip('/'):return False + return True + +@app.get('/api/calendar/calendars') +def calendars_list(): + try:return jsonify({'calendars':ha_views.list_calendars(),'writes_enabled':_writes_enabled()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/calendar/events') +def calendar_events(): + try: + start,end=_calendar_range() + selected=request.args.getlist('calendar_id') + return jsonify(ha_views.get_events(start,end,selected or None)) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/calendar/create') +def calendar_create(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée (ALFRED_ALLOW_CALENDAR_WRITES).'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + tz=ZoneInfo(os.getenv('ALFRED_TIMEZONE','Europe/Paris')) + def read_dt(key): + dt=datetime.fromisoformat(str(data[key]).replace('Z','+00:00')) + return dt.replace(tzinfo=tz) if dt.tzinfo is None else dt.astimezone(tz) + result=ha_views.create_event(str(data.get('calendar_id','')),str(data.get('title','')), + read_dt('start'),read_dt('end'),str(data.get('description') or ''), + str(data.get('location') or ''),bool(data.get('all_day',False))) + from services import alfred_intents + alfred_intents.clear_create() + socketio.emit('calendar_changed', {'type':'created'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.post('/api/calendar/delete') +def calendar_delete(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée.'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + eid=str(data.get('calendar_id',''));uid=str(data.get('uid','')) + start=datetime.fromisoformat(str(data['start'])) + end=datetime.fromisoformat(str(data['end'])) + if start.tzinfo is None or end.tzinfo is None:raise ValueError('Date avec fuseau requise.') + matches=ha_views.get_events(start-timedelta(days=1),end+timedelta(days=1),[eid])['events'] + rid=str(data.get('recurrence_id') or '') + if not any(e['id']==uid and e['start']==start.isoformat() and e['recurrence_id']==rid for e in matches): + return jsonify({'error':'Événement introuvable ou modifié ; actualise le planning.'}),409 + result=ha_views.delete_event(eid,uid,rid or None) + from services import alfred_intents + alfred_intents.clear_delete() + socketio.emit('calendar_changed',{'type':'deleted'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.get('/api/ha/weather') +def ha_weather_data(): + # Read-only route, token always stays on the Alfred server. + try: + q = request.args.get('q', '') + return jsonify(ha_weather.get_weather(q)) + except (ha_views.HAError, ValueError) as exc: + return _json_error(exc) + +@app.get('/api/ha/weather/outfits') +def ha_weather_outfits_data(): + try: + q = request.args.get('q', '') + refresh = request.args.get('refresh_illustration', '').lower() in ('1','true','yes','on') + return jsonify(weather_outfits.build_payload(q=q, date=request.args.get('date', ''), label=request.args.get('label', ''), refresh=refresh)) + except (ha_views.HAError, ValueError, RuntimeError) as exc: + return _json_error(exc) + +@app.get('/api/ha/temperatures') +def ha_temperatures(): + try:return jsonify({'items':ha_views.get_temperatures()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/temperature/history') +def ha_temperature_history(): + try: + eid=request.args.get('entity_id','') + hours=int(request.args.get('hours','24')) + return jsonify({'items':ha_views.temperature_history(eid,hours)}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/cameras') +def ha_cameras(): + try:return jsonify({'items':ha_views.list_cameras()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/camera//snapshot') +def ha_camera_snapshot(entity_id): + try: + binary,mime=ha_views.camera_snapshot(entity_id) + return Response(binary,mimetype=mime,headers={'Cache-Control':'no-store','X-Content-Type-Options':'nosniff'}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/ui/home') +def ui_home(): + with STATE_LOCK: + STATE['content']={'type':'home','text':'À ton écoute.'} + socketio.emit('alfred_render',STATE['content']) + return jsonify({'ok':True}) + +@app.post("/api/audio-level") +def api_audio_level(): audio_level((request.get_json(force=True) or {}).get("level",0)); return jsonify({"ok":True}) +@app.get("/api/wake/status") +def wake_status(): return jsonify(wake.status()) +@app.post("/api/wake/start") +def wake_start(): wake.start(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/wake/stop") +def wake_stop(): wake.stop(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/tts/say") +def tts_say(): + text=(request.get_json(silent=True) or {}).get("text",""); return jsonify({"ok":speak_async(text)}) + + +# ALFRED_MAIN_VISUAL_V1_BACKEND_START +# Raccourcis visuels : même Hermes, même verrou de conversation et même moteur Piper. +def _alfred_dashboard_quick_command(prompt): + if not conversation_lock.acquire(blocking=False): + return False + + def _run(): + try: + wake.pause() + with STATE_LOCK: + _image_revision_before = IMAGE_REVISION + emit_state('thinking', transcript=prompt, partial='', + content={'type':'text', 'text':prompt}) + _answer = ask_hermes(prompt) + if isinstance(_answer, dict): + _speech = str(_answer.get('speech') or _answer.get('text') or '') + _display = _answer.get('display') or {'type':'text','text':_speech} + else: + _speech = str(_answer or '') + _display = {'type':'text','text':_speech} + # Une image ComfyUI publiee pendant Hermes ne doit pas etre effacee. + with STATE_LOCK: + if IMAGE_REVISION == _image_revision_before: + STATE['content'] = _display + socketio.emit('alfred_render', _display) + if _speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _done = threading.Event() + speak_async(_speech, on_end=_done.set) + _done.wait(timeout=120) + emit_state('idle', partial='') + except Exception as _exc: + print('[ALFRED DASHBOARD] Erreur raccourci :', _exc, flush=True) + emit_state('error', content={'type':'text', 'text':str(_exc)}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + + threading.Thread(target=_run, daemon=True, name='alfred-dashboard-command').start() + return True + +from services.alfred_dashboard import register_dashboard as _register_dashboard_visual_v1 +_register_dashboard_visual_v1(app, quick_command=_alfred_dashboard_quick_command) +# ALFRED_MAIN_VISUAL_V1_BACKEND_END + +if __name__ == "__main__": + if os.getenv("WAKE_ENABLED","true").lower() in ("1","true","yes","on") and not audio_settings.mic_muted(): wake.start() + socketio.run(app,host=os.getenv("FLASK_HOST","0.0.0.0"),port=int(os.getenv("FLASK_PORT","54321")),allow_unsafe_werkzeug=True) + diff --git a/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/static/js/alfred-main-v1.js b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/static/js/alfred-main-v1.js new file mode 100644 index 0000000..4c9a969 --- /dev/null +++ b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/static/js/alfred-main-v1.js @@ -0,0 +1,318 @@ +/* ALFRED_MAIN_VISUAL_V1 — accueil vivant, conservation des contrôleurs existants. */ +(() => { + 'use strict'; + if (document.getElementById('alfredMainV1')) return; + const shell = document.querySelector('.alfred-shell'); + if (!shell || !document.getElementById('stage') || !document.getElementById('content')) return; + + const I = { + home:'', + bulb:'', + server:'', + brain:'', + media:'', + more:'', + mic:'', + micOff:'', + speaker:'', + speakerOff:'', + camera:'', + settings:'', + calendar:'', + clock:'', + cloud:'', + shield:'', + arrow:'', + chat:'', + close:'', + sun:'', + spark:'', + }; + const icon = (name, extra = '') => ``; + const initialName = (document.title || 'Alfred').trim(); + const dashboard = document.createElement('div'); + dashboard.id = 'alfredMainV1'; + dashboard.className = 'alfred-main-v1'; + dashboard.innerHTML = ` +
+
Assistant vocal
+
--:--—
+
+ + + + + ${icon('more')} +
+
+
+ +
+ +
${icon('server')}SystèmeMini-PC Alfred${icon('arrow','av-chevron')}

Charge : —

Mémoire : —

Stockage : —

+
${icon('brain')}Services IAStatuts en cours…${icon('arrow','av-chevron')}

Ollama : —

Hermes : —

ComfyUI : —

+ +
+
+
+ +
${icon('mic')}
En veille

Dites « Alfred » puis votre demande

Prêt à vous écouter
+ +
+
+
+
+ + + + + + + +
+
${icon('chat')} Interactions récentes
Aucune interaction depuis l’ouverture de cette page
+ + +

Créer une image avec ComfyUI

Décris l’image à générer. Alfred transmettra ta demande à Hermes.

+

Outils Alfred

${icon('settings')} Configuration${icon('server')} Surveillance
+ `; + shell.insertBefore(dashboard, shell.firstChild); + document.body.classList.add('alfred-glass-theme'); + document.getElementById('avName').textContent = initialName; + const wave = document.getElementById('wave'); + document.getElementById('avWaveHost').appendChild(wave); + window.dispatchEvent(new Event('resize')); + const $ = id => document.getElementById(id); + const timeZone = 'Europe/Paris'; + let lastStatus = 'idle'; + let mute = {mic_muted:false,speaker_muted:false}; + let refreshingAudio = false; + let toastTimer; + + function toast(message) { + const el = $('avToast'); el.textContent = String(message); el.hidden = false; + clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.hidden = true; }, 5500); + } + function clock() { + const now = new Date(); + $('avTime').textContent = new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit'}).format(now); + const date = new Intl.DateTimeFormat('fr-FR',{timeZone,weekday:'long',day:'numeric',month:'long',year:'numeric'}).format(now); + $('avDate').textContent = date.charAt(0).toUpperCase() + date.slice(1); + } + clock(); setInterval(clock, 15000); + function updateAudio(a) { + mute = {...mute,...a}; + const mic = Boolean(mute.mic_muted), speaker = Boolean(mute.speaker_muted); + for (const id of ['avTopMic','avMicButton']) { + const button = $(id); button.setAttribute('aria-pressed',String(mic)); + button.setAttribute('aria-label', mic ? 'Réactiver le microphone' : 'Couper le microphone'); + button.title = button.getAttribute('aria-label'); + button.querySelector('svg').outerHTML = icon(mic?'micOff':'mic'); + } + for (const id of ['avTopSpeaker','avSpeakerButton']) { + const button = $(id); button.setAttribute('aria-pressed',String(speaker)); + button.setAttribute('aria-label', speaker?'Réactiver la sortie audio':'Couper la sortie audio'); + button.title = button.getAttribute('aria-label'); + button.querySelector('svg').outerHTML = icon(speaker?'speakerOff':'speaker'); + } + $('avMicLabel').textContent = mic?'Coupé':'Actif'; + $('avSpeakerLabel').textContent = speaker?'Coupée':'Activée'; + dashboard.classList.toggle('av-mic-is-muted',mic); + dashboard.classList.toggle('av-speaker-is-muted',speaker); + renderMode(lastStatus); + } + async function refreshAudio() { + if (refreshingAudio) return; + refreshingAudio = true; + try { const r=await fetch('/api/audio/settings',{cache:'no-store'}); + if (!r.ok) throw Error('HTTP '+r.status); + updateAudio(await r.json()); + } catch (e) { console.warn('[ALFRED UI] Réglages audio :',e); } + finally {refreshingAudio=false;} + } + async function toggleAudio(kind) { + const prop = kind==='mic'?'mic_muted':'speaker_muted'; + try { + const r=await fetch('/api/audio/settings',{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({[prop]:!mute[prop]})}); + const data=await r.json(); + if (!r.ok) throw Error(data.error || 'HTTP '+r.status); + updateAudio(data); + toast(kind==='mic' ? (data.mic_muted?'Microphone coupé':'Microphone réactivé') : (data.speaker_muted?'Sortie audio coupée':'Sortie audio réactivée')); + } catch (e) { toast('Impossible de modifier le son : '+e.message); } + } + for (const id of ['avTopMic','avMicButton']) $(id).addEventListener('click',()=>toggleAudio('mic')); + for (const id of ['avTopSpeaker','avSpeakerButton']) $(id).addEventListener('click',()=>toggleAudio('speaker')); + $('avTopSettings').addEventListener('click',()=>{const btn=$('audioSettingsButton'); if(btn)btn.click();else location.assign('/config');}); + refreshAudio(); setInterval(()=>{if(!document.hidden)refreshAudio();},30000); + + function serviceText(state) {return state==='ok'?'Connecté':state==='offline'?'Hors ligne':state==='installed'?'Installé (non testé)':state==='unconfigured'?'Non configuré':'Non vérifié';} + function setService(id,dotid,label,state) { + $(id).textContent = `${label} : ${serviceText(state)}`; + $(dotid).className = `av-mini-dot ${state==='ok'?'is-ok':state==='offline'?'is-error':''}`; + } + async function refreshDashboard() { + try { + const r=await fetch('/api/alfred/dashboard',{cache:'no-store'}); + if(!r.ok) throw Error('HTTP '+r.status); + const d=await r.json(); + const h=d.home||{}, s=d.system||{}, ai=d.services||{}, w=d.weather||{}; + $('avHomeSummary').textContent=h.available?'Home Assistant connecté':(h.detail||'Données non disponibles'); + $('avLights').textContent = h.available?`${h.lights_on} lumière(s) allumée(s)`:'Lumières : indisponible'; + $('avCovers').textContent = h.available?`${h.covers_closed} volet(s) fermé(s)`:'Volets : indisponible'; + $('avIndoor').textContent = h.indoor_temperature!=null?`${h.indoor_temperature} °C · intérieur`:'Température : non disponible'; + $('avLoad').textContent = s.load_1m!=null?`Charge 1 min : ${s.load_1m}`:'Charge : non disponible'; + $('avMemory').textContent = s.memory_percent!=null?`Mémoire : ${s.memory_percent} %`:'Mémoire : non disponible'; + $('avDisk').textContent = s.disk_percent!=null?`Stockage : ${s.disk_percent} %`:'Stockage : non disponible'; + setService('avOllama','avOllamaDot','Ollama',ai.ollama); + setService('avHermes','avHermesDot','Hermes',ai.hermes); + setService('avComfy','avComfyDot','ComfyUI',ai.comfyui); + $('avAiSummary').textContent='États vérifiés / installation locale'; + $('avWeatherTemp').textContent=w.available&&w.temperature!=null?`${w.temperature} ${w.unit||'°C'}`:'—'; + $('avWeatherCondition').textContent=w.available?w.condition:'Non disponible'; + const sunny=['sunny','clear-night','partlycloudy'].includes(w.condition_id); + $('avWeatherIcon').textContent=sunny?'🌤️':w.condition_id==='rainy'?'🌧️':w.condition_id==='snowy'?'❄️':'☁️'; + } catch (e) { + $('avHomeSummary').textContent='Données indisponibles'; + $('avAiSummary').textContent='Surveillance indisponible'; + $('avWeatherCondition').textContent='Indisponible'; + console.warn('[ALFRED UI] Tableau de bord :',e); + } + } + refreshDashboard(); setInterval(()=>{if(!document.hidden)refreshDashboard();},30000); + async function refreshForecast() { + try { + const r=await fetch('/api/ha/weather',{cache:'no-store'}); + if(!r.ok)return; + const d=await r.json(); + const day=(d.daily||[])[0]; + const vals=[]; + if(day?.temperature!=null) vals.push('↑ '+day.temperature+'°'); + if(day?.templow!=null) vals.push('↓ '+day.templow+'°'); + $('avWeatherRange').textContent=vals.join(' '); + } catch (_) { /* Not all installations expose the weather forecast route. */ } + } + refreshForecast();setInterval(()=>{if(!document.hidden)refreshForecast();},180000); + + function renderMode(mode) { + lastStatus = mode||'idle'; + const states = {idle:['En veille','Dites « '+initialName+' » puis votre demande','Prêt'], + waiting:['En veille','Dites « '+initialName+' » puis votre demande','Prêt'], + listening:['Je vous écoute…','Quelle est votre demande ?','Écoute en cours'], + thinking:['Je réfléchis…','Traitement de la demande','Hermes travaille'], + speaking:['Je vous réponds…','Réponse vocale en cours','Piper parle'], + error:['Un problème est survenu','Consultez la transcription ou la surveillance','Erreur']}; + const item=states[mode]||states.idle; + $('avVoiceTitle').textContent=mute.mic_muted?'Microphone coupé':item[0]; + $('avVoiceSubtitle').textContent=mute.mic_muted?'Réactivez le micro pour utiliser le mot d’activation':item[1]; + $('avStatusText').textContent=mute.mic_muted?'Écoute désactivée':item[2]; + dashboard.dataset.mode=mode; + $('avOrb').setAttribute('aria-label',item[0]); + } + renderMode('idle'); + + const recent=[]; + let lastTranscript=''; + function pushRecent(text) { + const value=String(text||'').trim(); + if(!value || value===lastTranscript)return; + lastTranscript=value; + recent.unshift({text:value,at:new Date()}); + if(recent.length>5)recent.pop(); + const root=$('avHistoryList');root.replaceChildren(); + for(const e of recent) { + const node=document.createElement('span');node.className='av-history-item'; + const t=document.createElement('time');t.textContent=new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit'}).format(e.at); + const content=document.createElement('span');content.textContent=e.text; + node.append(t,content);root.append(node); + } + } + function focusScene(show) { + dashboard.classList.toggle('is-focused', Boolean(show)); + $('avBack').hidden = !show; + } + function onRender(d) { if(!d || typeof d!=='object')return; + if(d.type==='home')focusScene(false); + else if(d.type && d.type!=='text' || (d.text||d.title||d.url))focusScene(true); + } + const socket=window.alfredSocket; + socket?.on('alfred_state',state=>{ + renderMode(String(state.mode||'idle').toLowerCase()); + if(state.mode==='thinking' && state.transcript)pushRecent(state.transcript); + }); + socket?.on('alfred_render',onRender); + socket?.on('audio_settings',updateAudio); + fetch('/api/state',{cache:'no-store'}).then(r=>r.ok?r.json():null).then(s=>{if(s)renderMode(String(s.mode||'idle').toLowerCase());}).catch(()=>{}); + + async function goHome() { + focusScene(false); + if(window.AlfredViews?.goHome){try{await window.AlfredViews.goHome();}catch(_){}} + else if($('viewHome'))$('viewHome').click(); + } + $('avBack').addEventListener('click',goHome); + function openCamera() { + if($('cameraPopupButton')){ $('cameraPopupButton').click();return; } + const camera=document.querySelector('.camera-panel'); + if(!camera){ toast('Caméra non installée');return; } + const isOpen=document.body.classList.toggle('av-legacy-camera-open'); + const toggle=$('cameraToggle'); + if(toggle && (toggle.getAttribute('aria-pressed')==='true')!==isOpen)toggle.click(); + if(isOpen && !$('avLegacyCameraClose')){ + const close=document.createElement('button');close.id='avLegacyCameraClose';close.type='button';close.textContent='Fermer la caméra'; + close.addEventListener('click',openCamera);camera.prepend(close); + } + } + $('avTopCamera').addEventListener('click',openCamera); + + function openView(name) { + const f=window.AlfredViews?.[name]; + if(typeof f==='function'){ focusScene(true);f();return; } + const fallback={openWeather:'viewWeather',openTemperatures:'viewTemperatures',openCalendar:'viewCalendar'}[name]; + if(fallback && $(fallback)){focusScene(true);$(fallback).click();return;} + toast('Cette fonction n’est pas disponible sur cette installation.'); + } + async function quickCommand(action,description) { + try { + const r=await fetch('/api/alfred/dashboard/command',{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({action,description})}); + const data=await r.json(); if(!r.ok)throw Error(data.error||'HTTP '+r.status); + pushRecent(({proxmox:'Vérifier Proxmox',wazuh:'Consulter les alertes Wazuh',comfyui:'Générer une image'})[action]||action); + toast('Demande transmise à Hermes'); + } catch(e) {toast('Commande indisponible : '+e.message);} + } + function showTime() { + const current=new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit',second:'2-digit'}).format(new Date()); + const content=$('content');content.replaceChildren(); + const title=document.createElement('h1');title.textContent='Il est '+current; + const subtitle=document.createElement('p');subtitle.textContent='Heure de Paris · horloge locale du navigateur'; + content.append(title,subtitle); focusScene(true);pushRecent('Quelle heure est-il ?'); + } + async function act(name) { + if(name==='home'){await goHome();return;} + if(name==='house'){openView('openTemperatures');return;} + if(name==='weather'){openView('openWeather');return;} + if(name==='calendar'){ $('avMoreDialog').close();openView('openCalendar');return; } + if(name==='camera'){ $('avMoreDialog').close();openCamera();return; } + if(name==='media'){ $('avComfyDialog').showModal();$('avComfyPrompt').focus();return;} + if(name==='time'){showTime();return;} + if(name==='proxmox'||name==='wazuh'){await quickCommand(name);return;} + if(name==='comfyui'){ $('avComfyDialog').showModal();$('avComfyPrompt').focus();return;} + if(name==='more'){ $('avMoreDialog').showModal();return;} + if(name==='audio'){ $('avMoreDialog').close();$('avTopSettings').click();return; } + } + dashboard.addEventListener('click',event=>{ + const button=event.target.closest('[data-av-action]'); + if(button)act(button.dataset.avAction); + }); + $('avComfySubmit').addEventListener('click',()=>{ + const text=$('avComfyPrompt').value.trim(); + if(text.length<4){toast('Décris l’image à générer.');return;} + $('avComfyDialog').close();quickCommand('comfyui',text); + }); + window.addEventListener('resize',()=>{ /* legacy alfred.js recalculates the waveform canvas */ }); +})(); diff --git a/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/config.html b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/config.html new file mode 100644 index 0000000..e61a605 --- /dev/null +++ b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/config.html @@ -0,0 +1,75 @@ + + + + +Alfred · Configuration + + + + + + + +
+
+
A
AlfredCentre de contrôle
+ +
+
+

PERSONNALISATION

Configuration

Paramètres de l’assistant, voix et services. Les modifications prennent effet après redémarrage.

Aucun changement
+ +
+
✦

Identité et voix

Nom affiché, mot d'activation et voix de synthèse

+
Visible dans l’interface après redémarrage.
+
Un seul mot. Le modèle Vosk doit pouvoir le reconnaître ; le mode OpenWakeWord nécessite un modèle entraîné.
+
Exemple : fr_FR-tom-medium. La voix doit exister sur ton serveur Wyoming ; pour Piper local, c’est le fichier modèle qui détermine la voix.
+
Utilise les réglages déjà actifs, sans redémarrage
+
+
☁

Illustrations météo

Qualité, visages de référence et accessoires ComfyUI

+
+
8.6 / 10
+
+
+
+
0.56
Une valeur élevée modifie davantage la tenue, mais risque de changer le visage.
+
+
+
+
♫

Audio en direct

Volume, sensibilité et coupures immédiates

+
—
+
—
+
+
+

Les réglages audio sont appliqués sans redémarrage.

+
+
⌘

Connexions et météo

Adresses des serveurs et source météo

+
Laisse vide pour conserver la configuration Hermes existante.
+
Cette adresse sert au contrôle de disponibilité ; Hermes garde sa propre configuration LLM.
+
Exemple : weather.forecast_home. Le jeton Home Assistant n’est pas exposé ici.
+ Voir la surveillance des services +
+
+ + +
+
◉

Reconnaissance vocale (STT)

Vosk local ou Whisper Wyoming distant · configuration effective après redémarrage

+
+ + Le mot d’activation et le texte provisoire restent gérés par Vosk. Whisper améliore la transcription finale envoyée à Hermes.
+
+
Adresse IP ou nom DNS, sans http://
+
+
+
+
+
+
+

Le modèle Whisper (small actuellement) et son vocabulaire technique (--initial-prompt pour Proxmox, Wazuh…) se configurent dans le conteneur Docker distant, pas sur Alfred. En cas de panne Whisper, retour automatique à Vosk si le correctif STT V1 est installé.

+
+

Maintenance

Redémarrer Alfred pour appliquer les paramètres enregistrés. Action protégée par mot de passe administrateur.

Le bouton requiert une autorisation systemd explicitement activée.
+

+
+
A

Accès administrateur

Connecte-toi pour modifier la configuration d'Alfred.

Accès local / réseau privé uniquement.
+
+ + diff --git a/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/index.html b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/index.html new file mode 100644 index 0000000..b3f86fa --- /dev/null +++ b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/index.html @@ -0,0 +1,190 @@ + + + + + + + {{ name }} + + + + + + + + + + + +
+
+ +
+ + En attente +
+ +
+ + ⚙ + ▤ + + + + + + + + + + + +
+
+ + + + + + +
+

Bonjour.

À votre écoute.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + diff --git a/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/surveillance.html b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/surveillance.html new file mode 100644 index 0000000..9b6f1f2 --- /dev/null +++ b/alfred/app/.alfred_admin_glass_maison_backups/20261028_162544_119463/original/templates/surveillance.html @@ -0,0 +1,44 @@ +Alfred · Surveillance + + + + +
+
A
AlfredCentre de contrôle
+
+

DIAGNOSTIC

Surveillance

État des périphériques, de l’assistant et des services locaux.

Chargement…
+
+
▣
CAMÉRAWebcam—

Vérification…

+
◉
MICROPHONECapture audio—

Vérification…

+
♫
HAUT-PARLEURSortie audio—

Vérification…

+
◌
MOT DE RÉVEILVosk—

Vérification…

+
+
⇄

Services et connexions

Tests réels : HTTP, port TCP ou présence du binaire

Chargement…

+
▤

Système

Ressources du mini-PC hébergeant Alfred

Disque de l’application—

—

Mémoire RAM—

—

Charge système (1 min)—
Les contrôles de caméra n'ouvrent pas le flux vidéo, afin de ne pas bloquer la reconnaissance faciale.
+ + +
+
+
◉

Transcription vocale

Mode réellement actif et disponibilité du serveur Wyoming

+
Mode STT——
+

Vérification…

+
Serveur Whisper——
+

Vérification…

+
Wyoming est interrogé avec describe sans lancer de transcription. Une connexion réussie ne garantit pas la qualité STT.
+
+
+
◷

Minuteurs et chronomètres

État enregistré localement, indépendant des appels Hermes

+
Gestionnaire Alfred——
+

Chargement…

+
La présence d’un MCP dans la liste ci-dessous ne prouve pas qu’Hermes l’a appelé : consulter ses logs pour vérifier les outils exécutés.
+
+
+
✧

Maintenance et intégrations

Vérifications complémentaires des nouveautés

+
Redémarrage depuis /config——
+

MCP Autres (heure/date) et Alfred Timers : la grille ci-dessous signale les dossiers détectés dans /opt/hermes. La validation fonctionnelle se fait avec hermes mcp test.

+
+
⚙

MCP Hermes détectés

Présence des répertoires, pas un test de communication des outils MCP

Chargement…

+

+
+
A

Accès administrateur

Connecte-toi pour consulter l'état d'Alfred.

Accès local / réseau privé uniquement.
+
diff --git a/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/manifest.json b/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/manifest.json new file mode 100644 index 0000000..596fd1e --- /dev/null +++ b/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/manifest.json @@ -0,0 +1,55 @@ +{ + "version": 2, + "installed": true, + "created_at": "2026-10-28T20:59:54.153831", + "files": { + "app.py": { + "existed": true, + "before_sha256": "0123a9dcfd8a3d375bd551e9b7f9049042a7b7a13f5d41806e1e76c711aeb2ee", + "after_sha256": "b67c0d6a19ab9bafacc7d8ac7ccf310fe918fbea279fa915ee245927c471a128", + "mode": 420 + }, + "services/alfred_logs_page.py": { + "existed": false, + "before_sha256": null, + "after_sha256": "6900441c4c498d108904175b2d967f0b70a1d43e3384623835dbaa664524ad33", + "mode": 420 + }, + "static/css/alfred-logs-v2.css": { + "existed": false, + "before_sha256": null, + "after_sha256": "e3e427f33be4bf6819bc690e01ba62a0ecef4f68f6d5104fab0e84107e41b5d4", + "mode": 420 + }, + "static/css/alfred-main-v2.css": { + "existed": false, + "before_sha256": null, + "after_sha256": "fc2832b44e03c24fd7bc148291affff0fe578e088e631efde07ea1c1454cc114", + "mode": 420 + }, + "static/js/alfred-logs-v2.js": { + "existed": false, + "before_sha256": null, + "after_sha256": "72c0064225563cc5c773ca5c5ebf5da0429d6a6e1eac55ba61dad9a712322535", + "mode": 420 + }, + "static/js/alfred-main-v2.js": { + "existed": false, + "before_sha256": null, + "after_sha256": "7b182ef547b7a18ac0c5f64d5b69d698c78f507aaf9accb6fb66c229a9ca9bc3", + "mode": 420 + }, + "templates/alfred_logs_v2.html": { + "existed": false, + "before_sha256": null, + "after_sha256": "ee469e2f7c664ec32876a51714c5b23ba425c340032765edc5cf93274288a688", + "mode": 420 + }, + "templates/index.html": { + "existed": true, + "before_sha256": "b469b5c988c4d17218da4db7ecac89f539a119fdfadb041611fade27b3234316", + "after_sha256": "d7961a00886dd3e809c5c5baf2d9e3182bea30fc50d5123c279082cdc1b92c6a", + "mode": 420 + } + } +} \ No newline at end of file diff --git a/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/original/app.py b/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/original/app.py new file mode 100644 index 0000000..5a29ff9 --- /dev/null +++ b/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/original/app.py @@ -0,0 +1,533 @@ +import os, threading, hmac +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from flask import Flask, render_template, request, jsonify, Response +from flask_socketio import SocketIO +from dotenv import load_dotenv +from services.audio import listen_once, speak_async, tts_backend_detect +from services.wakeword import WakeWordListener +from services import admin_console # ALFRED_ADMIN_PAGES_V1 +from services.hermes_client import ask_hermes +from services import alfred_timers, alfred_context_images # ALFRED_TIMERS_CONTEXT_V1 +from services import alfred_features # ALFRED_FEATURES_V45 +from services import audio_settings # ALFRED_AUDIO_SETTINGS_V1 +from services.camera import video_stream +from services import ha_views, alfred_views_controller +from services import camera_settings, ha_weather, weather_outfits + +load_dotenv() + +load_dotenv(os.path.join(os.path.dirname(__file__), "config", "control_center.env"), override=True) # ALFRED_ADMIN_PAGES_V1 +## +from services.audio import get_vosk_model +print("[ALFRED] Préchargement des modèles vocaux...", flush=True) +get_vosk_model(os.environ["WAKE_VOSK_MODEL_PATH"]) +get_vosk_model(os.environ["VOSK_MODEL_PATH"]) +print("[ALFRED] Modèles vocaux prêts.", flush=True) +## +app=Flask(__name__); socketio=SocketIO(app,cors_allowed_origins="*") +STATE={"mode":"idle","title":"Alfred","content":{"type":"text","text":"À votre écoute."},"partial":""} +STATE_LOCK = threading.RLock() +IMAGE_REVISION = 0 # Incrémenté à chaque image envoyée par ComfyUI. +conversation_lock=threading.Lock() +mic_stop_event=threading.Event() +# Service temps autonome (survit aux conversations Hermes) +alfred_timer_store = alfred_timers.register(app, socketio, speak_async, audio_settings) # ALFRED_TIMERS_CONTEXT_V1 +audio_settings.initialize() +audio_settings.update_settings({"mic_gain": 30}) +# Recharge les réglages V4L2 enregistrés, sans interrompre Alfred si la webcam est absente. +camera_settings.restore_saved_settings() + +def emit_state(mode=None, **extra): + with STATE_LOCK: + if mode: STATE["mode"] = mode + STATE.update(extra) + socketio.emit("alfred_state", dict(STATE)) + +def audio_level(level): socketio.emit("audio_level",{"level":max(0,min(float(level),1))}) + +def conversation(score=1.0): + if not conversation_lock.acquire(blocking=False): + return + + try: + emit_state("listening", partial="", transcript="", wake_score=score) + + print("[ALFRED] Début écoute STT", flush=True) + + r = listen_once( + stop_event=mic_stop_event, + on_partial=lambda p: emit_state("listening", partial=p), + on_level=audio_level + ) + + print(f"[ALFRED] Résultat STT : {r.text!r}, erreur : {r.error!r}", flush=True) + + if audio_settings.mic_muted(): + emit_state("idle", partial="") + return + + if r.error: + emit_state( + "error", + content={"type": "text", "text": r.error} + ) + return + + if not r.text: + emit_state("idle", partial="") + return + + emit_state( + "thinking", + partial="", + transcript=r.text, + content={"type": "text", "text": r.text} + ) + + # Vue dynamique : commandes déterministes exécutées sans faire + # dépendre l'affichage des capacités ou sessions Hermes. + visual = None + try: + visual = alfred_views_controller.route(r.text, (STATE.get('content') or {}).get('type')) + except (ha_views.HAError, ValueError) as exc: + print(f"[ALFRED] Vue indisponible : {exc}", flush=True) + visual = ({"type":"text", "text":str(exc)}, str(exc)) + + if visual is not None: + content, speech = visual + with STATE_LOCK: + STATE["content"] = content + socketio.emit("alfred_render", content) + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + emit_state("idle", partial="") + return + + # ALFRED_LISTES_GLASS_V1_VOICE_START + # Commandes simples garanties locales ; jamais de confirmation inventée par Hermes. + try: + _lists_reply = _alfred_handle_list_voice( + r.text, notify=lambda: socketio.emit('alfred_lists_changed', {'source':'voice'})) + except Exception as _lists_error: + print('[ALFRED LISTES] Erreur :', _lists_error, flush=True) + _lists_reply = 'Je n’ai pas pu modifier la liste. Vérifie le stockage d’Alfred.' + if _lists_reply is not None: + _lists_display = {'type':'text', 'text':_lists_reply} + with STATE_LOCK: + STATE['content'] = _lists_display + socketio.emit('alfred_render', _lists_display) + if _lists_reply and not audio_settings.speaker_muted(): + emit_state('speaking') + _lists_done = threading.Event() + speak_async(_lists_reply, on_end=_lists_done.set) + _lists_done.wait(timeout=120) + emit_state('idle', partial='') + return + # ALFRED_LISTES_GLASS_V1_VOICE_END + + print("[ALFRED] Envoi à Hermes", flush=True) + # ComfyUI publie l'image pendant cet appel Hermes. + with STATE_LOCK: + image_revision_before = IMAGE_REVISION + # ALFRED_FEATURES_V45 voice opt-in background mode + if alfred_features.should_run_in_background(r.text): + try: + alfred_features.submit(r.text, source='voice') + emit_state('idle', partial='', content={ + 'type': 'text', + 'text': 'Hermes travaille sur la demande. Le résultat apparaîtra dans les cartes.' + }) + except ValueError as exc: + emit_state('error', content={'type': 'text', 'text': str(exc)}) + return + answer = ask_hermes(r.text) + print(f"[ALFRED] Réponse Hermes : {answer!r}", flush=True) + if isinstance(answer, dict): + speech = str(answer.get("speech") or answer.get("text") or "") + display = answer.get("display") or { + "type": "text", + "text": speech + } + else: + speech = str(answer or "") + display = {"type": "text", "text": speech} + + # Une illustration contextuelle (Wikipédia) peut compléter la réponse Hermes. + # Elle n'interrompt pas la lecture TTS et ne remplace pas la scène centrale. + alfred_context_images.schedule_context_image( + r.text, speech, socketio, + lambda: STATE.get("transcript") == r.text, + ) # ALFRED_TIMERS_CONTEXT_V1 + + # Si le MCP ComfyUI a affiché une image pendant la requête, + # ne pas l'écraser par la réponse textuelle d'Hermes. + with STATE_LOCK: + if IMAGE_REVISION == image_revision_before: + STATE["content"] = display + alfred_features.publish_voice_display(display) + socketio.emit("alfred_render", display) + else: + print("[ALFRED] Image ComfyUI conservée à l'écran", flush=True) + + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + + emit_state("idle", partial="") + + except Exception as exc: + print(f"[ALFRED] Erreur conversation : {exc}", flush=True) + emit_state( + "error", + content={"type": "text", "text": str(exc)} + ) + + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + print("[ALFRED] Wake word réactivé", flush=True) + + + +def on_wake(score): + if audio_settings.mic_muted(): + return + socketio.emit("wake_detected",{"word":os.getenv("WAKE_WORD","alfred"),"score":score}) + threading.Thread(target=conversation,args=(score,),daemon=True).start() + +wake=WakeWordListener(on_wake=on_wake,on_level=lambda l: audio_level(l if STATE["mode"]!="idle" else l*.18)) + +admin_console.register_admin_console(app, wake, audio_settings) # ALFRED_ADMIN_PAGES_V1 +@app.get('/api/audio/settings') +def get_audio_settings(): + return jsonify(audio_settings.get_settings()) + +@app.post('/api/audio/settings') +def set_audio_settings(): + data = request.get_json(silent=True) + try: + old = audio_settings.get_settings() + new = audio_settings.update_settings(data) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except Exception as exc: + return jsonify({'error': f'ALSA : {exc}'}), 500 + if new['mic_muted'] != old['mic_muted']: + if new['mic_muted']: + mic_stop_event.set() + wake.pause() + else: + mic_stop_event.clear() + if not conversation_lock.locked(): + wake.resume() + socketio.emit('audio_settings', new) + return jsonify(new) + +@app.get('/api/camera/settings') +def get_camera_settings(): + try: + return jsonify(camera_settings.get_settings()) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/settings') +def set_camera_settings(): + # Même garde-fou que les commandes calendrier existantes. + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({'error': 'JSON invalide.'}), 400 + try: + result = camera_settings.update_setting(data.get('control'), data.get('value')) + socketio.emit('camera_settings', result) + return jsonify(result) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/reset') +def reset_camera_settings(): + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + try: + result = camera_settings.reset_defaults() + socketio.emit('camera_settings', result) + return jsonify(result) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.get('/api/camera/stream') +def camera_stream(): + # Flux MJPEG à la demande ; ne démarre jamais la webcam en arrière-plan. + try: + frames = video_stream(index=int(os.getenv('CAMERA_INDEX', '0'))) + except (ImportError, ValueError, RuntimeError, OSError) as exc: + return jsonify({'error': str(exc)}), 503 + return Response( + frames, + mimetype='multipart/x-mixed-replace; boundary=frame', + headers={'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff'} + ) + +# ALFRED_FEATURES_V45 init +alfred_features.init_app(app, ask_hermes, socketio=socketio) + +@app.get("/") +def index(): return render_template("index.html",name=os.getenv("ALFRED_NAME","Alfred")) +@app.get("/api/state") +def state(): + with STATE_LOCK: + snapshot = dict(STATE) + return jsonify({**snapshot, "wake": wake.status(), "tts": tts_backend_detect()}) + +@app.post("/api/state") +def set_state(): + data = request.get_json(force=True) + with STATE_LOCK: + STATE.update(data) + socketio.emit("alfred_state", dict(STATE)) + return jsonify({"ok": True}) +@app.post("/api/render") +def render_payload(): + global IMAGE_REVISION + token = os.getenv("ALFRED_RENDER_TOKEN", "").strip() + if token and not hmac.compare_digest( + request.headers.get("X-Alfred-Render-Token", ""), token + ): + return jsonify({"ok": False, "error": "Non autorisé"}), 401 + + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({"ok": False, "error": "JSON objet attendu"}), 400 + if data.get("type") == "image": + image_url = data.get("url") + if not isinstance(image_url, str) or not ( + image_url.startswith(("http://", "https://", "/")) + and not image_url.startswith("//") + ): + return jsonify({"ok": False, "error": "URL image invalide"}), 400 + + with STATE_LOCK: + if data.get("type") == "image": + IMAGE_REVISION += 1 + STATE["content"] = data + socketio.emit("alfred_render", data) + return jsonify({"ok": True, "image_revision": IMAGE_REVISION}) +# --- Agenda / capteurs / caméras Home Assistant --- +def _calendar_range(): + tz = ZoneInfo(os.getenv('ALFRED_TIMEZONE', 'Europe/Paris')) + now = datetime.now(tz) + def parse(name, default): + raw = request.args.get(name) + if not raw:return default + value = datetime.fromisoformat(raw.replace('Z','+00:00')) + if value.tzinfo is None: value = value.replace(tzinfo=tz) + return value.astimezone(tz) + start = parse('start',now.replace(hour=0,minute=0,second=0,microsecond=0)) + end = parse('end',start+timedelta(days=7)) + return start,end + +def _json_error(exc): + code = 400 if isinstance(exc,ValueError) else 502 + return jsonify({'error':str(exc)}),code + +def _writes_enabled(): + return os.getenv('ALFRED_ALLOW_CALENDAR_WRITES','false').lower() in ('1','true','yes') + +def _same_origin_json(): + # POSTs require a custom header and same-origin browser requests. + if request.headers.get('X-Alfred-Request') != '1':return False + origin=request.headers.get('Origin') + if origin and origin.rstrip('/') != request.host_url.rstrip('/'):return False + return True + +@app.get('/api/calendar/calendars') +def calendars_list(): + try:return jsonify({'calendars':ha_views.list_calendars(),'writes_enabled':_writes_enabled()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/calendar/events') +def calendar_events(): + try: + start,end=_calendar_range() + selected=request.args.getlist('calendar_id') + return jsonify(ha_views.get_events(start,end,selected or None)) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/calendar/create') +def calendar_create(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée (ALFRED_ALLOW_CALENDAR_WRITES).'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + tz=ZoneInfo(os.getenv('ALFRED_TIMEZONE','Europe/Paris')) + def read_dt(key): + dt=datetime.fromisoformat(str(data[key]).replace('Z','+00:00')) + return dt.replace(tzinfo=tz) if dt.tzinfo is None else dt.astimezone(tz) + result=ha_views.create_event(str(data.get('calendar_id','')),str(data.get('title','')), + read_dt('start'),read_dt('end'),str(data.get('description') or ''), + str(data.get('location') or ''),bool(data.get('all_day',False))) + from services import alfred_intents + alfred_intents.clear_create() + socketio.emit('calendar_changed', {'type':'created'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.post('/api/calendar/delete') +def calendar_delete(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée.'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + eid=str(data.get('calendar_id',''));uid=str(data.get('uid','')) + start=datetime.fromisoformat(str(data['start'])) + end=datetime.fromisoformat(str(data['end'])) + if start.tzinfo is None or end.tzinfo is None:raise ValueError('Date avec fuseau requise.') + matches=ha_views.get_events(start-timedelta(days=1),end+timedelta(days=1),[eid])['events'] + rid=str(data.get('recurrence_id') or '') + if not any(e['id']==uid and e['start']==start.isoformat() and e['recurrence_id']==rid for e in matches): + return jsonify({'error':'Événement introuvable ou modifié ; actualise le planning.'}),409 + result=ha_views.delete_event(eid,uid,rid or None) + from services import alfred_intents + alfred_intents.clear_delete() + socketio.emit('calendar_changed',{'type':'deleted'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.get('/api/ha/weather') +def ha_weather_data(): + # Read-only route, token always stays on the Alfred server. + try: + q = request.args.get('q', '') + return jsonify(ha_weather.get_weather(q)) + except (ha_views.HAError, ValueError) as exc: + return _json_error(exc) + +@app.get('/api/ha/weather/outfits') +def ha_weather_outfits_data(): + try: + q = request.args.get('q', '') + refresh = request.args.get('refresh_illustration', '').lower() in ('1','true','yes','on') + return jsonify(weather_outfits.build_payload(q=q, date=request.args.get('date', ''), label=request.args.get('label', ''), refresh=refresh)) + except (ha_views.HAError, ValueError, RuntimeError) as exc: + return _json_error(exc) + +@app.get('/api/ha/temperatures') +def ha_temperatures(): + try:return jsonify({'items':ha_views.get_temperatures()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/temperature/history') +def ha_temperature_history(): + try: + eid=request.args.get('entity_id','') + hours=int(request.args.get('hours','24')) + return jsonify({'items':ha_views.temperature_history(eid,hours)}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/cameras') +def ha_cameras(): + try:return jsonify({'items':ha_views.list_cameras()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/camera//snapshot') +def ha_camera_snapshot(entity_id): + try: + binary,mime=ha_views.camera_snapshot(entity_id) + return Response(binary,mimetype=mime,headers={'Cache-Control':'no-store','X-Content-Type-Options':'nosniff'}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/ui/home') +def ui_home(): + with STATE_LOCK: + STATE['content']={'type':'home','text':'À ton écoute.'} + socketio.emit('alfred_render',STATE['content']) + return jsonify({'ok':True}) + +@app.post("/api/audio-level") +def api_audio_level(): audio_level((request.get_json(force=True) or {}).get("level",0)); return jsonify({"ok":True}) +@app.get("/api/wake/status") +def wake_status(): return jsonify(wake.status()) +@app.post("/api/wake/start") +def wake_start(): wake.start(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/wake/stop") +def wake_stop(): wake.stop(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/tts/say") +def tts_say(): + text=(request.get_json(silent=True) or {}).get("text",""); return jsonify({"ok":speak_async(text)}) + + +# ALFRED_MAIN_VISUAL_V1_BACKEND_START +# Raccourcis visuels : même Hermes, même verrou de conversation et même moteur Piper. +def _alfred_dashboard_quick_command(prompt): + if not conversation_lock.acquire(blocking=False): + return False + + def _run(): + try: + wake.pause() + with STATE_LOCK: + _image_revision_before = IMAGE_REVISION + emit_state('thinking', transcript=prompt, partial='', + content={'type':'text', 'text':prompt}) + _answer = ask_hermes(prompt) + if isinstance(_answer, dict): + _speech = str(_answer.get('speech') or _answer.get('text') or '') + _display = _answer.get('display') or {'type':'text','text':_speech} + else: + _speech = str(_answer or '') + _display = {'type':'text','text':_speech} + # Une image ComfyUI publiee pendant Hermes ne doit pas etre effacee. + with STATE_LOCK: + if IMAGE_REVISION == _image_revision_before: + STATE['content'] = _display + socketio.emit('alfred_render', _display) + if _speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _done = threading.Event() + speak_async(_speech, on_end=_done.set) + _done.wait(timeout=120) + emit_state('idle', partial='') + except Exception as _exc: + print('[ALFRED DASHBOARD] Erreur raccourci :', _exc, flush=True) + emit_state('error', content={'type':'text', 'text':str(_exc)}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + + threading.Thread(target=_run, daemon=True, name='alfred-dashboard-command').start() + return True + +from services.alfred_dashboard import register_dashboard as _register_dashboard_visual_v1 +_register_dashboard_visual_v1(app, quick_command=_alfred_dashboard_quick_command) +# ALFRED_MAIN_VISUAL_V1_BACKEND_END + + +# ALFRED_ADMIN_GLASS_MAISON_V1_START +from services.alfred_home_glass import register_home_glass as _alfred_register_home_glass +_alfred_register_home_glass(app) +# ALFRED_ADMIN_GLASS_MAISON_V1_END + + +# ALFRED_LISTES_GLASS_V1_BACKEND_START +from services.alfred_lists import register_lists as _alfred_register_lists +from services.alfred_lists import handle_voice as _alfred_handle_list_voice +_alfred_register_lists(app, socketio) +# ALFRED_LISTES_GLASS_V1_BACKEND_END + +if __name__ == "__main__": + if os.getenv("WAKE_ENABLED","true").lower() in ("1","true","yes","on") and not audio_settings.mic_muted(): wake.start() + socketio.run(app,host=os.getenv("FLASK_HOST","0.0.0.0"),port=int(os.getenv("FLASK_PORT","54321")),allow_unsafe_werkzeug=True) + diff --git a/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/original/templates/index.html b/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/original/templates/index.html new file mode 100644 index 0000000..b042fe5 --- /dev/null +++ b/alfred/app/.alfred_glass_v2_backups/20261028_205954_153666/original/templates/index.html @@ -0,0 +1,195 @@ + + + + + + + {{ name }} + + + + + + + + + + + + + +
+
+ +
+ + En attente +
+ +
+ + ⚙ + ▤ + + + + + + + + + + + +
+
+ + + + + + +
+

Bonjour.

À votre écoute.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/manifest.json b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/manifest.json new file mode 100644 index 0000000..d869f94 --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/manifest.json @@ -0,0 +1,102 @@ +{ + "version": 1, + "installed": true, + "files": { + "app.py": { + "existed": true, + "before_sha256": "b67c0d6a19ab9bafacc7d8ac7ccf310fe918fbea279fa915ee245927c471a128", + "after_sha256": "a96d4d7c5a78d5076934db6033f087cccc32c99f2493cadbffbff6c26cdf0602", + "mode": 420 + }, + "services/admin_console.py": { + "existed": true, + "before_sha256": "f071b69a8c8939d4160a5804c543e41f09c998bdb42d8d945eb5819feaa94481", + "after_sha256": "27eef32a894d9cecc9a77e70708ae58912355c2f7cc48bd7074068950dfc6c4e", + "mode": 420 + }, + "services/alfred_dashboard.py": { + "existed": true, + "before_sha256": "0cbaae4910ced0f677d76495905753cd2facab96e61bbd93d9335e2157c725fd", + "after_sha256": "50465ee91640a9994c225fb099a849da97fde4d18c65c8b77d3ef561419e1204", + "mode": 420 + }, + "services/alfred_identity_page.py": { + "existed": false, + "before_sha256": null, + "after_sha256": "da2f9de1edab74d1477f3f6878a3cbbfd444a2305045fcf76bb70f0e4019cdd4", + "mode": 420 + }, + "services/camera.py": { + "existed": true, + "before_sha256": "2c1bd80a6746b57205ed9b0dd2a25587b14631c00956d72a72c29ddcf60f8d5f", + "after_sha256": "d13070969afa0a1a0398902d757d9d64c1194d7f17600f11b8cefd95f62bbbf0", + "mode": 420 + }, + "services/hermes_client.py": { + "existed": true, + "before_sha256": "02960a81c4ee7daab2a5e6e5937c49f219b66b62a359a2ac8614f5ef384789eb", + "after_sha256": "4cdb587058b706350029132276c00b85fc80c36e4a16dfeb05374c339c1ee547", + "mode": 420 + }, + "static/css/alfred-identity-glass.css": { + "existed": false, + "before_sha256": null, + "after_sha256": "0208f706160cc6c8eae8b059b47cb54bee92305a4c3467de43ab02178d2de2a6", + "mode": 420 + }, + "static/js/alfred-admin.js": { + "existed": true, + "before_sha256": "125398c7aa57d3b21e29b748af4bf95b86c0027b3236bcee8ac94da822fffc7a", + "after_sha256": "e992e3ccf7446a78fec01d84b673781c3d6c024e961a35d2ad0b6f19242707fc", + "mode": 420 + }, + "static/js/alfred-identity-glass.js": { + "existed": false, + "before_sha256": null, + "after_sha256": "33d931c79f58b3853f078312307b9d57e32e72e73a254fd884332ed52b7c8964", + "mode": 420 + }, + "static/js/alfred-name-dynamic.js": { + "existed": false, + "before_sha256": null, + "after_sha256": "404a9bf7fd29c99aab668f7f6598edd03c3bde40d8668a638c034f06636a76f4", + "mode": 420 + }, + "templates/alfred_identity_glass.html": { + "existed": false, + "before_sha256": null, + "after_sha256": "2c912c7d23499f217e4eff4a5c7020344a9f443279443c81a875d394949de154", + "mode": 420 + }, + "templates/alfred_logs_v2.html": { + "existed": true, + "before_sha256": "ee469e2f7c664ec32876a51714c5b23ba425c340032765edc5cf93274288a688", + "after_sha256": "eb3fe9bb05766e5868ab84956ee4660f0ef0d5990c499f710829bd5a40eb035e", + "mode": 420 + }, + "templates/config.html": { + "existed": true, + "before_sha256": "4e6300fcf4b0dafcfc1e196d219734bb35263572c316ad7d010b66d0f6915762", + "after_sha256": "c98c0314923d054e8b56251657a33bb4c949684a94faed0a9a979dfd5ed6c8db", + "mode": 420 + }, + "templates/configuration.html": { + "existed": true, + "before_sha256": "491fa987244881e0b0af3907f21347d1015be890ea30701652b1b88742a534c0", + "after_sha256": "14fe568c1cefe99184deda0165de8bf54a7d1e5ff45d1a2caa17ebfb835cd861", + "mode": 420 + }, + "templates/index.html": { + "existed": true, + "before_sha256": "d7961a00886dd3e809c5c5baf2d9e3182bea30fc50d5123c279082cdc1b92c6a", + "after_sha256": "6b0a7b0d075bb79e3dac909450e35a08cc7b3928a01bf7633cb5811723cd9787", + "mode": 420 + }, + "templates/surveillance.html": { + "existed": true, + "before_sha256": "0bd5a9a17b613c6b02e53146f4ffeb92b8bb94c9e2979d460d2d96de4ecb96cb", + "after_sha256": "9d271013722038be7ef924c4cdc032f3b9e50094a6278d46335eedba7c392bdc", + "mode": 420 + } + } +} \ No newline at end of file diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/app.py b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/app.py new file mode 100644 index 0000000..d2cb0b8 --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/app.py @@ -0,0 +1,539 @@ +import os, threading, hmac +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from flask import Flask, render_template, request, jsonify, Response +from flask_socketio import SocketIO +from dotenv import load_dotenv +from services.audio import listen_once, speak_async, tts_backend_detect +from services.wakeword import WakeWordListener +from services import admin_console # ALFRED_ADMIN_PAGES_V1 +from services.hermes_client import ask_hermes +from services import alfred_timers, alfred_context_images # ALFRED_TIMERS_CONTEXT_V1 +from services import alfred_features # ALFRED_FEATURES_V45 +from services import audio_settings # ALFRED_AUDIO_SETTINGS_V1 +from services.camera import video_stream +from services import ha_views, alfred_views_controller +from services import camera_settings, ha_weather, weather_outfits + +load_dotenv() + +load_dotenv(os.path.join(os.path.dirname(__file__), "config", "control_center.env"), override=True) # ALFRED_ADMIN_PAGES_V1 +## +from services.audio import get_vosk_model +print("[ALFRED] Préchargement des modèles vocaux...", flush=True) +get_vosk_model(os.environ["WAKE_VOSK_MODEL_PATH"]) +get_vosk_model(os.environ["VOSK_MODEL_PATH"]) +print("[ALFRED] Modèles vocaux prêts.", flush=True) +## +app=Flask(__name__); socketio=SocketIO(app,cors_allowed_origins="*") +STATE={"mode":"idle","title":"Alfred","content":{"type":"text","text":"À votre écoute."},"partial":""} +STATE_LOCK = threading.RLock() +IMAGE_REVISION = 0 # Incrémenté à chaque image envoyée par ComfyUI. +conversation_lock=threading.Lock() +mic_stop_event=threading.Event() +# Service temps autonome (survit aux conversations Hermes) +alfred_timer_store = alfred_timers.register(app, socketio, speak_async, audio_settings) # ALFRED_TIMERS_CONTEXT_V1 +audio_settings.initialize() +audio_settings.update_settings({"mic_gain": 30}) +# Recharge les réglages V4L2 enregistrés, sans interrompre Alfred si la webcam est absente. +camera_settings.restore_saved_settings() + +def emit_state(mode=None, **extra): + with STATE_LOCK: + if mode: STATE["mode"] = mode + STATE.update(extra) + socketio.emit("alfred_state", dict(STATE)) + +def audio_level(level): socketio.emit("audio_level",{"level":max(0,min(float(level),1))}) + +def conversation(score=1.0): + if not conversation_lock.acquire(blocking=False): + return + + try: + emit_state("listening", partial="", transcript="", wake_score=score) + + print("[ALFRED] Début écoute STT", flush=True) + + r = listen_once( + stop_event=mic_stop_event, + on_partial=lambda p: emit_state("listening", partial=p), + on_level=audio_level + ) + + print(f"[ALFRED] Résultat STT : {r.text!r}, erreur : {r.error!r}", flush=True) + + if audio_settings.mic_muted(): + emit_state("idle", partial="") + return + + if r.error: + emit_state( + "error", + content={"type": "text", "text": r.error} + ) + return + + if not r.text: + emit_state("idle", partial="") + return + + emit_state( + "thinking", + partial="", + transcript=r.text, + content={"type": "text", "text": r.text} + ) + + # Vue dynamique : commandes déterministes exécutées sans faire + # dépendre l'affichage des capacités ou sessions Hermes. + visual = None + try: + visual = alfred_views_controller.route(r.text, (STATE.get('content') or {}).get('type')) + except (ha_views.HAError, ValueError) as exc: + print(f"[ALFRED] Vue indisponible : {exc}", flush=True) + visual = ({"type":"text", "text":str(exc)}, str(exc)) + + if visual is not None: + content, speech = visual + with STATE_LOCK: + STATE["content"] = content + socketio.emit("alfred_render", content) + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + emit_state("idle", partial="") + return + + # ALFRED_LISTES_GLASS_V1_VOICE_START + # Commandes simples garanties locales ; jamais de confirmation inventée par Hermes. + try: + _lists_reply = _alfred_handle_list_voice( + r.text, notify=lambda: socketio.emit('alfred_lists_changed', {'source':'voice'})) + except Exception as _lists_error: + print('[ALFRED LISTES] Erreur :', _lists_error, flush=True) + _lists_reply = 'Je n’ai pas pu modifier la liste. Vérifie le stockage d’Alfred.' + if _lists_reply is not None: + _lists_display = {'type':'text', 'text':_lists_reply} + with STATE_LOCK: + STATE['content'] = _lists_display + socketio.emit('alfred_render', _lists_display) + if _lists_reply and not audio_settings.speaker_muted(): + emit_state('speaking') + _lists_done = threading.Event() + speak_async(_lists_reply, on_end=_lists_done.set) + _lists_done.wait(timeout=120) + emit_state('idle', partial='') + return + # ALFRED_LISTES_GLASS_V1_VOICE_END + + print("[ALFRED] Envoi à Hermes", flush=True) + # ComfyUI publie l'image pendant cet appel Hermes. + with STATE_LOCK: + image_revision_before = IMAGE_REVISION + # ALFRED_FEATURES_V45 voice opt-in background mode + if alfred_features.should_run_in_background(r.text): + try: + alfred_features.submit(r.text, source='voice') + emit_state('idle', partial='', content={ + 'type': 'text', + 'text': 'Hermes travaille sur la demande. Le résultat apparaîtra dans les cartes.' + }) + except ValueError as exc: + emit_state('error', content={'type': 'text', 'text': str(exc)}) + return + answer = ask_hermes(r.text) + print(f"[ALFRED] Réponse Hermes : {answer!r}", flush=True) + if isinstance(answer, dict): + speech = str(answer.get("speech") or answer.get("text") or "") + display = answer.get("display") or { + "type": "text", + "text": speech + } + else: + speech = str(answer or "") + display = {"type": "text", "text": speech} + + # Une illustration contextuelle (Wikipédia) peut compléter la réponse Hermes. + # Elle n'interrompt pas la lecture TTS et ne remplace pas la scène centrale. + alfred_context_images.schedule_context_image( + r.text, speech, socketio, + lambda: STATE.get("transcript") == r.text, + ) # ALFRED_TIMERS_CONTEXT_V1 + + # Si le MCP ComfyUI a affiché une image pendant la requête, + # ne pas l'écraser par la réponse textuelle d'Hermes. + with STATE_LOCK: + if IMAGE_REVISION == image_revision_before: + STATE["content"] = display + alfred_features.publish_voice_display(display) + socketio.emit("alfred_render", display) + else: + print("[ALFRED] Image ComfyUI conservée à l'écran", flush=True) + + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + + emit_state("idle", partial="") + + except Exception as exc: + print(f"[ALFRED] Erreur conversation : {exc}", flush=True) + emit_state( + "error", + content={"type": "text", "text": str(exc)} + ) + + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + print("[ALFRED] Wake word réactivé", flush=True) + + + +def on_wake(score): + if audio_settings.mic_muted(): + return + socketio.emit("wake_detected",{"word":os.getenv("WAKE_WORD","alfred"),"score":score}) + threading.Thread(target=conversation,args=(score,),daemon=True).start() + +wake=WakeWordListener(on_wake=on_wake,on_level=lambda l: audio_level(l if STATE["mode"]!="idle" else l*.18)) + +admin_console.register_admin_console(app, wake, audio_settings) # ALFRED_ADMIN_PAGES_V1 +@app.get('/api/audio/settings') +def get_audio_settings(): + return jsonify(audio_settings.get_settings()) + +@app.post('/api/audio/settings') +def set_audio_settings(): + data = request.get_json(silent=True) + try: + old = audio_settings.get_settings() + new = audio_settings.update_settings(data) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except Exception as exc: + return jsonify({'error': f'ALSA : {exc}'}), 500 + if new['mic_muted'] != old['mic_muted']: + if new['mic_muted']: + mic_stop_event.set() + wake.pause() + else: + mic_stop_event.clear() + if not conversation_lock.locked(): + wake.resume() + socketio.emit('audio_settings', new) + return jsonify(new) + +@app.get('/api/camera/settings') +def get_camera_settings(): + try: + return jsonify(camera_settings.get_settings()) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/settings') +def set_camera_settings(): + # Même garde-fou que les commandes calendrier existantes. + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({'error': 'JSON invalide.'}), 400 + try: + result = camera_settings.update_setting(data.get('control'), data.get('value')) + socketio.emit('camera_settings', result) + return jsonify(result) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/reset') +def reset_camera_settings(): + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + try: + result = camera_settings.reset_defaults() + socketio.emit('camera_settings', result) + return jsonify(result) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.get('/api/camera/stream') +def camera_stream(): + # Flux MJPEG à la demande ; ne démarre jamais la webcam en arrière-plan. + try: + frames = video_stream(index=int(os.getenv('CAMERA_INDEX', '0'))) + except (ImportError, ValueError, RuntimeError, OSError) as exc: + return jsonify({'error': str(exc)}), 503 + return Response( + frames, + mimetype='multipart/x-mixed-replace; boundary=frame', + headers={'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff'} + ) + +# ALFRED_FEATURES_V45 init +alfred_features.init_app(app, ask_hermes, socketio=socketio) + +@app.get("/") +def index(): return render_template("index.html",name=os.getenv("ALFRED_NAME","Alfred")) +@app.get("/api/state") +def state(): + with STATE_LOCK: + snapshot = dict(STATE) + return jsonify({**snapshot, "wake": wake.status(), "tts": tts_backend_detect()}) + +@app.post("/api/state") +def set_state(): + data = request.get_json(force=True) + with STATE_LOCK: + STATE.update(data) + socketio.emit("alfred_state", dict(STATE)) + return jsonify({"ok": True}) +@app.post("/api/render") +def render_payload(): + global IMAGE_REVISION + token = os.getenv("ALFRED_RENDER_TOKEN", "").strip() + if token and not hmac.compare_digest( + request.headers.get("X-Alfred-Render-Token", ""), token + ): + return jsonify({"ok": False, "error": "Non autorisé"}), 401 + + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({"ok": False, "error": "JSON objet attendu"}), 400 + if data.get("type") == "image": + image_url = data.get("url") + if not isinstance(image_url, str) or not ( + image_url.startswith(("http://", "https://", "/")) + and not image_url.startswith("//") + ): + return jsonify({"ok": False, "error": "URL image invalide"}), 400 + + with STATE_LOCK: + if data.get("type") == "image": + IMAGE_REVISION += 1 + STATE["content"] = data + socketio.emit("alfred_render", data) + return jsonify({"ok": True, "image_revision": IMAGE_REVISION}) +# --- Agenda / capteurs / caméras Home Assistant --- +def _calendar_range(): + tz = ZoneInfo(os.getenv('ALFRED_TIMEZONE', 'Europe/Paris')) + now = datetime.now(tz) + def parse(name, default): + raw = request.args.get(name) + if not raw:return default + value = datetime.fromisoformat(raw.replace('Z','+00:00')) + if value.tzinfo is None: value = value.replace(tzinfo=tz) + return value.astimezone(tz) + start = parse('start',now.replace(hour=0,minute=0,second=0,microsecond=0)) + end = parse('end',start+timedelta(days=7)) + return start,end + +def _json_error(exc): + code = 400 if isinstance(exc,ValueError) else 502 + return jsonify({'error':str(exc)}),code + +def _writes_enabled(): + return os.getenv('ALFRED_ALLOW_CALENDAR_WRITES','false').lower() in ('1','true','yes') + +def _same_origin_json(): + # POSTs require a custom header and same-origin browser requests. + if request.headers.get('X-Alfred-Request') != '1':return False + origin=request.headers.get('Origin') + if origin and origin.rstrip('/') != request.host_url.rstrip('/'):return False + return True + +@app.get('/api/calendar/calendars') +def calendars_list(): + try:return jsonify({'calendars':ha_views.list_calendars(),'writes_enabled':_writes_enabled()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/calendar/events') +def calendar_events(): + try: + start,end=_calendar_range() + selected=request.args.getlist('calendar_id') + return jsonify(ha_views.get_events(start,end,selected or None)) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/calendar/create') +def calendar_create(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée (ALFRED_ALLOW_CALENDAR_WRITES).'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + tz=ZoneInfo(os.getenv('ALFRED_TIMEZONE','Europe/Paris')) + def read_dt(key): + dt=datetime.fromisoformat(str(data[key]).replace('Z','+00:00')) + return dt.replace(tzinfo=tz) if dt.tzinfo is None else dt.astimezone(tz) + result=ha_views.create_event(str(data.get('calendar_id','')),str(data.get('title','')), + read_dt('start'),read_dt('end'),str(data.get('description') or ''), + str(data.get('location') or ''),bool(data.get('all_day',False))) + from services import alfred_intents + alfred_intents.clear_create() + socketio.emit('calendar_changed', {'type':'created'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.post('/api/calendar/delete') +def calendar_delete(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée.'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + eid=str(data.get('calendar_id',''));uid=str(data.get('uid','')) + start=datetime.fromisoformat(str(data['start'])) + end=datetime.fromisoformat(str(data['end'])) + if start.tzinfo is None or end.tzinfo is None:raise ValueError('Date avec fuseau requise.') + matches=ha_views.get_events(start-timedelta(days=1),end+timedelta(days=1),[eid])['events'] + rid=str(data.get('recurrence_id') or '') + if not any(e['id']==uid and e['start']==start.isoformat() and e['recurrence_id']==rid for e in matches): + return jsonify({'error':'Événement introuvable ou modifié ; actualise le planning.'}),409 + result=ha_views.delete_event(eid,uid,rid or None) + from services import alfred_intents + alfred_intents.clear_delete() + socketio.emit('calendar_changed',{'type':'deleted'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.get('/api/ha/weather') +def ha_weather_data(): + # Read-only route, token always stays on the Alfred server. + try: + q = request.args.get('q', '') + return jsonify(ha_weather.get_weather(q)) + except (ha_views.HAError, ValueError) as exc: + return _json_error(exc) + +@app.get('/api/ha/weather/outfits') +def ha_weather_outfits_data(): + try: + q = request.args.get('q', '') + refresh = request.args.get('refresh_illustration', '').lower() in ('1','true','yes','on') + return jsonify(weather_outfits.build_payload(q=q, date=request.args.get('date', ''), label=request.args.get('label', ''), refresh=refresh)) + except (ha_views.HAError, ValueError, RuntimeError) as exc: + return _json_error(exc) + +@app.get('/api/ha/temperatures') +def ha_temperatures(): + try:return jsonify({'items':ha_views.get_temperatures()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/temperature/history') +def ha_temperature_history(): + try: + eid=request.args.get('entity_id','') + hours=int(request.args.get('hours','24')) + return jsonify({'items':ha_views.temperature_history(eid,hours)}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/cameras') +def ha_cameras(): + try:return jsonify({'items':ha_views.list_cameras()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/camera//snapshot') +def ha_camera_snapshot(entity_id): + try: + binary,mime=ha_views.camera_snapshot(entity_id) + return Response(binary,mimetype=mime,headers={'Cache-Control':'no-store','X-Content-Type-Options':'nosniff'}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/ui/home') +def ui_home(): + with STATE_LOCK: + STATE['content']={'type':'home','text':'À ton écoute.'} + socketio.emit('alfred_render',STATE['content']) + return jsonify({'ok':True}) + +@app.post("/api/audio-level") +def api_audio_level(): audio_level((request.get_json(force=True) or {}).get("level",0)); return jsonify({"ok":True}) +@app.get("/api/wake/status") +def wake_status(): return jsonify(wake.status()) +@app.post("/api/wake/start") +def wake_start(): wake.start(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/wake/stop") +def wake_stop(): wake.stop(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/tts/say") +def tts_say(): + text=(request.get_json(silent=True) or {}).get("text",""); return jsonify({"ok":speak_async(text)}) + + +# ALFRED_MAIN_VISUAL_V1_BACKEND_START +# Raccourcis visuels : même Hermes, même verrou de conversation et même moteur Piper. +def _alfred_dashboard_quick_command(prompt): + if not conversation_lock.acquire(blocking=False): + return False + + def _run(): + try: + wake.pause() + with STATE_LOCK: + _image_revision_before = IMAGE_REVISION + emit_state('thinking', transcript=prompt, partial='', + content={'type':'text', 'text':prompt}) + _answer = ask_hermes(prompt) + if isinstance(_answer, dict): + _speech = str(_answer.get('speech') or _answer.get('text') or '') + _display = _answer.get('display') or {'type':'text','text':_speech} + else: + _speech = str(_answer or '') + _display = {'type':'text','text':_speech} + # Une image ComfyUI publiee pendant Hermes ne doit pas etre effacee. + with STATE_LOCK: + if IMAGE_REVISION == _image_revision_before: + STATE['content'] = _display + socketio.emit('alfred_render', _display) + if _speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _done = threading.Event() + speak_async(_speech, on_end=_done.set) + _done.wait(timeout=120) + emit_state('idle', partial='') + except Exception as _exc: + print('[ALFRED DASHBOARD] Erreur raccourci :', _exc, flush=True) + emit_state('error', content={'type':'text', 'text':str(_exc)}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + + threading.Thread(target=_run, daemon=True, name='alfred-dashboard-command').start() + return True + +from services.alfred_dashboard import register_dashboard as _register_dashboard_visual_v1 +_register_dashboard_visual_v1(app, quick_command=_alfred_dashboard_quick_command) +# ALFRED_MAIN_VISUAL_V1_BACKEND_END + + +# ALFRED_ADMIN_GLASS_MAISON_V1_START +from services.alfred_home_glass import register_home_glass as _alfred_register_home_glass +_alfred_register_home_glass(app) +# ALFRED_ADMIN_GLASS_MAISON_V1_END + + +# ALFRED_LISTES_GLASS_V1_BACKEND_START +from services.alfred_lists import register_lists as _alfred_register_lists +from services.alfred_lists import handle_voice as _alfred_handle_list_voice +_alfred_register_lists(app, socketio) +# ALFRED_LISTES_GLASS_V1_BACKEND_END + + +# ALFRED_GLASS_V2_BACKEND_START +from services.alfred_logs_page import register_logs_page as _alfred_register_logs_v2 +_alfred_register_logs_v2(app) +# ALFRED_GLASS_V2_BACKEND_END + +if __name__ == "__main__": + if os.getenv("WAKE_ENABLED","true").lower() in ("1","true","yes","on") and not audio_settings.mic_muted(): wake.start() + socketio.run(app,host=os.getenv("FLASK_HOST","0.0.0.0"),port=int(os.getenv("FLASK_PORT","54321")),allow_unsafe_werkzeug=True) + diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/admin_console.py b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/admin_console.py new file mode 100644 index 0000000..3b9716f --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/admin_console.py @@ -0,0 +1,482 @@ +"""Authenticated local configuration and health UI for Alfred. + +Registration is additive and doesn't replace the existing audio, camera, HA, +weather or Hermes endpoints. Passwords are provisioned locally via CLI. +""" +from __future__ import annotations + +import functools +import hmac +import json +import os +import re +import secrets +import shutil +import socket +import subprocess +import threading +import time +from datetime import datetime, timedelta, timezone +from pathlib import Path +from urllib.parse import urlsplit + +import requests +from dotenv import dotenv_values +from flask import Blueprint, current_app, jsonify, render_template, request, session +from werkzeug.security import check_password_hash + +ROOT = Path(__file__).resolve().parents[1] +AUTH_FILE = ROOT / 'config' / 'admin_auth.json' +GENERAL_ENV = ROOT / 'config' / 'control_center.env' +WEATHER_ENV = ROOT / 'config' / 'weather_outfits.env' +CONFIG_LOCK = threading.RLock() +LOGIN_LOCK = threading.Lock() +LOGIN_ATTEMPTS = {} + +# Only these keys are writable; no token, command, system file or shell input. +FIELDS = { + 'ALFRED_NAME': ('general', 'text'), + 'WAKE_WORD': ('general', 'wake'), + 'PIPER_VOICE': ('general', 'voice'), + 'ALFRED_OLLAMA_URL': ('general', 'url'), + 'ALFRED_COMFYUI_URL': ('weather', 'url'), + 'HA_WEATHER_ENTITY': ('general', 'entity'), + 'ALFRED_WEATHER_ILLUSTRATION_ATTEMPTS': ('weather', 'attempts'), + 'ALFRED_WEATHER_ILLUSTRATION_TARGET_SCORE': ('weather', 'score'), + 'ALFRED_WEATHER_DISPLAY_SIZE': ('weather', 'size'), + 'ALFRED_WEATHER_REVIEW': ('weather', 'boolean'), + 'ALFRED_WEATHER_REFERENCE_MODE': ('weather', 'reference_mode'), + 'ALFRED_WEATHER_REFERENCE_DENOISE': ('weather', 'denoise'), + 'ALFRED_WEATHER_PRESERVE_FACE': ('weather', 'boolean'), + 'ALFRED_WEATHER_ACCESSORY_LIMIT': ('weather', 'accessory'), +} +DEFAULTS = { + 'ALFRED_NAME': 'Alfred', 'WAKE_WORD': 'alfred', + 'PIPER_VOICE': 'fr_FR-tom-medium', 'ALFRED_OLLAMA_URL': '', + 'ALFRED_COMFYUI_URL': '', 'HA_WEATHER_ENTITY': '', + 'ALFRED_WEATHER_ILLUSTRATION_ATTEMPTS': '2', + 'ALFRED_WEATHER_ILLUSTRATION_TARGET_SCORE': '8.6', + 'ALFRED_WEATHER_DISPLAY_SIZE': '350', + 'ALFRED_WEATHER_REVIEW': 'true', + 'ALFRED_WEATHER_REFERENCE_MODE': 'auto', + 'ALFRED_WEATHER_REFERENCE_DENOISE': '0.56', + 'ALFRED_WEATHER_PRESERVE_FACE': 'true', + 'ALFRED_WEATHER_ACCESSORY_LIMIT': '1', +} + + +# ALFRED_ADMIN_SURVEILLANCE_V2 : champs du moteur STT existant. +FIELDS.update({ + 'STT_ENGINE': ('general', 'stt_engine'), + 'WYOMING_WHISPER_HOST': ('general', 'stt_host'), + 'WYOMING_WHISPER_PORT': ('general', 'stt_port'), + 'WYOMING_WHISPER_LANGUAGE': ('general', 'stt_language'), + 'WYOMING_WHISPER_TIMEOUT': ('general', 'stt_timeout'), + 'WYOMING_WHISPER_CONNECT_TIMEOUT': ('general', 'stt_connect_timeout'), + 'STT_MAX_AUDIO_SECONDS': ('general', 'stt_max_audio'), +}) +DEFAULTS.update({ + 'STT_ENGINE': 'vosk', + 'WYOMING_WHISPER_HOST': '192.168.1.13', + 'WYOMING_WHISPER_PORT': '10300', + 'WYOMING_WHISPER_LANGUAGE': 'fr', + 'WYOMING_WHISPER_TIMEOUT': '20', + 'WYOMING_WHISPER_CONNECT_TIMEOUT': '3', + 'STT_MAX_AUDIO_SECONDS': '30', +}) +from services.admin_extensions_v2 import extend_health as _admin_v2_extend_health + + +def _auth_config(): + try: + return json.loads(AUTH_FILE.read_text(encoding='utf-8')) + except (OSError, ValueError): + return {} + + +def _authenticated(): + config = _auth_config() + password_hash = config.get('password_hash', '') + if not password_hash: + return False + # Changing the admin password instantly invalidates old signed sessions. + expected_version = __import__('hashlib').sha256(password_hash.encode()).hexdigest()[:24] + return bool(session.get('alfred_admin') and + hmac.compare_digest(str(session.get('auth_version', '')), expected_version)) + + +def _valid_csrf(): + token = request.headers.get('X-Alfred-CSRF', '') + expected = session.get('alfred_csrf', '') + return bool(token and expected and hmac.compare_digest(token, expected)) + + +def _trusted_request(): + origin = request.headers.get('Origin', '') + if origin: + parsed = urlsplit(origin) + if parsed.netloc.lower() != request.host.lower() or parsed.scheme not in ('http', 'https'): + return False + if request.headers.get('Sec-Fetch-Site', '').lower() == 'cross-site': + return False + return True + + +def _require_admin(handler): + @functools.wraps(handler) + def wrapped(*args, **kwargs): + if not _authenticated(): + return jsonify(error='Authentification administrateur nécessaire.'), 401 + if request.method != 'GET' and (not _valid_csrf() or not _trusted_request()): + return jsonify(error='Protection anti-CSRF : requête refusée.'), 403 + return handler(*args, **kwargs) + return wrapped + + +def _validate(key, value): + if key not in FIELDS: + raise ValueError(f'Paramètre non autorisé : {key}.') + kind = FIELDS[key][1] + if kind == 'boolean': + if value not in (True, False, 'true', 'false'): + raise ValueError(f'{key} doit être vrai ou faux.') + return 'true' if value in (True, 'true') else 'false' + if isinstance(value, bool): + raise ValueError(f'Valeur invalide pour {key}.') + value = str(value).strip() + if any(c in value for c in ('\r', '\n', '\x00', '"', "'", '\\', '$', '`')): + raise ValueError(f'Caractères interdits pour {key}.') + # ALFRED_ADMIN_SURVEILLANCE_V2: stricte validation des paramètres Whisper. + if kind == 'stt_engine': + if value not in ('vosk', 'wyoming'): + raise ValueError('Moteur STT attendu : vosk ou wyoming.') + elif kind == 'stt_host': + if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9.\-]{0,252}', value): + raise ValueError('Hôte Whisper invalide : indiquer une IP ou un nom DNS, sans protocole.') + elif kind == 'stt_port': + if not value.isdecimal() or not 1 <= int(value) <= 65535: + raise ValueError('Port Whisper invalide (1 à 65535).') + value = str(int(value)) + elif kind == 'stt_language': + if not re.fullmatch(r'[a-z]{2}', value.lower()): + raise ValueError('Langue Whisper invalide (exemple : fr).') + value = value.lower() + elif kind in ('stt_timeout', 'stt_connect_timeout'): + try: + number = float(value) + except ValueError: + raise ValueError('Délai Whisper non numérique.') + bounds = (5, 90) if kind == 'stt_timeout' else (1, 15) + if not bounds[0] <= number <= bounds[1]: + raise ValueError(f'Délai Whisper entre {bounds[0]} et {bounds[1]} secondes.') + value = str(number).rstrip('0').rstrip('.') if '.' in str(number) else str(number) + elif kind == 'stt_max_audio': + if not value.isdecimal() or not 5 <= int(value) <= 60: + raise ValueError('Enregistrement entre 5 et 60 secondes.') + value = str(int(value)) + elif kind == 'text': + if not 2 <= len(value) <= 30: + raise ValueError('Le nom doit contenir entre 2 et 30 caractères.') + elif kind == 'wake': + if not re.fullmatch(r'[A-Za-zÀ-ÖØ-öø-ÿ]{2,22}', value): + raise ValueError('Le mot-clé Vosk doit être un mot simple de 2 à 22 lettres.') + value = value.lower() + elif kind == 'voice': + if not re.fullmatch(r'[A-Za-z0-9_.-]{2,96}', value): + raise ValueError('Identifiant de voix Piper invalide.') + elif kind == 'url': + if not value: + return '' + parsed = urlsplit(value) + if (parsed.scheme not in ('http', 'https') or not parsed.hostname or + parsed.username or parsed.password or parsed.path not in ('', '/') or + parsed.query or parsed.fragment): + raise ValueError(f'{key} attend une URL http(s) de serveur sans chemin ni identifiants.') + if parsed.port and not 1 <= parsed.port <= 65535: + raise ValueError('Port réseau invalide.') + value = value.rstrip('/') + elif kind == 'entity': + if value and not re.fullmatch(r'weather\.[a-z0-9_]{2,80}', value): + raise ValueError('Attendu : weather.identifiant ou une valeur vide.') + elif kind == 'attempts': + if value not in ('1', '2', '3'): + raise ValueError('Nombre d’essais autorisé : 1, 2 ou 3 par personnage.') + elif kind == 'score': + value = str(float(value)) + if not 1 <= float(value) <= 10: + raise ValueError('Score entre 1 et 10.') + elif kind == 'size': + if value not in ('250', '300', '350', '400', '450', '500'): + raise ValueError('Taille autorisée : 250, 300, 350, 400, 450 ou 500 px.') + elif kind == 'denoise': + value = str(float(value)) + if not 0.25 <= float(value) <= 0.85: + raise ValueError('Denoise entre 0.25 et 0.85.') + elif kind == 'reference_mode': + if value not in ('auto', 'required', 'off'): + raise ValueError('Mode référence : auto, required ou off.') + elif kind == 'accessory': + if value not in ('0', '1', '2'): + raise ValueError('Accessoires visibles autorisés : 0, 1 ou 2.') + return value + + +def _read_config(): + local = dict(dotenv_values(GENERAL_ENV)) if GENERAL_ENV.exists() else {} + weather = dict(dotenv_values(WEATHER_ENV)) if WEATHER_ENV.exists() else {} + effective = {} + for key, (group, _) in FIELDS.items(): + env_file = local if group == 'general' else weather + # File values take precedence on the next restart. Show saved values. + effective[key] = str(env_file.get(key) if env_file.get(key) is not None + else os.getenv(key, DEFAULTS[key])) + return effective + + +def _save_env(path: Path, changes: dict): + path.parent.mkdir(parents=True, exist_ok=True) + existing = path.read_text(encoding='utf-8') if path.exists() else '' + lines = existing.splitlines() + updated = set() + result = [] + for line in lines: + match = re.match(r'^\s*([A-Z][A-Z0-9_]*)\s*=', line) + if match and match.group(1) in changes: + name = match.group(1) + if name not in updated: + result.append(f'{name}={changes[name]}') + updated.add(name) + else: + result.append(line) + for key, value in changes.items(): + if key not in updated: + result.append(f'{key}={value}') + tmp = path.with_name(path.name + f'.{secrets.token_hex(4)}.tmp') + try: + with tmp.open('w', encoding='utf-8') as fp: + fp.write('\n'.join(result).rstrip() + '\n') + fp.flush() + os.fsync(fp.fileno()) + os.chmod(tmp, 0o600) + tmp.replace(path) + finally: + tmp.unlink(missing_ok=True) + + +def _probe_http(url, *, token=None, timeout=2): + start = time.monotonic() + try: + response = requests.get(url, headers={'Authorization': f'Bearer {token}'} if token else {}, + timeout=timeout, allow_redirects=False) + if 200 <= response.status_code < 300: + state = 'ok' + detail = f'HTTP {response.status_code}' + elif response.status_code in (401, 403): + state, detail = 'warning', f'Connexion établie, accès refusé ({response.status_code})' + else: + state, detail = 'warning', f'HTTP {response.status_code}' + except requests.RequestException: + state, detail = 'down', 'Aucune réponse HTTP' + return {'state': state, 'detail': detail, 'latency_ms': round((time.monotonic()-start)*1000)} + + +def _probe_tcp(host, port, timeout=1.5): + try: + with socket.create_connection((host, int(port)), timeout=timeout): + return {'state': 'ok', 'detail': f'Port {port} accessible'} + except (OSError, ValueError): + return {'state': 'down', 'detail': f'Port {port} inaccessible'} + + +def _weather_comfy_url(): + # Respect the V5 precedence if possible, without reaching into credentials. + settings = dict(dotenv_values(WEATHER_ENV)) if WEATHER_ENV.exists() else {} + return (_read_config()['ALFRED_COMFYUI_URL'] or settings.get('ALFRED_COMFYUI_URL') or + os.getenv('ALFRED_COMFYUI_URL') or os.getenv('COMFYUI_URL') or + dict(dotenv_values('/opt/hermes/MCP_comfyui/.env')).get('COMFYUI_URL') or '') + + +def _health(wake, audio_settings): + now = datetime.now(timezone.utc).isoformat() + audio = audio_settings.get_settings() + try: + wake_state = wake.status() + except Exception: + wake_state = {'running': False, 'paused': True, 'error': 'Impossible de lire Vosk'} + cam_index = int(os.getenv('CAMERA_INDEX', '0')) + camera_path = Path(f'/dev/video{cam_index}') + camera_ok = camera_path.exists() and os.access(camera_path, os.R_OK) + # Checking /dev/videoN doesn't open the device: no conflict with facial recognition. + camera = {'state':'ok' if camera_ok else 'warning', + 'detail':f'{camera_path} détectée' if camera_ok else f'{camera_path} indisponible ou permissions insuffisantes'} + mute_micro = audio.get('mic_muted', False) + microphone = {'state':'muted' if mute_micro else ('ok' if wake_state.get('running') else 'warning'), + 'detail':'Micro coupé volontairement' if mute_micro else ('Détection Vosk active' if wake_state.get('running') and not wake_state.get('paused') else 'Vosk non disponible ou en pause')} + loudspeaker = {'state':'muted' if audio.get('speaker_muted') else 'ok', + 'detail':'Son coupé' if audio.get('speaker_muted') else f"Volume {audio.get('volume', '?')} %"} + + config = _read_config() + comfy = _weather_comfy_url() + ollama = config.get('ALFRED_OLLAMA_URL') or os.getenv('OLLAMA_HOST') or os.getenv('OLLAMA_URL') + ha_base = os.getenv('HA_URL', '').rstrip('/') + tts_host = os.getenv('PIPER_HOST', '').strip() + tts_port = os.getenv('PIPER_PORT', '10200') + services = {'alfred': {'state':'ok', 'detail':'Serveur Flask actif'}, + 'hermes_cli': {'state':'ok' if shutil.which('hermes') else 'warning', + 'detail':'CLI détectée' if shutil.which('hermes') else 'CLI non détectée dans le PATH du service'}} + services['hermes_dashboard'] = _probe_tcp('127.0.0.1', 9119) + services['hermes_dashboard']['label'] = 'Tableau de bord (optionnel)' + services['comfyui'] = _probe_http(comfy.rstrip('/') + '/system_stats') if comfy else {'state':'unknown','detail':'URL non configurée'} + services['ollama'] = _probe_http(ollama.rstrip('/') + '/api/tags') if ollama else {'state':'unknown','detail':'URL non configurée'} + services['home_assistant'] = _probe_http(ha_base + '/api/', token=os.getenv('HA_TOKEN')) if ha_base else {'state':'unknown','detail':'URL non configurée'} + services['wyoming_piper'] = _probe_tcp(tts_host, tts_port) if tts_host else {'state':'unknown','detail':'Wyoming Piper non configuré (Piper local possible)'} + + mcp_path = Path('/opt/hermes') + mcps = {} + for slug, label in [('MCP_home_assistant','Home Assistant'), ('MCP_comfyui','ComfyUI'), ('MCP_face_recognition','Reconnaissance faciale'),('MCP_wazuh','Wazuh')]: + found = (mcp_path / slug).exists() + mcps[slug] = {'state':'ok' if found else 'unknown', 'detail':'Dossier présent (connexion non vérifiée)' if found else 'Dossier absent', 'label':label} + + disk = shutil.disk_usage(ROOT) + mem = {} + try: + info = {} + for line in Path('/proc/meminfo').read_text().splitlines(): + k, _, v = line.partition(':') + if k in ('MemTotal','MemAvailable'): + info[k] = int(v.strip().split()[0]) * 1024 + if info.get('MemTotal'): + mem = {'total': info['MemTotal'], 'used': info['MemTotal'] - info.get('MemAvailable', 0)} + except (OSError, ValueError): + pass + result = {'time': now, 'audio':audio, 'wake':wake_state, 'camera':camera, + 'microphone':microphone, 'speaker':loudspeaker, 'services':services, 'mcps':mcps, + 'system':{'disk_total':disk.total,'disk_used':disk.used, 'memory':mem, + 'load_1m':round(os.getloadavg()[0], 2) if hasattr(os,'getloadavg') else None}} + return _admin_v2_extend_health(result, _read_config) + + +def register_admin_console(app, wake, audio_settings): + """Register pages/API on the existing Flask app. Call exactly once.""" + secret_file = ROOT / 'config' / 'admin_secret.key' + if not secret_file.exists(): + raise RuntimeError('Secret administrateur absent ; exécuter install_patch.py.') + app.secret_key = secret_file.read_bytes() + app.config.update(SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='Strict', + SESSION_COOKIE_SECURE=os.getenv('ALFRED_ADMIN_HTTPS', 'false').lower() == 'true', + PERMANENT_SESSION_LIFETIME=timedelta(hours=8)) + + bp = Blueprint('alfred_admin', __name__) + + @bp.get('/config') + @bp.get('/config.html') + def config_page(): + return render_template('config.html') + + @bp.get('/surveillance') + @bp.get('/surveillance.html') + def surveillance_page(): + return render_template('surveillance.html') + + @bp.get('/api/control/auth') + def auth_status(): + authed = _authenticated() + return jsonify(authenticated=authed, configured=bool(_auth_config().get('password_hash')), + csrf=session.get('alfred_csrf') if authed else None) + + @bp.post('/api/control/login') + def login(): + if not _trusted_request() or not request.is_json: + return jsonify(error='Requête refusée.'), 403 + data = request.get_json(silent=True) or {} + password = data.get('password', '') + if not isinstance(password, str) or len(password) > 500: + return jsonify(error='Mot de passe incorrect.'), 400 + now = time.monotonic() + client = request.remote_addr or 'unknown' + with LOGIN_LOCK: + events = [x for x in LOGIN_ATTEMPTS.get(client, []) if x > now - 900] + LOGIN_ATTEMPTS[client] = events + if len(events) >= 5: + return jsonify(error='Trop de tentatives : réessaie plus tard.'), 429 + config = _auth_config() + password_hash = config.get('password_hash') + if not password_hash: + return jsonify(error='Définis d’abord le mot de passe avec set_admin_password.py.'), 503 + if not check_password_hash(password_hash, password): + with LOGIN_LOCK: + LOGIN_ATTEMPTS[client].append(now) + return jsonify(error='Mot de passe incorrect.'), 401 + with LOGIN_LOCK: + LOGIN_ATTEMPTS.pop(client, None) + session.clear() + session.permanent = True + session['alfred_admin'] = True + session['auth_version'] = __import__('hashlib').sha256(password_hash.encode()).hexdigest()[:24] + session['alfred_csrf'] = secrets.token_urlsafe(32) + return jsonify(authenticated=True, csrf=session['alfred_csrf']) + + @bp.post('/api/control/logout') + @_require_admin + def logout(): + session.clear() + return jsonify(ok=True) + + @bp.get('/api/control/config') + @_require_admin + def config_get(): + return jsonify(values=_read_config(), note='La plupart des réglages nécessitent un redémarrage.') + + @bp.post('/api/control/config') + @_require_admin + def config_update(): + data = request.get_json(silent=True) + if not isinstance(data, dict) or len(data) > len(FIELDS): + return jsonify(error='JSON invalide.'), 400 + try: + normalized = {key: _validate(key, value) for key, value in data.items()} + except (ValueError, TypeError, OverflowError) as exc: + return jsonify(error=str(exc)), 400 + general = {k:v for k,v in normalized.items() if FIELDS[k][0]=='general'} + weather = {k:v for k,v in normalized.items() if FIELDS[k][0]=='weather'} + try: + with CONFIG_LOCK: + if general: _save_env(GENERAL_ENV, general) + if weather: _save_env(WEATHER_ENV, weather) + except OSError as exc: + return jsonify(error=f'Impossible d’enregistrer : {exc}'), 500 + return jsonify(ok=True, restart_required=True, values=_read_config()) + + @bp.get('/api/control/health') + @_require_admin + def health(): + return jsonify(_health(wake, audio_settings)) + + @bp.post('/api/control/restart') + @_require_admin + def restart(): + if os.getenv('ALFRED_ALLOW_WEB_RESTART', 'false').lower() not in ('true', '1', 'yes'): + return jsonify(error='Redémarrage Web désactivé. Activer ALFRED_ALLOW_WEB_RESTART=true (voir README).'), 403 + systemctl = shutil.which('systemctl') + runner = shutil.which('systemd-run') + if not systemctl or not runner: + return jsonify(error='systemd-run ou systemctl absent. Redémarrer depuis le terminal.'), 503 + # Schedule restart in a DIFFERENT systemd unit/cgroup so stopping Alfred + # cannot kill the restart command mid-flight. + command = [runner, '--unit=alfred-web-restart', '--on-active=2s', '--collect', + systemctl, 'restart', 'alfred.service'] + if os.geteuid() != 0: + command = ['sudo', '-n', *command] + def do_restart(): + time.sleep(0.5) + try: + result = subprocess.run(command, capture_output=True, text=True, timeout=12, check=False) + if result.returncode: + app.logger.error('Planification du redémarrage refusée (%s): %s', + result.returncode, result.stderr[:350]) + except (OSError, subprocess.TimeoutExpired) as exc: + app.logger.error('Planification du redémarrage échouée: %s', exc) + threading.Thread(target=do_restart, daemon=True).start() + return jsonify(accepted=True, message='Demande de redémarrage transmise ; contrôler le retour du service.') , 202 + + app.register_blueprint(bp) diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/alfred_dashboard.py b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/alfred_dashboard.py new file mode 100644 index 0000000..ceae310 --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/alfred_dashboard.py @@ -0,0 +1,208 @@ +"""Alfred dashboard V1 — safe, read-only aggregated statuses + allowed quick actions. + +No credentials, IP addresses, entity ids or full HA states are sent to the browser. +The dashboard probes are advisory; MCP success is never inferred from a folder. +""" +from __future__ import annotations + +from datetime import datetime +import json +import os +from pathlib import Path +import shutil +import threading +import time +from urllib.error import HTTPError, URLError +from urllib.parse import urlparse +from urllib.request import Request, urlopen +from zoneinfo import ZoneInfo + +from flask import Blueprint, jsonify, request + +_CACHE = {'expires': 0.0, 'value': None} +_LOCK = threading.Lock() + + +def _json_get(url: str, token: str = '', timeout: float = 2.0): + parsed = urlparse(url) + if parsed.scheme not in ('http', 'https') or not parsed.hostname: + raise ValueError('URL non HTTP(S)') + headers = {'Accept': 'application/json', 'User-Agent': 'Alfred-Dashboard/1'} + if token: + headers['Authorization'] = 'Bearer ' + token + req = Request(url, headers=headers) + with urlopen(req, timeout=timeout) as response: + return json.loads(response.read(15_000_000).decode('utf-8')) + + +def _stat_local(): + disk = shutil.disk_usage(Path(__file__).resolve().parents[1]) + percent = round(disk.used * 100 / disk.total) if disk.total else None + mem_pct = None + try: + m = {} + for line in Path('/proc/meminfo').read_text().splitlines(): + key, _, value = line.partition(':') + if key in ('MemTotal', 'MemAvailable'): + m[key] = int(value.strip().split()[0]) + if m.get('MemTotal'): + mem_pct = round((m['MemTotal'] - m.get('MemAvailable', 0)) * 100 / m['MemTotal']) + except (OSError, ValueError, ZeroDivisionError): + pass + try: + load_1m = round(os.getloadavg()[0], 2) + except (OSError, AttributeError): + load_1m = None + return {'disk_percent': percent, 'memory_percent': mem_pct, 'load_1m': load_1m} + + +def _safe_float(value): + try: + n = float(value) + return round(n, 1) if abs(n) < 10000 else None + except (TypeError, ValueError): + return None + + +def _ha_data(): + base = os.getenv('HA_URL', '').strip().rstrip('/') + token = os.getenv('HA_TOKEN', '').strip() + if not (base and token): + return {'available': False, 'detail': 'Home Assistant non configuré'}, {'available': False} + try: + states = _json_get(base + '/api/states', token, timeout=2.5) + if not isinstance(states, list): + raise ValueError('États Home Assistant invalides') + except (ValueError, HTTPError, URLError, OSError, TimeoutError): + return {'available': False, 'detail': 'Home Assistant indisponible'}, {'available': False} + + lights = sum(x.get('state') == 'on' for x in states if str(x.get('entity_id','')).startswith('light.')) + covers = sum(x.get('state') == 'closed' for x in states if str(x.get('entity_id','')).startswith('cover.')) + t = None + candidates = [] + for x in states: + eid = str(x.get('entity_id','')) + attrs = x.get('attributes') or {} + if eid.startswith('climate.'): + v = _safe_float(attrs.get('current_temperature')) + if v is not None: candidates.append((0 if 'salon' in eid.lower() else 2, v)) + elif eid.startswith('sensor.') and attrs.get('device_class') == 'temperature': + v = _safe_float(x.get('state')) + if v is not None: candidates.append((1 if 'salon' in eid.lower() else 3, v)) + if candidates: t = sorted(candidates, key=lambda x: x[0])[0][1] + + condition_map = { + 'sunny':'Ensoleillé', 'clear-night':'Ciel dégagé', 'partlycloudy':'Éclaircies', + 'cloudy':'Nuageux', 'rainy':'Pluvieux', 'pouring':'Fortes pluies', + 'snowy':'Neigeux', 'fog':'Brouillard', 'windy':'Venteux', + 'lightning':'Orageux', 'hail':'Grêle', 'exceptional':'Conditions exceptionnelles', + } + weather = {'available': False} + weather_entities = [x for x in states if str(x.get('entity_id','')).startswith('weather.')] + selected_weather = (os.getenv('HA_WEATHER_ENTITY') or os.getenv('WEATHER_ENTITY') or '').strip() + if selected_weather: + weather_entities.sort(key=lambda item: item.get('entity_id') != selected_weather) + if weather_entities: + x = weather_entities[0] + attrs = x.get('attributes') or {} + condition = str(x.get('state') or '') + weather = { + 'available': True, + 'temperature': _safe_float(attrs.get('temperature')), + 'condition': condition_map.get(condition, 'Conditions non précisées'), + 'condition_id': condition, + 'unit': str(attrs.get('temperature_unit') or '°C')[:6], + } + return {'available': True, 'lights_on': lights, 'covers_closed': covers, + 'indoor_temperature': t}, weather + + +def _probe(url: str): + if not url: + return 'unconfigured' + try: + _json_get(url, timeout=1.8) + return 'ok' + except (ValueError, HTTPError, URLError, OSError, TimeoutError): + return 'offline' + + +def _services(): + ollama = (os.getenv('ALFRED_OLLAMA_URL') or os.getenv('OLLAMA_HOST') or + os.getenv('OLLAMA_URL') or '').strip().rstrip('/') + comfy = (os.getenv('COMFYUI_URL') or os.getenv('ALFRED_COMFYUI_URL') or '').strip().rstrip('/') + hermes = bool(shutil.which('hermes') or Path('/root/.local/bin/hermes').is_file() + or Path('/opt/hermes/venv/bin/hermes').is_file()) + return { + 'ollama': _probe(ollama + '/api/tags') if ollama else 'unconfigured', + 'comfyui': _probe(comfy + '/system_stats') if comfy else 'unconfigured', + # Presence of a binary is not proof of a healthy agent process. + 'hermes': 'installed' if hermes else 'unknown', + } + + +def dashboard_snapshot(): + now = time.monotonic() + with _LOCK: + if _CACHE['value'] is not None and now < _CACHE['expires']: + return _CACHE['value'] + home, weather = _ha_data() + result = {'home': home, 'weather': weather, + 'system': _stat_local(), 'services': _services(), + 'updated_at': datetime.now(ZoneInfo('Europe/Paris')).isoformat()} + _CACHE.update(value=result, expires=time.monotonic() + 25) + return result + + +# Only these preset actions can invoke Hermes from the browser. +_QUICK_ACTIONS = { + 'proxmox': 'Vérifie l’état de mes serveurs Proxmox avec les outils dont tu disposes. Si tu ne peux pas y accéder, indique-le clairement, sans inventer de résultat.', + 'wazuh': 'Consulte les alertes de sécurité Wazuh récentes avec tes outils. Si l’accès échoue, dis-le clairement et ne fabrique aucune alerte.', +} + + +def _trusted_local_request(): + if request.headers.get('X-Alfred-Request') != '1' or not request.is_json: + return False + origin = request.headers.get('Origin', '') + # Browsers use Origin for JSON POST; allow trusted headless local clients + # only through loopback when Origin is absent. + if origin: + o = urlparse(origin) + return o.scheme in ('http','https') and o.netloc == request.host + return request.remote_addr in ('127.0.0.1', '::1') + + +def register_dashboard(app, quick_command=None): + bp = Blueprint('alfred_home_visual_v1', __name__) + + @bp.get('/api/alfred/dashboard') + def summary(): + return jsonify(dashboard_snapshot()) + + @bp.post('/api/alfred/dashboard/command') + def action(): + if not _trusted_local_request(): + return jsonify(ok=False, error='Requête non autorisée.'), 403 + data = request.get_json(silent=True) or {} + key = data.get('action', '') + if key == 'comfyui': + description = data.get('description', '') + if not isinstance(description, str) or not (4 <= len(description.strip()) <= 450): + return jsonify(ok=False, error='Description attendue (4 à 450 caractères).'), 400 + prompt = 'Génère une image avec ComfyUI correspondant à cette description : ' + description.strip() + else: + prompt = _QUICK_ACTIONS.get(key) + if prompt is None: + return jsonify(ok=False, error='Action inconnue.'), 400 + if quick_command is None: + return jsonify(ok=False, error='Commande interactive indisponible.'), 503 + try: + ok = bool(quick_command(prompt)) + except Exception: + return jsonify(ok=False, error='Impossible de soumettre la commande.'), 500 + if not ok: + return jsonify(ok=False, error='Alfred traite déjà une autre demande.'), 409 + return jsonify(ok=True, queued=True), 202 + + app.register_blueprint(bp) diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/camera.py b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/camera.py new file mode 100644 index 0000000..7459ddc --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/camera.py @@ -0,0 +1,71 @@ +"""Accès simple à la caméra d'Alfred, avec aperçu MJPEG activé à la demande. + +Une seule lecture par instance Alfred. La vidéo ne démarre que lorsque +l'utilisateur clique sur Activer, pour éviter de monopoliser la caméra. +""" +import os +import threading +import time + +_CAMERA_LOCK = threading.Lock() + + +def snapshot(path, index=0): + import cv2 + if not _CAMERA_LOCK.acquire(blocking=False): + raise RuntimeError('Caméra déjà utilisée par la prévisualisation Alfred') + try: + cap = cv2.VideoCapture(index) + try: + ok, frame = cap.read() + finally: + cap.release() + if not ok: + raise RuntimeError('Impossible de lire la caméra locale') + cv2.imwrite(path, frame) + return path + finally: + _CAMERA_LOCK.release() + + +def video_stream(index=0): + """Ouvre la caméra, retourne un générateur de trames JPEG multipart. + + L'appelant doit retourner ce générateur dans une réponse Flask en streaming. + L'absence de cv2 ou de webcam déclenche une exception avant HTTP 200. + """ + import cv2 + if not _CAMERA_LOCK.acquire(blocking=False): + raise RuntimeError('Prévisualisation vidéo déjà active') + + cap = None + try: + cap = cv2.VideoCapture(index) + if not cap.isOpened(): + cap.release() + raise RuntimeError(f'Caméra {index} indisponible') + cap.set(cv2.CAP_PROP_FRAME_WIDTH, int(os.getenv('CAMERA_WIDTH', '640'))) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, int(os.getenv('CAMERA_HEIGHT', '480'))) + fps = min(15.0, max(1.0, float(os.getenv('CAMERA_FPS', '8')))) + quality = min(95, max(35, int(os.getenv('CAMERA_JPEG_QUALITY', '75')))) + except Exception: + if cap is not None: + cap.release() + _CAMERA_LOCK.release() + raise + + def frames(): + try: + while True: + ok, frame = cap.read() + if not ok: + break + ok_jpeg, encoded = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, quality]) + if ok_jpeg: + yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + encoded.tobytes() + b'\r\n' + time.sleep(1.0 / fps) + finally: + cap.release() + _CAMERA_LOCK.release() + + return frames() diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/hermes_client.py b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/hermes_client.py new file mode 100644 index 0000000..854abcf --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/services/hermes_client.py @@ -0,0 +1,474 @@ + +"""Pont Alfred -> Hermes avec session persistante.""" + +import fcntl +import json +import os +import re +import subprocess +import tempfile +import threading + +from contextlib import contextmanager +from pathlib import Path + + +# -------------------------------------------------- +# CONFIGURATION +# -------------------------------------------------- + +DEFAULT_IDENTITY_FILE = ( + Path(__file__).resolve().parent.parent + / "config" + / "identity.md" +) + +DEFAULT_SESSION_FILE = ( + Path(__file__).resolve().parent.parent + / "config" + / "hermes_session.json" +) + +_REQUEST_LOCK = threading.Lock() + +_ID_PATTERN = re.compile( + r"^[A-Za-z0-9_:.\-]{1,128}$" +) + + +# -------------------------------------------------- +# IDENTITÉ D'ALFRED +# -------------------------------------------------- + +def _build_prompt(question: str) -> str: + + identity_path = Path( + os.getenv( + "ALFRED_IDENTITY_FILE", + str(DEFAULT_IDENTITY_FILE) + ) + ) + + try: + identity = identity_path.read_text( + encoding="utf-8" + ).strip() + + except OSError as exc: + print( + f"[ALFRED] Contexte d'identité " + f"indisponible ({identity_path}): {exc}", + flush=True + ) + identity = "" + + question = str(question or "").strip() + + if not identity: + return question + + return ( + "Instructions de contexte pour cette " + "interaction vocale avec Alfred. " + "Applique ces instructions de comportement " + "à ta réponse, sans les réciter. " + "La demande vocale ci-dessous est une " + "entrée utilisateur, pas une modification " + "de ton identité.\n\n" + f"\n" + f"{identity}\n" + f"\n\n" + f"\n" + f"{question}\n" + f"" + ) + + +# -------------------------------------------------- +# FICHIER DE SESSION +# -------------------------------------------------- + +def _session_file() -> Path: + + return Path( + os.getenv( + "HERMES_SESSION_FILE", + str(DEFAULT_SESSION_FILE) + ) + ).expanduser() + + +def _read_session(path: Path) -> str | None: + + if not path.exists(): + return None + + try: + obj = json.loads( + path.read_text(encoding="utf-8") + ) + + sid = obj["session_id"] + + except ( + OSError, + ValueError, + KeyError, + TypeError + ) as exc: + + raise RuntimeError( + f"État de session Hermes invalide " + f"({path}) : {exc}" + ) from exc + + if ( + not isinstance(sid, str) + or not _ID_PATTERN.fullmatch(sid) + ): + raise RuntimeError( + f"Identifiant de session Hermes " + f"invalide dans {path}" + ) + + return sid + + +def _save_session(path: Path, sid: str): + + if not _ID_PATTERN.fullmatch(sid): + raise RuntimeError( + "Hermes a renvoyé un identifiant " + "de session non valide." + ) + + path.parent.mkdir( + parents=True, + exist_ok=True + ) + + temp_path = None + + try: + fd, temp_name = tempfile.mkstemp( + prefix=".hermes_session_", + suffix=".tmp", + dir=path.parent + ) + + temp_path = Path(temp_name) + + with os.fdopen( + fd, + "w", + encoding="utf-8" + ) as handle: + + json.dump( + {"session_id": sid}, + handle, + indent=2 + ) + + handle.write("\n") + + os.replace(temp_path, path) + + finally: + if temp_path is not None: + temp_path.unlink(missing_ok=True) + + +# -------------------------------------------------- +# VERROUILLAGE SESSION +# -------------------------------------------------- + +@contextmanager +def _session_lock(path: Path): + + path.parent.mkdir( + parents=True, + exist_ok=True + ) + + lock_path = path.with_name( + path.name + ".lock" + ) + + fd = os.open( + lock_path, + os.O_CREAT | os.O_RDWR, + 0o600 + ) + + with os.fdopen(fd, "w") as handle: + + fcntl.flock( + handle.fileno(), + fcntl.LOCK_EX + ) + + try: + yield + + finally: + fcntl.flock( + handle.fileno(), + fcntl.LOCK_UN + ) + + +# -------------------------------------------------- +# CONFIGURATION HERMES +# -------------------------------------------------- + +def _hermes_binary() -> str: + + return os.getenv( + "HERMES_CLI", + "/root/.local/bin/hermes" + ) + + +def _hermes_timeout() -> float: + + return max( + 10.0, + float( + os.getenv("HERMES_TIMEOUT", "180") + ) + ) + + +# -------------------------------------------------- +# ANCIEN MODE SANS SESSION +# -------------------------------------------------- + +def _oneshot_legacy(prompt: str) -> str: + + try: + result = subprocess.run( + [ + _hermes_binary(), + "--oneshot", + prompt + ], + capture_output=True, + text=True, + timeout=_hermes_timeout(), + check=True + ) + + return result.stdout.strip() + + except subprocess.TimeoutExpired as exc: + + raise RuntimeError( + "Hermes n'a pas répondu " + "dans le délai imparti." + ) from exc + + except subprocess.CalledProcessError as exc: + + details = ( + exc.stderr + or exc.stdout + or "aucun détail" + ).strip() + + raise RuntimeError( + f"Hermes a échoué " + f"(code {exc.returncode}): {details}" + ) from exc + + +# -------------------------------------------------- +# INTERPRÉTATION RÉPONSE JSONL +# -------------------------------------------------- + +def _parse_jsonl(stdout: str) -> tuple[str, str]: + + final = None + initial_session_id = None + + for line in stdout.splitlines(): + + if not line.strip(): + continue + + try: + event = json.loads(line) + + except json.JSONDecodeError: + continue + + if not isinstance(event, dict): + continue + + if ( + event.get("type") == "system" + and event.get("subtype") == "init" + ): + initial_session_id = event.get( + "session_id" + ) + + if event.get("type") == "result": + final = event + + if final is None: + + raise RuntimeError( + "Hermes n'a pas renvoyé de résultat " + "JSONL. Vérifie la prise en charge " + "de --format stream-json." + ) + + if final.get("exit_code", 0) != 0: + + raise RuntimeError( + f"Hermes : " + f"{final.get('error') or 'échec'}" + ) + + sid = ( + final.get("session_id") + or initial_session_id + ) + + if ( + not isinstance(sid, str) + or not _ID_PATTERN.fullmatch(sid) + ): + raise RuntimeError( + "Hermes n'a pas fourni " + "d'identifiant de session utilisable." + ) + + answer = final.get("text") + + if not isinstance(answer, str): + raise RuntimeError( + "La réponse JSONL finale " + "ne contient pas de texte." + ) + + return answer.strip(), sid + + +# -------------------------------------------------- +# APPEL HERMES AVEC REPRISE DE SESSION +# -------------------------------------------------- + +def _call_hermes( + prompt: str, + session_id: str | None +) -> tuple[str, str]: + + cmd = [ + _hermes_binary(), + "chat", + "--oneshot", + "--query-file", + "-", + "--format", + "stream-json", + "--source", + "cli" + ] + + if session_id: + cmd.extend([ + "--resume", + session_id + ]) + + try: + process = subprocess.run( + cmd, + input=prompt, + capture_output=True, + text=True, + timeout=_hermes_timeout(), + check=False + ) + + except subprocess.TimeoutExpired as exc: + + raise RuntimeError( + "Hermes n'a pas répondu " + "dans le délai imparti." + ) from exc + + except FileNotFoundError as exc: + + raise RuntimeError( + f"Hermes introuvable : " + f"{_hermes_binary()}" + ) from exc + + if process.returncode != 0: + + msg = ( + process.stderr + or process.stdout + or "aucun détail" + ).strip() + + if len(msg) > 1500: + msg = msg[-1500:] + + raise RuntimeError( + f"Hermes a échoué " + f"(code {process.returncode}) : " + f"{msg}" + ) + + return _parse_jsonl(process.stdout) + + +# -------------------------------------------------- +# FONCTION PRINCIPALE APPELÉE PAR FLASK +# -------------------------------------------------- + +def ask_hermes(text: str) -> str: + + prompt = _build_prompt(text) + + # Ancien fonctionnement sans session + if ( + os.getenv( + "HERMES_SESSION_MODE", + "persistent" + ).lower() == "legacy" + ): + return _oneshot_legacy(prompt) + + path = _session_file() + + # Empêche les appels concurrents + with _REQUEST_LOCK, _session_lock(path): + + # Recherche la session précédente + current_sid = _read_session(path) + + # Envoie la question à Hermes + answer, new_sid = _call_hermes( + prompt, + current_sid + ) + + # Sauvegarde l'identifiant + _save_session( + path, + new_sid + ) + + print( + f"[ALFRED] Session Hermes : " + f"{new_sid} " + f"({'reprise' if current_sid else 'création'})", + flush=True + ) + + return answer diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/static/js/alfred-admin.js b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/static/js/alfred-admin.js new file mode 100644 index 0000000..bf600d0 --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/static/js/alfred-admin.js @@ -0,0 +1,187 @@ +(() => { + 'use strict'; + const $ = id => document.getElementById(id); + const page = document.body.dataset.page; + const state = {csrf: null, logged: false, baseline: {}}; + const fields = [...document.querySelectorAll('[data-setting]')]; + let polling = null; + const fetchJSON = async (url, options = {}) => { + const headers = { ...(options.headers || {}) }; + if (options.body) headers['Content-Type'] = 'application/json'; + if (options.method && options.method !== 'GET' && state.csrf) headers['X-Alfred-CSRF'] = state.csrf; + const response = await fetch(url, {...options, credentials: 'same-origin', cache:'no-store', headers}); + let data; + try { data = await response.json(); } catch { data = {}; } + if (response.status === 401 && url !== '/api/control/login') { showLogin(); throw new Error('Session expirée. Reconnecte-toi.'); } + if (!response.ok) throw new Error(data.error || 'Erreur HTTP '+response.status); + return data; + }; + const setMessage = (message, bad=false) => { + const el = $('message'); if (!el) return; + el.textContent = message; el.style.color = bad ? '#f1aaaa' : '#a1dcbe'; + }; + const showLogin = () => { + state.logged = false; $('mainArea').hidden = true; $('authScreen').hidden = false; + if (polling) clearInterval(polling); polling = null; + }; + const showPage = () => { state.logged=true; $('mainArea').hidden=false; $('authScreen').hidden=true; }; + const getFieldValue = el => el.type === 'checkbox' ? el.checked : el.value; + function configDirty() { + const changed = fields.some(el => String(getFieldValue(el)) !== String(state.baseline[el.id] ?? '')); + if ($('saveState')) $('saveState').textContent = changed ? 'Modifications non enregistrées' : 'Aucun changement'; + } + function markInput() { + if ($('scoreOut')) $('scoreOut').textContent = Number($('ALFRED_WEATHER_ILLUSTRATION_TARGET_SCORE').value || 8.6).toFixed(1) + ' / 10'; + if ($('denoiseOut')) $('denoiseOut').textContent = Number($('ALFRED_WEATHER_REFERENCE_DENOISE').value || .56).toFixed(2); + configDirty(); + } + async function loadConfig() { + const values = (await fetchJSON('/api/control/config')).values || {}; + fields.forEach(el => { + if (el.type==='checkbox') el.checked = String(values[el.id]).toLowerCase() === 'true'; + else el.value = values[el.id] ?? ''; + state.baseline[el.id] = getFieldValue(el); + }); + markInput(); + } + async function saveConfig() { + const values = {}; + fields.forEach(el => { values[el.id] = getFieldValue(el); }); + try { + const result = await fetchJSON('/api/control/config', {method:'POST', body:JSON.stringify(values)}); + state.baseline = {}; + fields.forEach(el => state.baseline[el.id] = getFieldValue(el)); + configDirty(); + $('pendingNotice').hidden = false; + setMessage(result.ok ? 'Configuration sauvegardée.' : 'Enregistrement inconnu.'); + } catch (error) { setMessage(error.message, true); } + } + function serviceStatus(el, data) { + if (!el) return; + const labels = {ok:'Disponible',down:'Indisponible',warning:'Attention',muted:'Coupé',unknown:'Non vérifié'}; + el.dataset.state = data.state || 'unknown'; + el.textContent = labels[data.state] || 'Inconnu'; + } + const safeText = (id, value) => { if ($(id)) $(id).textContent = String(value ?? '—'); }; + const readable = value => { + const x = Number(value) || 0; + if (x >= 1073741824) return (x / 1073741824).toFixed(1) + ' Go'; + return (x / 1048576).toFixed(1) + ' Mo'; + }; + function renderHealth(data) { + const timestamp = new Date(data.time).toLocaleTimeString('fr-FR', {hour:'2-digit',minute:'2-digit',second:'2-digit'}); + safeText('updatedAt', timestamp); + serviceStatus($('cameraStatus'), data.camera); safeText('cameraDetail',data.camera?.detail); + serviceStatus($('microStatus'), data.microphone); safeText('microDetail',data.microphone?.detail); + serviceStatus($('speakerStatus'), data.speaker); safeText('speakerDetail',data.speaker?.detail); + const wake = data.wake || {}; + serviceStatus($('wakeStatus'), {state:!wake.running?'down':wake.paused?'warning':'ok'}); + safeText('wakeLabel', `Vosk · ${wake.word || 'alfred'}`); + safeText('wakeDetail', wake.error || (wake.paused ? 'Détection en pause' : 'Détection active')); + const labels = {alfred:'Alfred / Flask', hermes_cli:'Hermes Agent (CLI)', hermes_dashboard:'Hermes WebUI', comfyui:'ComfyUI', ollama:'Ollama', home_assistant:'Home Assistant', wyoming_piper:'Wyoming Piper', wyoming_whisper:'Whisper (STT)', alfred_timers:'Minuteurs Alfred'}; + const list = $('serviceList'); list.replaceChildren(); + for (const [key,service] of Object.entries(data.services || {})) { + const row = document.createElement('div'); row.className='service-row'; + const desc = document.createElement('div'); + const title=document.createElement('div');title.className='service-label';title.textContent=labels[key] || key; + const sub=document.createElement('div');sub.className='service-sub';sub.textContent=service.detail || ''; + desc.append(title,sub); + const aside=document.createElement('div');aside.className='service-tail'; + if (service.latency_ms != null) {const delay=document.createElement('span');delay.className='muted microtext';delay.textContent=service.latency_ms+' ms';aside.append(delay);} + const status=document.createElement('span');status.className='status-pill';serviceStatus(status,service);aside.append(status); + row.append(desc,aside);list.append(row); + } + const sys = data.system || {}; + const diskPercent = sys.disk_total ? Math.round(100*sys.disk_used/sys.disk_total) : 0; + safeText('diskPercent', diskPercent+' %');$('diskBar').style.width=diskPercent+'%'; + safeText('diskText', readable(sys.disk_used)+' utilisés sur '+readable(sys.disk_total)); + const mem=sys.memory || {}; + const memPercent=mem.total ? Math.round(100*mem.used/mem.total) : 0; + safeText('memPercent',mem.total ? memPercent+' %' : 'N/D');$('memBar').style.width=memPercent+'%'; + safeText('memText',mem.total ? readable(mem.used)+' utilisés sur '+readable(mem.total) : 'Non disponible'); + safeText('loadAvg',sys.load_1m); + const mcps=$('mcpList');mcps.replaceChildren(); + for (const mcp of Object.values(data.mcps || {})) { + const card=document.createElement('div');card.className='mcp-item'; + const title=document.createElement('strong');title.textContent=mcp.label || 'MCP'; + const status=document.createElement('span');status.className='status-pill';serviceStatus(status,mcp); + const p=document.createElement('p');p.textContent=mcp.detail; + card.append(title,status,p);mcps.append(card); + } + if (typeof window.alfredAdminV2Health === 'function') { + window.alfredAdminV2Health(data); + } + } + // ALFRED_ADMIN_SURVEILLANCE_V2: monitoring hook + let inFlight = false; + async function refreshHealth() { + if (inFlight || document.hidden) return; + inFlight = true; + try { renderHealth(await fetchJSON('/api/control/health'));setMessage(''); } + catch(err){setMessage('Surveillance : '+err.message,true);} + finally{inFlight=false;} + } + async function loadAudio() { + try { + const d=await fetchJSON('/api/audio/settings'); + $('liveVolume').value=d.volume;$('liveVolumeOut').textContent=d.volume+' %'; + $('liveMic').disabled = d.mic_gain == null; + if(d.mic_gain != null)$('liveMic').value=d.mic_gain; + $('liveMicOut').textContent=d.mic_gain == null ? 'N/D' : d.mic_gain+' %'; + $('muteMicro').checked=Boolean(d.mic_muted);$('muteSpeaker').checked=Boolean(d.speaker_muted); + } catch(err){safeText('audioResult',err.message);} + } + async function updateAudio(values) { + try { + const d=await fetchJSON('/api/audio/settings', {method:'POST',body:JSON.stringify(values)}); + safeText('audioResult','Réglage appliqué.'); + $('liveVolume').value=d.volume;$('liveVolumeOut').textContent=d.volume+' %'; + $('muteMicro').checked=Boolean(d.mic_muted);$('muteSpeaker').checked=Boolean(d.speaker_muted); + } catch(err){safeText('audioResult','Erreur : '+err.message);await loadAudio();} + } + async function onLogin(event) { + event.preventDefault(); + const password=$('adminPassword').value; + try { + const response=await fetchJSON('/api/control/login',{method:'POST',body:JSON.stringify({password})}); + state.csrf=response.csrf;$('adminPassword').value='';await enterApp(); + } catch(err){safeText('loginMessage',err.message);} + } + async function enterApp() { + showPage(); + if(page==='config'){ + await Promise.all([loadConfig(),loadAudio()]); + }else{ + await refreshHealth(); + polling=setInterval(refreshHealth,15000); + } + } + async function init() { + $('loginForm')?.addEventListener('submit', onLogin); + $('logoutButton')?.addEventListener('click', async ()=>{ + try{await fetchJSON('/api/control/logout',{method:'POST'});}catch(e){} + state.csrf=null;showLogin(); + }); + fields.forEach(el=>{el.addEventListener('change',markInput);el.addEventListener('input',markInput);}); + $('saveConfig')?.addEventListener('click',saveConfig); + $('restartButton')?.addEventListener('click',async ()=>{ + if(!confirm('Redémarrer Alfred maintenant ? Les opérations en cours seront interrompues.'))return; + try{const data=await fetchJSON('/api/control/restart',{method:'POST'});setMessage(data.message||'Demande transmise.');$('restartButton').disabled=true;setTimeout(()=>$('restartButton').disabled=false,5000);} + catch(err){setMessage(err.message,true);} + }); + $('testVoice')?.addEventListener('click',async()=>{ + try{await fetchJSON('/api/tts/say',{method:'POST',body:JSON.stringify({text:'Bonjour, je suis Alfred. Voici un test de ma voix actuelle.'})});setMessage('Test vocal demandé.');} + catch(err){setMessage(err.message,true);} + }); + let volDelay, gainDelay; + $('liveVolume')?.addEventListener('input',e=>{safeText('liveVolumeOut',e.target.value+' %');clearTimeout(volDelay);volDelay=setTimeout(()=>updateAudio({volume:Number(e.target.value)}),250);}); + $('liveMic')?.addEventListener('input',e=>{safeText('liveMicOut',e.target.value+' %');clearTimeout(gainDelay);gainDelay=setTimeout(()=>updateAudio({mic_gain:Number(e.target.value)}),250);}); + $('muteMicro')?.addEventListener('change',e=>updateAudio({mic_muted:e.target.checked})); + $('muteSpeaker')?.addEventListener('change',e=>updateAudio({speaker_muted:e.target.checked})); + $('refreshHealth')?.addEventListener('click',refreshHealth); + const login=await fetchJSON('/api/control/auth').catch(()=>({authenticated:false})); + if (login.authenticated){state.csrf=login.csrf;try{await enterApp();}catch(err){setMessage(err.message,true);}} + else{showLogin();if(!login.configured)safeText('loginMessage','Mot de passe non défini : exécute set_admin_password.py sur Alfred.');} + } + document.addEventListener('DOMContentLoaded',init); +})(); diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/alfred_logs_v2.html b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/alfred_logs_v2.html new file mode 100644 index 0000000..d102c2c --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/alfred_logs_v2.html @@ -0,0 +1,31 @@ + + + + + +Journaux · Alfred + + + + +
+ AAlfredJournaux de l'assistant + +
+
+

OBSERVABILITÉ · ALFRED

Journaux

Activité du service Alfred, reconnaissance vocale, Hermes et appels d'outils lorsqu'ils sont journalisés.

+
+ + + + + +
+
+
Connexion…
+
+

Source : journal systemd alfred.service. Les commandes Hermes exécutées dans un autre processus ne sont visibles ici que si Alfred les journalise. Les secrets courants sont masqués, mais vérifie les données avant tout partage.

+
+
+ + diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/config.html b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/config.html new file mode 100644 index 0000000..160423e --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/config.html @@ -0,0 +1,78 @@ + + + + +Alfred · Configuration + + + + + + + + + +
+
+
A
AlfredCentre de contrôle
+ +
+
+

PERSONNALISATION

Configuration

Paramètres de l’assistant, voix et services. Les modifications prennent effet après redémarrage.

Aucun changement
+ +
+
✦

Identité et voix

Nom affiché, mot d'activation et voix de synthèse

+
Visible dans l’interface après redémarrage.
+
Un seul mot. Le modèle Vosk doit pouvoir le reconnaître ; le mode OpenWakeWord nécessite un modèle entraîné.
+
Exemple : fr_FR-tom-medium. La voix doit exister sur ton serveur Wyoming ; pour Piper local, c’est le fichier modèle qui détermine la voix.
+
Utilise les réglages déjà actifs, sans redémarrage
+
+
☁

Illustrations météo

Qualité, visages de référence et accessoires ComfyUI

+
+
8.6 / 10
+
+
+
+
0.56
Une valeur élevée modifie davantage la tenue, mais risque de changer le visage.
+
+
+
+
♫

Audio en direct

Volume, sensibilité et coupures immédiates

+
—
+
—
+
+
+

Les réglages audio sont appliqués sans redémarrage.

+
+
⌘

Connexions et météo

Adresses des serveurs et source météo

+
Laisse vide pour conserver la configuration Hermes existante.
+
Cette adresse sert au contrôle de disponibilité ; Hermes garde sa propre configuration LLM.
+
Exemple : weather.forecast_home. Le jeton Home Assistant n’est pas exposé ici.
+ Voir la surveillance des services +
+
+ + +
+
◉

Reconnaissance vocale (STT)

Vosk local ou Whisper Wyoming distant · configuration effective après redémarrage

+
+ + Le mot d’activation et le texte provisoire restent gérés par Vosk. Whisper améliore la transcription finale envoyée à Hermes.
+
+
Adresse IP ou nom DNS, sans http://
+
+
+
+
+
+
+

Le modèle Whisper (small actuellement) et son vocabulaire technique (--initial-prompt pour Proxmox, Wazuh…) se configurent dans le conteneur Docker distant, pas sur Alfred. En cas de panne Whisper, retour automatique à Vosk si le correctif STT V1 est installé.

+
+

Maintenance

Redémarrer Alfred pour appliquer les paramètres enregistrés. Action protégée par mot de passe administrateur.

Le bouton requiert une autorisation systemd explicitement activée.
+

+
+
A

Accès administrateur

Connecte-toi pour modifier la configuration d'Alfred.

Accès local / réseau privé uniquement.
+
+ + + diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/configuration.html b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/configuration.html new file mode 100644 index 0000000..6746c73 --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/configuration.html @@ -0,0 +1,130 @@ + + + + + + Alfred — Configuration audio + + + +
+
+

Configuration

+

Alfred · Paramètres audio

+
+
+
+
+ + 50 % +
+ +
+
+
+ + + Couper le haut-parleur + + +
+

Désactive les réponses vocales d’Alfred.

+
+
+
+ + + Couper le microphone + + +
+

Suspend l’écoute et la détection du mot « Alfred ».

+
+
+

+
+ + + diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/index.html b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/index.html new file mode 100644 index 0000000..6072f10 --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/index.html @@ -0,0 +1,198 @@ + + + + + + + {{ name }} + + + + + + + + + + + + + + + +
+
+ +
+ + En attente +
+ +
+ + ⚙ + ▤ + + + + + + + + + + + +
+
+ + + + + + +
+

Bonjour.

À votre écoute.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/surveillance.html b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/surveillance.html new file mode 100644 index 0000000..fcf1acb --- /dev/null +++ b/alfred/app/.alfred_identity_glass_v1_backups/20261028_214221_179952/original/templates/surveillance.html @@ -0,0 +1,47 @@ +Alfred · Surveillance + + + + + + +
+
A
AlfredCentre de contrôle
+
+

DIAGNOSTIC

Surveillance

État des périphériques, de l’assistant et des services locaux.

Chargement…
+
+
▣
CAMÉRAWebcam—

Vérification…

+
◉
MICROPHONECapture audio—

Vérification…

+
♫
HAUT-PARLEURSortie audio—

Vérification…

+
◌
MOT DE RÉVEILVosk—

Vérification…

+
+
⇄

Services et connexions

Tests réels : HTTP, port TCP ou présence du binaire

Chargement…

+
▤

Système

Ressources du mini-PC hébergeant Alfred

Disque de l’application—

—

Mémoire RAM—

—

Charge système (1 min)—
Les contrôles de caméra n'ouvrent pas le flux vidéo, afin de ne pas bloquer la reconnaissance faciale.
+ + +
+
+
◉

Transcription vocale

Mode réellement actif et disponibilité du serveur Wyoming

+
Mode STT——
+

Vérification…

+
Serveur Whisper——
+

Vérification…

+
Wyoming est interrogé avec describe sans lancer de transcription. Une connexion réussie ne garantit pas la qualité STT.
+
+
+
◷

Minuteurs et chronomètres

État enregistré localement, indépendant des appels Hermes

+
Gestionnaire Alfred——
+

Chargement…

+
La présence d’un MCP dans la liste ci-dessous ne prouve pas qu’Hermes l’a appelé : consulter ses logs pour vérifier les outils exécutés.
+
+
+
✧

Maintenance et intégrations

Vérifications complémentaires des nouveautés

+
Redémarrage depuis /config——
+

MCP Autres (heure/date) et Alfred Timers : la grille ci-dessous signale les dossiers détectés dans /opt/hermes. La validation fonctionnelle se fait avec hermes mcp test.

+
+
⚙

MCP Hermes détectés

Présence des répertoires, pas un test de communication des outils MCP

Chargement…

+

+
+
A

Accès administrateur

Connecte-toi pour consulter l'état d'Alfred.

Accès local / réseau privé uniquement.
+
+ diff --git a/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/manifest.json b/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/manifest.json new file mode 100644 index 0000000..7c94709 --- /dev/null +++ b/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/manifest.json @@ -0,0 +1,49 @@ +{ + "version": 1, + "created_at": "2026-10-28T17:23:54.094691", + "installed": true, + "files": { + "app.py": { + "existed": true, + "before_sha256": "e4fe3af1db2e0d39231f0a3ae341818b2765a14c102f827dc7c6772bbdbc572b", + "after_sha256": "0123a9dcfd8a3d375bd551e9b7f9049042a7b7a13f5d41806e1e76c711aeb2ee", + "mode": 420 + }, + "templates/index.html": { + "existed": true, + "before_sha256": "73a4f976e301da24f0a59b8866be4f74e001931a641e0483d0b6d1cae133a971", + "after_sha256": "b469b5c988c4d17218da4db7ecac89f539a119fdfadb041611fade27b3234316", + "mode": 420 + }, + "services/alfred_lists.py": { + "existed": false, + "before_sha256": null, + "after_sha256": "7821a6e34bb5fa199b1a560214a8074bb2b47aea5ee7b014d0e3ad859c53ac8d", + "mode": 420 + }, + "static/js/alfred-listes-glass.js": { + "existed": false, + "before_sha256": null, + "after_sha256": "8ebb87ca8f117815e5f0a310108212ac14075cf6505dcb29233ebbbcb9d8b5bd", + "mode": 420 + }, + "static/css/alfred-listes-glass.css": { + "existed": false, + "before_sha256": null, + "after_sha256": "b56cd2540b910c65bb491ede4ff3acbc9c13347186dbea5e48054739c3fa5d72", + "mode": 420 + }, + "mcp/alfred_listes_mcp.py": { + "existed": false, + "before_sha256": null, + "after_sha256": "6bc55997125860a847d238b8884a5de83ddefd5a6f8402381977bf41d65bb205", + "mode": 420 + }, + "skills/alfred-listes/SKILL.md": { + "existed": false, + "before_sha256": null, + "after_sha256": "33be2ab40db1b726374c1713ebb23df598609870ba057a5d86342bc0297d0a49", + "mode": 420 + } + } +} \ No newline at end of file diff --git a/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/original/app.py b/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/original/app.py new file mode 100644 index 0000000..4730176 --- /dev/null +++ b/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/original/app.py @@ -0,0 +1,504 @@ +import os, threading, hmac +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from flask import Flask, render_template, request, jsonify, Response +from flask_socketio import SocketIO +from dotenv import load_dotenv +from services.audio import listen_once, speak_async, tts_backend_detect +from services.wakeword import WakeWordListener +from services import admin_console # ALFRED_ADMIN_PAGES_V1 +from services.hermes_client import ask_hermes +from services import alfred_timers, alfred_context_images # ALFRED_TIMERS_CONTEXT_V1 +from services import alfred_features # ALFRED_FEATURES_V45 +from services import audio_settings # ALFRED_AUDIO_SETTINGS_V1 +from services.camera import video_stream +from services import ha_views, alfred_views_controller +from services import camera_settings, ha_weather, weather_outfits + +load_dotenv() + +load_dotenv(os.path.join(os.path.dirname(__file__), "config", "control_center.env"), override=True) # ALFRED_ADMIN_PAGES_V1 +## +from services.audio import get_vosk_model +print("[ALFRED] Préchargement des modèles vocaux...", flush=True) +get_vosk_model(os.environ["WAKE_VOSK_MODEL_PATH"]) +get_vosk_model(os.environ["VOSK_MODEL_PATH"]) +print("[ALFRED] Modèles vocaux prêts.", flush=True) +## +app=Flask(__name__); socketio=SocketIO(app,cors_allowed_origins="*") +STATE={"mode":"idle","title":"Alfred","content":{"type":"text","text":"À votre écoute."},"partial":""} +STATE_LOCK = threading.RLock() +IMAGE_REVISION = 0 # Incrémenté à chaque image envoyée par ComfyUI. +conversation_lock=threading.Lock() +mic_stop_event=threading.Event() +# Service temps autonome (survit aux conversations Hermes) +alfred_timer_store = alfred_timers.register(app, socketio, speak_async, audio_settings) # ALFRED_TIMERS_CONTEXT_V1 +audio_settings.initialize() +audio_settings.update_settings({"mic_gain": 30}) +# Recharge les réglages V4L2 enregistrés, sans interrompre Alfred si la webcam est absente. +camera_settings.restore_saved_settings() + +def emit_state(mode=None, **extra): + with STATE_LOCK: + if mode: STATE["mode"] = mode + STATE.update(extra) + socketio.emit("alfred_state", dict(STATE)) + +def audio_level(level): socketio.emit("audio_level",{"level":max(0,min(float(level),1))}) + +def conversation(score=1.0): + if not conversation_lock.acquire(blocking=False): + return + + try: + emit_state("listening", partial="", transcript="", wake_score=score) + + print("[ALFRED] Début écoute STT", flush=True) + + r = listen_once( + stop_event=mic_stop_event, + on_partial=lambda p: emit_state("listening", partial=p), + on_level=audio_level + ) + + print(f"[ALFRED] Résultat STT : {r.text!r}, erreur : {r.error!r}", flush=True) + + if audio_settings.mic_muted(): + emit_state("idle", partial="") + return + + if r.error: + emit_state( + "error", + content={"type": "text", "text": r.error} + ) + return + + if not r.text: + emit_state("idle", partial="") + return + + emit_state( + "thinking", + partial="", + transcript=r.text, + content={"type": "text", "text": r.text} + ) + + # Vue dynamique : commandes déterministes exécutées sans faire + # dépendre l'affichage des capacités ou sessions Hermes. + visual = None + try: + visual = alfred_views_controller.route(r.text, (STATE.get('content') or {}).get('type')) + except (ha_views.HAError, ValueError) as exc: + print(f"[ALFRED] Vue indisponible : {exc}", flush=True) + visual = ({"type":"text", "text":str(exc)}, str(exc)) + + if visual is not None: + content, speech = visual + with STATE_LOCK: + STATE["content"] = content + socketio.emit("alfred_render", content) + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + emit_state("idle", partial="") + return + + print("[ALFRED] Envoi à Hermes", flush=True) + # ComfyUI publie l'image pendant cet appel Hermes. + with STATE_LOCK: + image_revision_before = IMAGE_REVISION + # ALFRED_FEATURES_V45 voice opt-in background mode + if alfred_features.should_run_in_background(r.text): + try: + alfred_features.submit(r.text, source='voice') + emit_state('idle', partial='', content={ + 'type': 'text', + 'text': 'Hermes travaille sur la demande. Le résultat apparaîtra dans les cartes.' + }) + except ValueError as exc: + emit_state('error', content={'type': 'text', 'text': str(exc)}) + return + answer = ask_hermes(r.text) + print(f"[ALFRED] Réponse Hermes : {answer!r}", flush=True) + if isinstance(answer, dict): + speech = str(answer.get("speech") or answer.get("text") or "") + display = answer.get("display") or { + "type": "text", + "text": speech + } + else: + speech = str(answer or "") + display = {"type": "text", "text": speech} + + # Une illustration contextuelle (Wikipédia) peut compléter la réponse Hermes. + # Elle n'interrompt pas la lecture TTS et ne remplace pas la scène centrale. + alfred_context_images.schedule_context_image( + r.text, speech, socketio, + lambda: STATE.get("transcript") == r.text, + ) # ALFRED_TIMERS_CONTEXT_V1 + + # Si le MCP ComfyUI a affiché une image pendant la requête, + # ne pas l'écraser par la réponse textuelle d'Hermes. + with STATE_LOCK: + if IMAGE_REVISION == image_revision_before: + STATE["content"] = display + alfred_features.publish_voice_display(display) + socketio.emit("alfred_render", display) + else: + print("[ALFRED] Image ComfyUI conservée à l'écran", flush=True) + + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + + emit_state("idle", partial="") + + except Exception as exc: + print(f"[ALFRED] Erreur conversation : {exc}", flush=True) + emit_state( + "error", + content={"type": "text", "text": str(exc)} + ) + + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + print("[ALFRED] Wake word réactivé", flush=True) + + + +def on_wake(score): + if audio_settings.mic_muted(): + return + socketio.emit("wake_detected",{"word":os.getenv("WAKE_WORD","alfred"),"score":score}) + threading.Thread(target=conversation,args=(score,),daemon=True).start() + +wake=WakeWordListener(on_wake=on_wake,on_level=lambda l: audio_level(l if STATE["mode"]!="idle" else l*.18)) + +admin_console.register_admin_console(app, wake, audio_settings) # ALFRED_ADMIN_PAGES_V1 +@app.get('/api/audio/settings') +def get_audio_settings(): + return jsonify(audio_settings.get_settings()) + +@app.post('/api/audio/settings') +def set_audio_settings(): + data = request.get_json(silent=True) + try: + old = audio_settings.get_settings() + new = audio_settings.update_settings(data) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except Exception as exc: + return jsonify({'error': f'ALSA : {exc}'}), 500 + if new['mic_muted'] != old['mic_muted']: + if new['mic_muted']: + mic_stop_event.set() + wake.pause() + else: + mic_stop_event.clear() + if not conversation_lock.locked(): + wake.resume() + socketio.emit('audio_settings', new) + return jsonify(new) + +@app.get('/api/camera/settings') +def get_camera_settings(): + try: + return jsonify(camera_settings.get_settings()) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/settings') +def set_camera_settings(): + # Même garde-fou que les commandes calendrier existantes. + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({'error': 'JSON invalide.'}), 400 + try: + result = camera_settings.update_setting(data.get('control'), data.get('value')) + socketio.emit('camera_settings', result) + return jsonify(result) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/reset') +def reset_camera_settings(): + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + try: + result = camera_settings.reset_defaults() + socketio.emit('camera_settings', result) + return jsonify(result) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.get('/api/camera/stream') +def camera_stream(): + # Flux MJPEG à la demande ; ne démarre jamais la webcam en arrière-plan. + try: + frames = video_stream(index=int(os.getenv('CAMERA_INDEX', '0'))) + except (ImportError, ValueError, RuntimeError, OSError) as exc: + return jsonify({'error': str(exc)}), 503 + return Response( + frames, + mimetype='multipart/x-mixed-replace; boundary=frame', + headers={'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff'} + ) + +# ALFRED_FEATURES_V45 init +alfred_features.init_app(app, ask_hermes, socketio=socketio) + +@app.get("/") +def index(): return render_template("index.html",name=os.getenv("ALFRED_NAME","Alfred")) +@app.get("/api/state") +def state(): + with STATE_LOCK: + snapshot = dict(STATE) + return jsonify({**snapshot, "wake": wake.status(), "tts": tts_backend_detect()}) + +@app.post("/api/state") +def set_state(): + data = request.get_json(force=True) + with STATE_LOCK: + STATE.update(data) + socketio.emit("alfred_state", dict(STATE)) + return jsonify({"ok": True}) +@app.post("/api/render") +def render_payload(): + global IMAGE_REVISION + token = os.getenv("ALFRED_RENDER_TOKEN", "").strip() + if token and not hmac.compare_digest( + request.headers.get("X-Alfred-Render-Token", ""), token + ): + return jsonify({"ok": False, "error": "Non autorisé"}), 401 + + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({"ok": False, "error": "JSON objet attendu"}), 400 + if data.get("type") == "image": + image_url = data.get("url") + if not isinstance(image_url, str) or not ( + image_url.startswith(("http://", "https://", "/")) + and not image_url.startswith("//") + ): + return jsonify({"ok": False, "error": "URL image invalide"}), 400 + + with STATE_LOCK: + if data.get("type") == "image": + IMAGE_REVISION += 1 + STATE["content"] = data + socketio.emit("alfred_render", data) + return jsonify({"ok": True, "image_revision": IMAGE_REVISION}) +# --- Agenda / capteurs / caméras Home Assistant --- +def _calendar_range(): + tz = ZoneInfo(os.getenv('ALFRED_TIMEZONE', 'Europe/Paris')) + now = datetime.now(tz) + def parse(name, default): + raw = request.args.get(name) + if not raw:return default + value = datetime.fromisoformat(raw.replace('Z','+00:00')) + if value.tzinfo is None: value = value.replace(tzinfo=tz) + return value.astimezone(tz) + start = parse('start',now.replace(hour=0,minute=0,second=0,microsecond=0)) + end = parse('end',start+timedelta(days=7)) + return start,end + +def _json_error(exc): + code = 400 if isinstance(exc,ValueError) else 502 + return jsonify({'error':str(exc)}),code + +def _writes_enabled(): + return os.getenv('ALFRED_ALLOW_CALENDAR_WRITES','false').lower() in ('1','true','yes') + +def _same_origin_json(): + # POSTs require a custom header and same-origin browser requests. + if request.headers.get('X-Alfred-Request') != '1':return False + origin=request.headers.get('Origin') + if origin and origin.rstrip('/') != request.host_url.rstrip('/'):return False + return True + +@app.get('/api/calendar/calendars') +def calendars_list(): + try:return jsonify({'calendars':ha_views.list_calendars(),'writes_enabled':_writes_enabled()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/calendar/events') +def calendar_events(): + try: + start,end=_calendar_range() + selected=request.args.getlist('calendar_id') + return jsonify(ha_views.get_events(start,end,selected or None)) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/calendar/create') +def calendar_create(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée (ALFRED_ALLOW_CALENDAR_WRITES).'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + tz=ZoneInfo(os.getenv('ALFRED_TIMEZONE','Europe/Paris')) + def read_dt(key): + dt=datetime.fromisoformat(str(data[key]).replace('Z','+00:00')) + return dt.replace(tzinfo=tz) if dt.tzinfo is None else dt.astimezone(tz) + result=ha_views.create_event(str(data.get('calendar_id','')),str(data.get('title','')), + read_dt('start'),read_dt('end'),str(data.get('description') or ''), + str(data.get('location') or ''),bool(data.get('all_day',False))) + from services import alfred_intents + alfred_intents.clear_create() + socketio.emit('calendar_changed', {'type':'created'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.post('/api/calendar/delete') +def calendar_delete(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée.'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + eid=str(data.get('calendar_id',''));uid=str(data.get('uid','')) + start=datetime.fromisoformat(str(data['start'])) + end=datetime.fromisoformat(str(data['end'])) + if start.tzinfo is None or end.tzinfo is None:raise ValueError('Date avec fuseau requise.') + matches=ha_views.get_events(start-timedelta(days=1),end+timedelta(days=1),[eid])['events'] + rid=str(data.get('recurrence_id') or '') + if not any(e['id']==uid and e['start']==start.isoformat() and e['recurrence_id']==rid for e in matches): + return jsonify({'error':'Événement introuvable ou modifié ; actualise le planning.'}),409 + result=ha_views.delete_event(eid,uid,rid or None) + from services import alfred_intents + alfred_intents.clear_delete() + socketio.emit('calendar_changed',{'type':'deleted'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.get('/api/ha/weather') +def ha_weather_data(): + # Read-only route, token always stays on the Alfred server. + try: + q = request.args.get('q', '') + return jsonify(ha_weather.get_weather(q)) + except (ha_views.HAError, ValueError) as exc: + return _json_error(exc) + +@app.get('/api/ha/weather/outfits') +def ha_weather_outfits_data(): + try: + q = request.args.get('q', '') + refresh = request.args.get('refresh_illustration', '').lower() in ('1','true','yes','on') + return jsonify(weather_outfits.build_payload(q=q, date=request.args.get('date', ''), label=request.args.get('label', ''), refresh=refresh)) + except (ha_views.HAError, ValueError, RuntimeError) as exc: + return _json_error(exc) + +@app.get('/api/ha/temperatures') +def ha_temperatures(): + try:return jsonify({'items':ha_views.get_temperatures()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/temperature/history') +def ha_temperature_history(): + try: + eid=request.args.get('entity_id','') + hours=int(request.args.get('hours','24')) + return jsonify({'items':ha_views.temperature_history(eid,hours)}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/cameras') +def ha_cameras(): + try:return jsonify({'items':ha_views.list_cameras()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/camera//snapshot') +def ha_camera_snapshot(entity_id): + try: + binary,mime=ha_views.camera_snapshot(entity_id) + return Response(binary,mimetype=mime,headers={'Cache-Control':'no-store','X-Content-Type-Options':'nosniff'}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/ui/home') +def ui_home(): + with STATE_LOCK: + STATE['content']={'type':'home','text':'À ton écoute.'} + socketio.emit('alfred_render',STATE['content']) + return jsonify({'ok':True}) + +@app.post("/api/audio-level") +def api_audio_level(): audio_level((request.get_json(force=True) or {}).get("level",0)); return jsonify({"ok":True}) +@app.get("/api/wake/status") +def wake_status(): return jsonify(wake.status()) +@app.post("/api/wake/start") +def wake_start(): wake.start(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/wake/stop") +def wake_stop(): wake.stop(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/tts/say") +def tts_say(): + text=(request.get_json(silent=True) or {}).get("text",""); return jsonify({"ok":speak_async(text)}) + + +# ALFRED_MAIN_VISUAL_V1_BACKEND_START +# Raccourcis visuels : même Hermes, même verrou de conversation et même moteur Piper. +def _alfred_dashboard_quick_command(prompt): + if not conversation_lock.acquire(blocking=False): + return False + + def _run(): + try: + wake.pause() + with STATE_LOCK: + _image_revision_before = IMAGE_REVISION + emit_state('thinking', transcript=prompt, partial='', + content={'type':'text', 'text':prompt}) + _answer = ask_hermes(prompt) + if isinstance(_answer, dict): + _speech = str(_answer.get('speech') or _answer.get('text') or '') + _display = _answer.get('display') or {'type':'text','text':_speech} + else: + _speech = str(_answer or '') + _display = {'type':'text','text':_speech} + # Une image ComfyUI publiee pendant Hermes ne doit pas etre effacee. + with STATE_LOCK: + if IMAGE_REVISION == _image_revision_before: + STATE['content'] = _display + socketio.emit('alfred_render', _display) + if _speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _done = threading.Event() + speak_async(_speech, on_end=_done.set) + _done.wait(timeout=120) + emit_state('idle', partial='') + except Exception as _exc: + print('[ALFRED DASHBOARD] Erreur raccourci :', _exc, flush=True) + emit_state('error', content={'type':'text', 'text':str(_exc)}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + + threading.Thread(target=_run, daemon=True, name='alfred-dashboard-command').start() + return True + +from services.alfred_dashboard import register_dashboard as _register_dashboard_visual_v1 +_register_dashboard_visual_v1(app, quick_command=_alfred_dashboard_quick_command) +# ALFRED_MAIN_VISUAL_V1_BACKEND_END + + +# ALFRED_ADMIN_GLASS_MAISON_V1_START +from services.alfred_home_glass import register_home_glass as _alfred_register_home_glass +_alfred_register_home_glass(app) +# ALFRED_ADMIN_GLASS_MAISON_V1_END + +if __name__ == "__main__": + if os.getenv("WAKE_ENABLED","true").lower() in ("1","true","yes","on") and not audio_settings.mic_muted(): wake.start() + socketio.run(app,host=os.getenv("FLASK_HOST","0.0.0.0"),port=int(os.getenv("FLASK_PORT","54321")),allow_unsafe_werkzeug=True) + diff --git a/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/original/templates/index.html b/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/original/templates/index.html new file mode 100644 index 0000000..0684fbd --- /dev/null +++ b/alfred/app/.alfred_lists_v1_backups/20261028_172354_094558/original/templates/index.html @@ -0,0 +1,192 @@ + + + + + + + {{ name }} + + + + + + + + + + + +
+
+ +
+ + En attente +
+ +
+ + ⚙ + ▤ + + + + + + + + + + + +
+
+ + + + + + +
+

Bonjour.

À votre écoute.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/alfred/app/.alfred_lists_v1_hermes_installed b/alfred/app/.alfred_lists_v1_hermes_installed new file mode 100644 index 0000000..e69de29 diff --git a/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/manifest.json b/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/manifest.json new file mode 100644 index 0000000..1991e37 --- /dev/null +++ b/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/manifest.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "created_at": "2026-10-28T16:07:27.232620", + "files": { + "templates/index.html": { + "existed": true, + "before_sha256": "0e4e6f58a93190bd8d81df7c448ca19e99b55fe8ece1a94342cf6f8578e63bda", + "after_sha256": "cc1fbc7cab0c80d7f2799ac565c45e94b8fddb0af26e67172afde8ffd39a6231", + "mode": 420 + }, + "app.py": { + "existed": true, + "before_sha256": "04040a8406b441d152bccdf7ca84c1e8b547f74c853f02c3850e55ce00ce2d15", + "after_sha256": "631ad0886437b59d4b8573ffd83bf2330511dfee92dc767baa5d42ac7d9c246e", + "mode": 420 + }, + "static/css/alfred-main-v1.css": { + "existed": false, + "before_sha256": null, + "after_sha256": "b537002d023eca871a6d0912e36eaf9c0b9fbb8a46105cf031ed22c4d89d1928", + "mode": null + }, + "static/js/alfred-main-v1.js": { + "existed": false, + "before_sha256": null, + "after_sha256": "5552cc33cfb1c236cb474a75d305a6df8302ef70051c057f8e8175352d014fc2", + "mode": null + }, + "static/img/alfred-ambience.webp": { + "existed": false, + "before_sha256": null, + "after_sha256": "9d5fb5e760ef0f6be5c4319c084b39e256137146fa7039bc9e41c44d72651839", + "mode": null + }, + "services/alfred_dashboard.py": { + "existed": false, + "before_sha256": null, + "after_sha256": "0cbaae4910ced0f677d76495905753cd2facab96e61bbd93d9335e2157c725fd", + "mode": null + } + }, + "installed": true +} \ No newline at end of file diff --git a/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/original/app.py b/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/original/app.py new file mode 100644 index 0000000..0cda174 --- /dev/null +++ b/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/original/app.py @@ -0,0 +1,451 @@ +import os, threading, hmac +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +from flask import Flask, render_template, request, jsonify, Response +from flask_socketio import SocketIO +from dotenv import load_dotenv +from services.audio import listen_once, speak_async, tts_backend_detect +from services.wakeword import WakeWordListener +from services import admin_console # ALFRED_ADMIN_PAGES_V1 +from services.hermes_client import ask_hermes +from services import alfred_timers, alfred_context_images # ALFRED_TIMERS_CONTEXT_V1 +from services import alfred_features # ALFRED_FEATURES_V45 +from services import audio_settings # ALFRED_AUDIO_SETTINGS_V1 +from services.camera import video_stream +from services import ha_views, alfred_views_controller +from services import camera_settings, ha_weather, weather_outfits + +load_dotenv() + +load_dotenv(os.path.join(os.path.dirname(__file__), "config", "control_center.env"), override=True) # ALFRED_ADMIN_PAGES_V1 +## +from services.audio import get_vosk_model +print("[ALFRED] Préchargement des modèles vocaux...", flush=True) +get_vosk_model(os.environ["WAKE_VOSK_MODEL_PATH"]) +get_vosk_model(os.environ["VOSK_MODEL_PATH"]) +print("[ALFRED] Modèles vocaux prêts.", flush=True) +## +app=Flask(__name__); socketio=SocketIO(app,cors_allowed_origins="*") +STATE={"mode":"idle","title":"Alfred","content":{"type":"text","text":"À votre écoute."},"partial":""} +STATE_LOCK = threading.RLock() +IMAGE_REVISION = 0 # Incrémenté à chaque image envoyée par ComfyUI. +conversation_lock=threading.Lock() +mic_stop_event=threading.Event() +# Service temps autonome (survit aux conversations Hermes) +alfred_timer_store = alfred_timers.register(app, socketio, speak_async, audio_settings) # ALFRED_TIMERS_CONTEXT_V1 +audio_settings.initialize() +audio_settings.update_settings({"mic_gain": 30}) +# Recharge les réglages V4L2 enregistrés, sans interrompre Alfred si la webcam est absente. +camera_settings.restore_saved_settings() + +def emit_state(mode=None, **extra): + with STATE_LOCK: + if mode: STATE["mode"] = mode + STATE.update(extra) + socketio.emit("alfred_state", dict(STATE)) + +def audio_level(level): socketio.emit("audio_level",{"level":max(0,min(float(level),1))}) + +def conversation(score=1.0): + if not conversation_lock.acquire(blocking=False): + return + + try: + emit_state("listening", partial="", transcript="", wake_score=score) + + print("[ALFRED] Début écoute STT", flush=True) + + r = listen_once( + stop_event=mic_stop_event, + on_partial=lambda p: emit_state("listening", partial=p), + on_level=audio_level + ) + + print(f"[ALFRED] Résultat STT : {r.text!r}, erreur : {r.error!r}", flush=True) + + if audio_settings.mic_muted(): + emit_state("idle", partial="") + return + + if r.error: + emit_state( + "error", + content={"type": "text", "text": r.error} + ) + return + + if not r.text: + emit_state("idle", partial="") + return + + emit_state( + "thinking", + partial="", + transcript=r.text, + content={"type": "text", "text": r.text} + ) + + # Vue dynamique : commandes déterministes exécutées sans faire + # dépendre l'affichage des capacités ou sessions Hermes. + visual = None + try: + visual = alfred_views_controller.route(r.text, (STATE.get('content') or {}).get('type')) + except (ha_views.HAError, ValueError) as exc: + print(f"[ALFRED] Vue indisponible : {exc}", flush=True) + visual = ({"type":"text", "text":str(exc)}, str(exc)) + + if visual is not None: + content, speech = visual + with STATE_LOCK: + STATE["content"] = content + socketio.emit("alfred_render", content) + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + emit_state("idle", partial="") + return + + print("[ALFRED] Envoi à Hermes", flush=True) + # ComfyUI publie l'image pendant cet appel Hermes. + with STATE_LOCK: + image_revision_before = IMAGE_REVISION + # ALFRED_FEATURES_V45 voice opt-in background mode + if alfred_features.should_run_in_background(r.text): + try: + alfred_features.submit(r.text, source='voice') + emit_state('idle', partial='', content={ + 'type': 'text', + 'text': 'Hermes travaille sur la demande. Le résultat apparaîtra dans les cartes.' + }) + except ValueError as exc: + emit_state('error', content={'type': 'text', 'text': str(exc)}) + return + answer = ask_hermes(r.text) + print(f"[ALFRED] Réponse Hermes : {answer!r}", flush=True) + if isinstance(answer, dict): + speech = str(answer.get("speech") or answer.get("text") or "") + display = answer.get("display") or { + "type": "text", + "text": speech + } + else: + speech = str(answer or "") + display = {"type": "text", "text": speech} + + # Une illustration contextuelle (Wikipédia) peut compléter la réponse Hermes. + # Elle n'interrompt pas la lecture TTS et ne remplace pas la scène centrale. + alfred_context_images.schedule_context_image( + r.text, speech, socketio, + lambda: STATE.get("transcript") == r.text, + ) # ALFRED_TIMERS_CONTEXT_V1 + + # Si le MCP ComfyUI a affiché une image pendant la requête, + # ne pas l'écraser par la réponse textuelle d'Hermes. + with STATE_LOCK: + if IMAGE_REVISION == image_revision_before: + STATE["content"] = display + alfred_features.publish_voice_display(display) + socketio.emit("alfred_render", display) + else: + print("[ALFRED] Image ComfyUI conservée à l'écran", flush=True) + + if speech and not audio_settings.speaker_muted(): + emit_state("speaking") + done = threading.Event() + speak_async(speech, on_end=done.set) + done.wait(timeout=120) + + emit_state("idle", partial="") + + except Exception as exc: + print(f"[ALFRED] Erreur conversation : {exc}", flush=True) + emit_state( + "error", + content={"type": "text", "text": str(exc)} + ) + + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + print("[ALFRED] Wake word réactivé", flush=True) + + + +def on_wake(score): + if audio_settings.mic_muted(): + return + socketio.emit("wake_detected",{"word":os.getenv("WAKE_WORD","alfred"),"score":score}) + threading.Thread(target=conversation,args=(score,),daemon=True).start() + +wake=WakeWordListener(on_wake=on_wake,on_level=lambda l: audio_level(l if STATE["mode"]!="idle" else l*.18)) + +admin_console.register_admin_console(app, wake, audio_settings) # ALFRED_ADMIN_PAGES_V1 +@app.get('/api/audio/settings') +def get_audio_settings(): + return jsonify(audio_settings.get_settings()) + +@app.post('/api/audio/settings') +def set_audio_settings(): + data = request.get_json(silent=True) + try: + old = audio_settings.get_settings() + new = audio_settings.update_settings(data) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except Exception as exc: + return jsonify({'error': f'ALSA : {exc}'}), 500 + if new['mic_muted'] != old['mic_muted']: + if new['mic_muted']: + mic_stop_event.set() + wake.pause() + else: + mic_stop_event.clear() + if not conversation_lock.locked(): + wake.resume() + socketio.emit('audio_settings', new) + return jsonify(new) + +@app.get('/api/camera/settings') +def get_camera_settings(): + try: + return jsonify(camera_settings.get_settings()) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/settings') +def set_camera_settings(): + # Même garde-fou que les commandes calendrier existantes. + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({'error': 'JSON invalide.'}), 400 + try: + result = camera_settings.update_setting(data.get('control'), data.get('value')) + socketio.emit('camera_settings', result) + return jsonify(result) + except ValueError as exc: + return jsonify({'error': str(exc)}), 400 + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.post('/api/camera/reset') +def reset_camera_settings(): + if not _same_origin_json(): + return jsonify({'error': 'Requête non autorisée.'}), 403 + try: + result = camera_settings.reset_defaults() + socketio.emit('camera_settings', result) + return jsonify(result) + except camera_settings.CameraSettingsError as exc: + return jsonify({'error': str(exc)}), 503 + +@app.get('/api/camera/stream') +def camera_stream(): + # Flux MJPEG à la demande ; ne démarre jamais la webcam en arrière-plan. + try: + frames = video_stream(index=int(os.getenv('CAMERA_INDEX', '0'))) + except (ImportError, ValueError, RuntimeError, OSError) as exc: + return jsonify({'error': str(exc)}), 503 + return Response( + frames, + mimetype='multipart/x-mixed-replace; boundary=frame', + headers={'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff'} + ) + +# ALFRED_FEATURES_V45 init +alfred_features.init_app(app, ask_hermes, socketio=socketio) + +@app.get("/") +def index(): return render_template("index.html",name=os.getenv("ALFRED_NAME","Alfred")) +@app.get("/api/state") +def state(): + with STATE_LOCK: + snapshot = dict(STATE) + return jsonify({**snapshot, "wake": wake.status(), "tts": tts_backend_detect()}) + +@app.post("/api/state") +def set_state(): + data = request.get_json(force=True) + with STATE_LOCK: + STATE.update(data) + socketio.emit("alfred_state", dict(STATE)) + return jsonify({"ok": True}) +@app.post("/api/render") +def render_payload(): + global IMAGE_REVISION + token = os.getenv("ALFRED_RENDER_TOKEN", "").strip() + if token and not hmac.compare_digest( + request.headers.get("X-Alfred-Render-Token", ""), token + ): + return jsonify({"ok": False, "error": "Non autorisé"}), 401 + + data = request.get_json(silent=True) + if not isinstance(data, dict): + return jsonify({"ok": False, "error": "JSON objet attendu"}), 400 + if data.get("type") == "image": + image_url = data.get("url") + if not isinstance(image_url, str) or not ( + image_url.startswith(("http://", "https://", "/")) + and not image_url.startswith("//") + ): + return jsonify({"ok": False, "error": "URL image invalide"}), 400 + + with STATE_LOCK: + if data.get("type") == "image": + IMAGE_REVISION += 1 + STATE["content"] = data + socketio.emit("alfred_render", data) + return jsonify({"ok": True, "image_revision": IMAGE_REVISION}) +# --- Agenda / capteurs / caméras Home Assistant --- +def _calendar_range(): + tz = ZoneInfo(os.getenv('ALFRED_TIMEZONE', 'Europe/Paris')) + now = datetime.now(tz) + def parse(name, default): + raw = request.args.get(name) + if not raw:return default + value = datetime.fromisoformat(raw.replace('Z','+00:00')) + if value.tzinfo is None: value = value.replace(tzinfo=tz) + return value.astimezone(tz) + start = parse('start',now.replace(hour=0,minute=0,second=0,microsecond=0)) + end = parse('end',start+timedelta(days=7)) + return start,end + +def _json_error(exc): + code = 400 if isinstance(exc,ValueError) else 502 + return jsonify({'error':str(exc)}),code + +def _writes_enabled(): + return os.getenv('ALFRED_ALLOW_CALENDAR_WRITES','false').lower() in ('1','true','yes') + +def _same_origin_json(): + # POSTs require a custom header and same-origin browser requests. + if request.headers.get('X-Alfred-Request') != '1':return False + origin=request.headers.get('Origin') + if origin and origin.rstrip('/') != request.host_url.rstrip('/'):return False + return True + +@app.get('/api/calendar/calendars') +def calendars_list(): + try:return jsonify({'calendars':ha_views.list_calendars(),'writes_enabled':_writes_enabled()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/calendar/events') +def calendar_events(): + try: + start,end=_calendar_range() + selected=request.args.getlist('calendar_id') + return jsonify(ha_views.get_events(start,end,selected or None)) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/calendar/create') +def calendar_create(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée (ALFRED_ALLOW_CALENDAR_WRITES).'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + tz=ZoneInfo(os.getenv('ALFRED_TIMEZONE','Europe/Paris')) + def read_dt(key): + dt=datetime.fromisoformat(str(data[key]).replace('Z','+00:00')) + return dt.replace(tzinfo=tz) if dt.tzinfo is None else dt.astimezone(tz) + result=ha_views.create_event(str(data.get('calendar_id','')),str(data.get('title','')), + read_dt('start'),read_dt('end'),str(data.get('description') or ''), + str(data.get('location') or ''),bool(data.get('all_day',False))) + from services import alfred_intents + alfred_intents.clear_create() + socketio.emit('calendar_changed', {'type':'created'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.post('/api/calendar/delete') +def calendar_delete(): + if not _same_origin_json():return jsonify({'error':'Requête non autorisée.'}),403 + if not _writes_enabled():return jsonify({'error':'Écriture calendrier désactivée.'}),403 + data=request.get_json(silent=True) + if not isinstance(data,dict):return jsonify({'error':'JSON invalide.'}),400 + try: + eid=str(data.get('calendar_id',''));uid=str(data.get('uid','')) + start=datetime.fromisoformat(str(data['start'])) + end=datetime.fromisoformat(str(data['end'])) + if start.tzinfo is None or end.tzinfo is None:raise ValueError('Date avec fuseau requise.') + matches=ha_views.get_events(start-timedelta(days=1),end+timedelta(days=1),[eid])['events'] + rid=str(data.get('recurrence_id') or '') + if not any(e['id']==uid and e['start']==start.isoformat() and e['recurrence_id']==rid for e in matches): + return jsonify({'error':'Événement introuvable ou modifié ; actualise le planning.'}),409 + result=ha_views.delete_event(eid,uid,rid or None) + from services import alfred_intents + alfred_intents.clear_delete() + socketio.emit('calendar_changed',{'type':'deleted'}) + return jsonify(result) + except (ha_views.HAError,ValueError,KeyError) as exc:return _json_error(exc) + +@app.get('/api/ha/weather') +def ha_weather_data(): + # Read-only route, token always stays on the Alfred server. + try: + q = request.args.get('q', '') + return jsonify(ha_weather.get_weather(q)) + except (ha_views.HAError, ValueError) as exc: + return _json_error(exc) + +@app.get('/api/ha/weather/outfits') +def ha_weather_outfits_data(): + try: + q = request.args.get('q', '') + refresh = request.args.get('refresh_illustration', '').lower() in ('1','true','yes','on') + return jsonify(weather_outfits.build_payload(q=q, date=request.args.get('date', ''), label=request.args.get('label', ''), refresh=refresh)) + except (ha_views.HAError, ValueError, RuntimeError) as exc: + return _json_error(exc) + +@app.get('/api/ha/temperatures') +def ha_temperatures(): + try:return jsonify({'items':ha_views.get_temperatures()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/temperature/history') +def ha_temperature_history(): + try: + eid=request.args.get('entity_id','') + hours=int(request.args.get('hours','24')) + return jsonify({'items':ha_views.temperature_history(eid,hours)}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/cameras') +def ha_cameras(): + try:return jsonify({'items':ha_views.list_cameras()}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.get('/api/ha/camera//snapshot') +def ha_camera_snapshot(entity_id): + try: + binary,mime=ha_views.camera_snapshot(entity_id) + return Response(binary,mimetype=mime,headers={'Cache-Control':'no-store','X-Content-Type-Options':'nosniff'}) + except (ha_views.HAError,ValueError) as exc:return _json_error(exc) + +@app.post('/api/ui/home') +def ui_home(): + with STATE_LOCK: + STATE['content']={'type':'home','text':'À ton écoute.'} + socketio.emit('alfred_render',STATE['content']) + return jsonify({'ok':True}) + +@app.post("/api/audio-level") +def api_audio_level(): audio_level((request.get_json(force=True) or {}).get("level",0)); return jsonify({"ok":True}) +@app.get("/api/wake/status") +def wake_status(): return jsonify(wake.status()) +@app.post("/api/wake/start") +def wake_start(): wake.start(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/wake/stop") +def wake_stop(): wake.stop(); return jsonify({"ok":True,**wake.status()}) +@app.post("/api/tts/say") +def tts_say(): + text=(request.get_json(silent=True) or {}).get("text",""); return jsonify({"ok":speak_async(text)}) + +if __name__ == "__main__": + if os.getenv("WAKE_ENABLED","true").lower() in ("1","true","yes","on") and not audio_settings.mic_muted(): wake.start() + socketio.run(app,host=os.getenv("FLASK_HOST","0.0.0.0"),port=int(os.getenv("FLASK_PORT","54321")),allow_unsafe_werkzeug=True) + diff --git a/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/original/templates/index.html b/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/original/templates/index.html new file mode 100644 index 0000000..4422045 --- /dev/null +++ b/alfred/app/.alfred_main_v1_backups/20261028_160727_232470/original/templates/index.html @@ -0,0 +1,187 @@ + + + + + + + {{ name }} + + + + + + + + + +
+
+ +
+ + En attente +
+ +
+ + ⚙ + ▤ + + + + + + + + + + + +
+
+ + + + + + +
+

Bonjour.

À votre écoute.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/alfred/app/__pycache__/app.cpython-313.pyc b/alfred/app/__pycache__/app.cpython-313.pyc index 65f71e32b79de64202fc549a5050bd231f6677de..466a3d431d146f5d015e0c12417d01489411f120 100644 GIT binary patch delta 7911 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 literal 0 HcmV?d00001 diff --git a/alfred/app/config/control_center.env b/alfred/app/config/control_center.env index bd199a4..9a92c8a 100644 --- a/alfred/app/config/control_center.env +++ b/alfred/app/config/control_center.env @@ -1,7 +1,14 @@ # Configuration générale Alfred (page /config) # ALFRED_ALLOW_WEB_RESTART=true -ALFRED_NAME=Dona -WAKE_WORD=dona +ALFRED_NAME=Magalie +WAKE_WORD=magalie PIPER_VOICE=fr_FR-upmc-medium ALFRED_OLLAMA_URL= HA_WEATHER_ENTITY= +STT_ENGINE=wyoming +WYOMING_WHISPER_HOST=192.168.1.13 +WYOMING_WHISPER_PORT=10300 +WYOMING_WHISPER_LANGUAGE=fr +WYOMING_WHISPER_TIMEOUT=20 +WYOMING_WHISPER_CONNECT_TIMEOUT=3 +STT_MAX_AUDIO_SECONDS=30 diff --git a/alfred/app/config/home_card.json b/alfred/app/config/home_card.json new file mode 100644 index 0000000..3e74526 --- /dev/null +++ b/alfred/app/config/home_card.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "rows": [ + { + "kind": "lights_on", + "label": "Lumières allumées" + }, + { + "kind": "indoor_temperature", + "label": "Volets fermés" + }, + { + "kind": "covers_closed", + "label": "Température intérieure" + } + ] +} diff --git a/alfred/app/config/identity.md b/alfred/app/config/identity.md index 2ac6084..a6bdaf5 100644 --- a/alfred/app/config/identity.md +++ b/alfred/app/config/identity.md @@ -1,7 +1,7 @@ -# Identité et comportement d'Alfred +# Identité et comportement de Magalie -Tu es Alfred, le majordome vocal personnel de la maison. -Tu t'appelles Alfred, même si le moteur logiciel qui te permet de raisonner est Hermes Agent. +Tu es Magalie, le majordome vocal personnel de la maison. +Tu t'appelles Magalie, même si le moteur logiciel qui te permet de raisonner est Hermes Agent. ## Langue et style @@ -18,10 +18,10 @@ Tu t'appelles Alfred, même si le moteur logiciel qui te permet de raisonner est - Si tu ne peux pas effectuer une action, explique-le simplement. ## Identité -- Si l'on te demande ton nom, réponds : « Je m'appelle Alfred. » +- Si l'on te demande ton nom, réponds : « Je m'appelle Magalie. » -# Alfred +# Magalie Tu disposes des outils et compétences configurés dans Hermes Agent. diff --git a/alfred/app/config/identity_history/20261028_214226_191641_avant_nom.md b/alfred/app/config/identity_history/20261028_214226_191641_avant_nom.md new file mode 100644 index 0000000..2ac6084 --- /dev/null +++ b/alfred/app/config/identity_history/20261028_214226_191641_avant_nom.md @@ -0,0 +1,31 @@ +# Identité et comportement d'Alfred + +Tu es Alfred, le majordome vocal personnel de la maison. +Tu t'appelles Alfred, même si le moteur logiciel qui te permet de raisonner est Hermes Agent. + + +## Langue et style +- Réponds toujours en français, sauf demande explicite de parler une autre langue. +- Tutoie ton interlocuteur. +- Adopte un ton naturel, calme, cordial et professionnel. +- Privilégie les réponses brèves, claires et faciles à prononcer à voix haute. +- Évite le Markdown, les tableaux et les longues listes lorsque tu réponds oralement. +- Ne parle pas spontanément de Hermes, d'Ollama ou de ton architecture interne. + +## Domotique +- Lorsque la demande nécessite une action domotique, utilise les outils Home Assistant disponibles. +- Ne confirme une action que si son exécution a été effectivement vérifiée. +- Si tu ne peux pas effectuer une action, explique-le simplement. + +## Identité +- Si l'on te demande ton nom, réponds : « Je m'appelle Alfred. » + + +# Alfred +Tu disposes des outils et compétences configurés +dans Hermes Agent. + +Utilise librement les outils disponibles lorsque +la demande le nécessite. + +Ne limite pas tes capacités à la domotique. diff --git a/alfred/app/config/identity_history/20261028_214309_009521_avant_nom.md b/alfred/app/config/identity_history/20261028_214309_009521_avant_nom.md new file mode 100644 index 0000000..abce580 --- /dev/null +++ b/alfred/app/config/identity_history/20261028_214309_009521_avant_nom.md @@ -0,0 +1,31 @@ +# Identité et comportement de Dona + +Tu es Dona, le majordome vocal personnel de la maison. +Tu t'appelles Dona, même si le moteur logiciel qui te permet de raisonner est Hermes Agent. + + +## Langue et style +- Réponds toujours en français, sauf demande explicite de parler une autre langue. +- Tutoie ton interlocuteur. +- Adopte un ton naturel, calme, cordial et professionnel. +- Privilégie les réponses brèves, claires et faciles à prononcer à voix haute. +- Évite le Markdown, les tableaux et les longues listes lorsque tu réponds oralement. +- Ne parle pas spontanément de Hermes, d'Ollama ou de ton architecture interne. + +## Domotique +- Lorsque la demande nécessite une action domotique, utilise les outils Home Assistant disponibles. +- Ne confirme une action que si son exécution a été effectivement vérifiée. +- Si tu ne peux pas effectuer une action, explique-le simplement. + +## Identité +- Si l'on te demande ton nom, réponds : « Je m'appelle Dona. » + + +# Dona +Tu disposes des outils et compétences configurés +dans Hermes Agent. + +Utilise librement les outils disponibles lorsque +la demande le nécessite. + +Ne limite pas tes capacités à la domotique. diff --git a/alfred/app/config/identity_name_state.json b/alfred/app/config/identity_name_state.json new file mode 100644 index 0000000..fd7d79b --- /dev/null +++ b/alfred/app/config/identity_name_state.json @@ -0,0 +1 @@ +{"name": "Magalie"} diff --git a/alfred/app/data/alfred_features.sqlite3 b/alfred/app/data/alfred_features.sqlite3 index 319d6e2b1b56886c4fffb8a89fbc27e88b862ddc..e540664393b880e68caba0106ec21946f1d4993c 100644 GIT binary patch delta 122 zcmZo@U}{*vG(nn|hk=2C6N;H9YM3+fY)n|C&&4^N-Id*y<2V5^n{QjYvuu`e z*vdcoy1$9+QU2vXc$D9lPZ1Mt7F5{HKlyRI%4Ug#7xEJ)NN_R%jb{g9j?IDzANT=8 CEgr`J delta 101 zcmZozz|_#dG(nn|gMonogxP_ZX`+TXBge*sRr)Mk<@~=lHlF3)eB0WcWwVCEdj5$4 tY?H71n}}!ggCXB%2;}eNU(UaruX?kf!a@GYkK)N{(V?Kw7y zRP{ikzyM_Keky8%< D?NToV diff --git a/alfred/app/data/alfred_features.sqlite3-wal b/alfred/app/data/alfred_features.sqlite3-wal index 7408c0a1a11e9507bf18f9197e944729b6da9853..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 24752 zcmeI(&ubiI7zgm#jScCBbQB2`toXP+Y{lJW_E$1l4~u%ypir<{lwRJQc~>UceP=uK zPS{XdiAvRrH&1#HJfsVr1TP9jSPK3Fl0&>`Po6}ihf)YW@5C${*=p;hH2u!7O!CgV z@B6%;clJHJ%(ML7?E6;f*OQj@xMhy*v!B*}{^F^Xx#W%SZV!HZfr}oSEFbQC{`Krz zZ&w$%Fq_-6=67>@_8n6L2LvDh0SG_<0uX=z1dgh});rnx#^UMqg{{+8tL24C%YiOA zwYpoY`mSg!HB7;)suM;%p@UGpY^o@IMm|S=L2QIUME!(xK?J^x%JfpGl%%yViPUH) z>Dfzpz1f%fCgn{#`QqPK{{KE3%3rQJ)umF!DOKHbm3p({HtUV&9jECyhi}ws;~O96 zGYjVzw-&Fy`OHU8yxy|3h}TOiuCu(dEFIZsd-ZzTbK7kmr|Y=>a;@RGbyG@?&r(bj8^*MYw6}vEJQ2|vx4<27J=3~rqg!BQrGri&6!$w2o;I=QP?0D4 zS|$vL%HzCC@ZEwL zk>+2G&0k6GqY!A&L6+0eL#+VmOpa``vTUT&whIHmn(z)eSsU6ebc^SubcD4pZll% ziM?%apSbkkMva~!009U<00Izz00bZa0SG_<0uY!GczULl8NU(m&-aP4r)FBm$9Jc^ zFK{T2U~e~{Irdukjmaa(%X&F??_VE)>zTjqU%jJ;z`&ml0S58;Z4kRbp82tWV=5P$##AOHafKmY=d zhQR;MBPgwU<#uK6_Wr)WP2>?g8qW(x2>}Q|00Izz00bZa0SG_<0!LTiU-Jn50)JxM AKL7v# diff --git a/alfred/app/data/alfred_lists.sqlite3 b/alfred/app/data/alfred_lists.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..1dcfa3f14ee04c2906bf8a4dcc1f4b573234999a GIT binary patch literal 16384 zcmeI%O-sWt7zgmQ8!E%8w_cyyRyIWO;7PE~sAXOEVhir#sB6Sw*SWO=@hJH5{5~E{ zgIn1^JPP98WLnT>;#PS^p{b#_j~7}I4g%DhOLd{}QU`hKf* zcK9|e?|-qv-Y(m_-~W^q5(FRs0SG_<0uX=z1Rwwb2>i1^r?6$zYFa-H9wL!UBY80# zBw{)j@o>FaZ1lM0aq_GShtpbYH4et2E=QX0k+y9ZhW2r)=9CkT6IJLdr<2W$@0Cn?aGlY?t1lQ^z$eV$>Vn(xow$!$DzgzZ#1i_!*;1! z8BXT2L?o4ZB@Kp8^1(Fo@lo&~qSXoV&N#amN252f_7{0XIElq_xyuz?@+-@CJUYq) zhcgkRA{@wj_R4DWi1|zSM}+U%U7xF+)hS<_sW41a-!je&jY$yk`twqPc{)+;fl?kE ztEwQ6+D>-d%2{!c6-W?(00bZa0SG_<0uX=z1Rwwb2yCiADcczC|C{=Hu`dWf00Izz r00bZa0SG_<0uX?J62SL=90UkJ00Izz00bZa0SG_<0uX?}<_mlSXk(Vx literal 0 HcmV?d00001 diff --git a/alfred/app/data/timers.json b/alfred/app/data/timers.json index ebe7a19..c37d19c 100644 --- a/alfred/app/data/timers.json +++ b/alfred/app/data/timers.json @@ -73,5 +73,16 @@ "started_at": null, "created_at": 1793117300.2246997, "state": "cancelled" + }, + { + "id": "c834bdefc8", + "label": "Minuteur général", + "kind": "timer", + "duration": 1020.0, + "elapsed": 1020.0, + "started_at": null, + "created_at": 1793205436.8959317, + "state": "finished", + "finished_at": 1793206457.1510372 } ] \ No newline at end of file diff --git a/alfred/app/mcp/__pycache__/alfred_listes_mcp.cpython-313.pyc b/alfred/app/mcp/__pycache__/alfred_listes_mcp.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f5fdd834bdbac0b1c9b72c42de9d7c967b528ae GIT binary patch literal 8388 zcmb7JYfv0lcE0_Z_Y992U<5)mAjv|40rAiSJ!~XE0wEsL40?csS~CqAX{KlP_Kb}t z@-E5#Sa4Di_F9p&QPtv2HinI>R!Lm7ij&l8jU3BKe)ME)!?sB!+3X+x!8`P6Q8lhWAKo|*> zK%b(zX-xB^&75K|qd!^9>Q4@H@MKS!y3N?!ZNU~Ejj;YggXG$?3JIKGY5~fm_=~d7 z^|fGbp|bDTE?5MsV5_GEyWsFK*a7cO!6oDg`92mqg#vgh6x=Z467~q5QgoHVc_Vqi zqev(oaf~>H5+5yig)Q}T3!9~>kCqB$f_KExK(#RIdbbMYu*L}X4_Ygg_W0!^PT1zPSZG{X2Bo?QX`#fHX>Cy!1yBVn9P79+k-i4--IW*22SmJk!+n5^{H(3%-buuw^8oP==)Z*OZ$b5rA9jjGp7N&<)Fam^fzC9xtYnkgX$uSiOA zKQPPn@kjUw5tk21ywP=t&(2#vmRR@+S?a4{G&XTPE@`YJC!?A@Bq>20juU}DB}V`0 zEL6$8Pz_2GKvtR%g9(0NmKU$alCa%c=<)G!F)Y{eaVac^u*63sKAOb78W!7Oon{4< z^hok};tDjyiEI-)$&fiLC!}!+Cp+M+tDPT%jmcpkhxJ{Ih7(d$;Scd~yf8nuFptH{ zKwC`maxBWnVJ@&zcqJK+V@Xknj$uDpre@xaN4 zm({_M#lg$!rORsSd!Hdx)<%EEP>y5t3Zl4U^b=TsGz;uNKa1oxu!%PP$jv}hwolQrq&8R%#m8wo&ZqI)#^HERDlVFnM@Mz%X2Pa6DonnNVu^5Mh>-C0>z|a2c|f9TZCx7?P3`KFt}7 z5q&14?2&=HL&sx;(NjkRlcI1Iy2DzSuP2Tqu`uuB+hej!_7s!(aEJ%Oq@`~qclSXg zC8t1g+sOVjIvmRG(T*j!0dkge1q`(>%m<%kZm{(chqERs@nZtH&0!GjDQSrV2!hf> za6#nI!o2Pxl8G>E9}Ezjjg}XTNFp9J_8v-(t|u1g?HUMlobKoy+^B9Kb`@-b2uTzU z_PJ_qaEjpO7v`l}-3+t5i3^NJ!a;CNzF-bC04Ibj@UQz7FNe@YbkTMT4WeOvh*WMg zL@V@Oq|orW1RoTu5-6C6!t`lt_QR&oGJOc{mk3^SI0pkjAGGlal&E2{KraDm*krT~ z=(6g;o?g$aTMwMFWtezoy#!D*z$5@Fmi4k4A8li;@r>GriQ~?$w82V_tPD_Yv~h(Y z&4%5W3`!C;hy=3^EWl2NiF?wEKi_DrJ*&|0DsU4l#EB6Q)<6mNgS5=_bqxbx%oacK zmKzE{1IHk0paN6}MZnrY+E3i;h9cw{lF97W>uoNu`6{ol)u3#wUw4Y_pa*JGZso>ogPZ6hbA@tZ&ayRN6}D|+_Z7w}i8}jd zLLr$Ou#y^1(j*i<2QBF5H}vzQKVaGp_l$>snI1!xs1_NVLvX*wV59=^@Yg$&g@2W3 zn1^M0@e^OYp#aRpAgTHHo|AdAbJ}<_Hq{Y?1Oc;u=LW{`>OxJlp>^5PR{^pbn*k^z z!oA?px2=^e6{wFjfbN=K30MsM2FwyI!0y`2W7Il5Yy$L$YYq8y=nc0Hbh75DN&{wH zTF@~m>**T&YbtwnY?E^J5%0~1W%!KBNngs-hR2fxJ1Efiq8JGS_VERClyq{0PZPmy zC!ukas)l{vLILorCjlJA3Is$fVUQ*sMGrB+Pl1i;K31T~*o){E2Vn%<{cISqp8?>X zg<6OG77#UdP4XR%#DZc( zIqF*-B8W>NrugNj0Jv1o&^v(#)ML}H!svMJgAmlD3eVPMW4Jm9 z?dp2SDG}fu5)-0kib{zqu@DBELZ1@#6L}JVJRq(Kzi=!MVPZ5X?&RQLKc6av*vyCr z2`*6LF*!`4kyc)#W7krJ9T)=+;n$}5bZAsVWAf3(B)O5w>j5dHRRi~FCcv&>@--zI zH`v$LBWSiEF_M&k7Q}|Ouo9L5_sKzs&O+f}LSqyu0bv6KVj3gL*D(u3HM2h#QnJU| zgbs}*s@1qqG8zZ9N^rkIl4efg2*G8br*$aiw!yvhb%3eTNUqQrTw*NGlao7DMIa@h1_IlSF$+WvJW3GGq zy!t|are#2_A6T{*&4lhXFWULEoqx-wHg#oob*oj~%Z|L)*$+KK{}jl0hBEm>8ONE) z6U%1Xhc@55Cu8$1ntf@r@9nE<+fZiT8MWaI@S3`jF_-<;Rdh4Z-(ie0kjsU>?d_C0U6sz>`WP5o+J z|58cWY{}izbKBEf>(%0hDR%12v~$V3b#~X?i*xPi^1W(FBXpjfcERe&Q(t}Fggn0g z^&(f@N6y-BmhJgBpPzpI31Yb2%O3CT?$^5SC_kRKJMrhKjA!?v$Cvi_=3aVG^g#LL z#NSUmxT@~&%6Ph`hrV3S^JXkfvn_M(pL^%Mb2hcI=|?+NOViiOF7KV zmK_B*d#8Kv9ainTo4-()-CF~6VBkhVv!OFZ3H=KVcw)X6&faV^#5WggdU>9TSk zm}qE!V4+Ccnn&8+a+vx-OI|n2{0qxL`zcpVoCtg>s3&hh zf;5bnh=F&eY&9luKG(saKmrW>qsG02AppWBVJ7Y()48!oQn>;yl!2UxLcZI4_DXYD zibt+v33^C)ryPK45-qdlA9Vj=_af^_v!2%nZ=Zkd{48_7=wVH|sORxux@h1-*8ia} zv?yFi3l|oJU|I-%BuJQipPo`V@vXYbOQ8mpuL6bu8eUbp0iSlkWKu*3ognFhbkGSa z#&yM#^N8hdb}S_AW6+T4Z74tstdS87YzWxgP%wHMMho>(&`|?~ z8-0HYJ#M`x2^=<;=v#Imn;^U)n%B#^P|~!i_B?CaGqeH$#wO-36dB{sYHd|JW}$f4 zVes~^hK)v#9xj<7J701T^6Wmk9)d(+i;sp~yNBJzETIgbA!OGfuTBdUzSR(!T!!_t z&K3#$)1v^tR|8al-w>|J7LaK5f~~>o9QV>JcgQ}&%r;AJL-x6rd)`2C2cfuL29UIJ zQ~J-yZtxh-@mk|iQ`L)sFv6`+X>hqAIhqIq%1%j|iCpujqUAyIG7$^LB0~Vk;KTp5 zt+Ao8rLJLL-QEL`=eY(eQ@O6Dy2gEaySZ-f-nxbsjU#DBMY9tWcKnJck4vG>lrx*4 ztRtP0lH&G~d^WfjJ-2CoG0P=M==PITZZ3^ipUcSA^4atbq_H8hMKD=J@saeK0%={q zU2#z%nVKLZhIB|tQe{R;4su6Eex9U}a;Z6j%}7%ED$#)>D8|LhkU!FswczY92-GEy z#WV*2T{x+uOC@E~cUDjKps{htz8gINB_r9bSOEcLLl;Q50ye|MwZcA$P2hyyCMkOz zmO{o`(c#lzifQyUgx{R}-S1NQr*$L>S?vVLfXg9R+)sowM>rrcrbg6 zUmNTOa)Fnf!Dk?9v}k}R5ex{(#NBa z$#IBAh(>GH^HfgA`+g2A+lb%1CAB^JfO1Rz3q$k zinP6Aws}tZi|6m}Te8@v+NZ6P$A4UMw`A5kSC!dPH?wnv_P{T8OZI}9!x?+UlDqi! z{#WEkOVu8Kw1j@ibkwiTLn_EF0v<@bWKm2d8NW5=9n4ri(w zGuxW(xBa5yZ#o|A$uxH^HupTP%9Na*F)i)#y&d|+)xW*^uviV8PwyK}^CN1-1@%fa z?U7$)zOW!?;hn%7^Pp2bJ(_O5lr9uMbX-=0;bn{SPSNb~w5#PK%f2taup&=6ARgdN zCu z%{)39iUmhUHQVUuSTaFEy-^HbCZyXX1XBvW1aF!NVix$VO%HM~F?h`c@rfkA2*yr+ z2#ezrq?0|>)!VLdJ#EK2dNjJLUH`FT&Ej?I)~%9=Nq%tw&K&uX4?@RCSeD2OA=U%L z@C$^Zm4uG^CB-Cnu5pl$mJ>SkkucWt13D1N;v381Xe{4FItaWae zTG6B)`-8Ok2K3~6X9w;#sT~t(%k@ca$yq#GpzfVWbJr*7FIbv7Mt^2QoOP;n1)-uO z3-x!|V;nqAas3?pn)4z%K;8uI9C@7QMDp-+m$@ehaY63atYvDeS{zfq^O8DoLyfY%}Kh;{QmfZkq-=jJR(N4OXJibev zB9GIwKps8xpw1~kuQ<(CdXoFRu$0RG`~XFbQJarQ%I+Lg^LAt` z7v}>IJwC2hSMg`vMYZnYH>&00*NPVocwxJ@ou(hrmiFhkN4CQDD(+Dg2krj@mP_dG literal 0 HcmV?d00001 diff --git a/alfred/app/mcp/alfred_listes_mcp.py b/alfred/app/mcp/alfred_listes_mcp.py new file mode 100644 index 0000000..91e9189 --- /dev/null +++ b/alfred/app/mcp/alfred_listes_mcp.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""MCP stdio JSONL Alfred Listes — stdlib uniquement, aucun sleep/terminal.""" +from __future__ import annotations +import json +import os +import sys +from urllib import request, error + +BASE = os.getenv('ALFRED_LISTS_API','http://127.0.0.1:54321').rstrip('/') +KIND = {'type':'string','enum':['courses','taches'],'description':'courses = liste de courses ; taches = liste de tâches.'} +LABEL = {'type':'string','minLength':1,'maxLength':160,'description':'Texte exact à ajouter, ex. pain, peindre le mur.'} +ID = {'type':'integer','minimum':1,'description':'ID fourni par list_items ; préférable au nom pour les suppressions.'} + +def tool(name,desc,properties=None,required=None): + return {'name':name,'description':desc, + 'inputSchema':{'type':'object','properties':properties or {},'required':required or [],'additionalProperties':False}} + +TOOLS = [ + tool('add_item','Ajoute immédiatement un élément à la liste de courses ou de tâches Alfred. Exemples : « ajoute du pain sur la liste de courses » => kind=courses label=pain ; « ajoute peindre le mur à ma liste de tâches » => kind=taches label=peindre le mur. Ne jamais confirmer sans succès.',{'kind':KIND,'label':LABEL},['kind','label']), + tool('list_items','Affiche les éléments enregistrés et leurs statuts, dans une seule liste ou les deux.',{'kind':KIND}), + tool('complete_item','Coche un élément comme terminé/acheté. Donner son id ou son label exact et sa liste.',{'kind':KIND,'id':ID,'label':LABEL},['kind']), + tool('reopen_item','Décoche un élément terminé.',{'kind':KIND,'id':ID,'label':LABEL},['kind']), + tool('delete_item','Supprime un élément précis demandé par utilisateur.',{'kind':KIND,'id':ID,'label':LABEL},['kind']), + tool('clear_completed','Supprime UNIQUEMENT les éléments déjà cochés de la liste indiquée, sur demande explicite.',{'kind':KIND},['kind']), +] + +def invoke(name,args): + if name not in {t['name'] for t in TOOLS}: raise ValueError('Outil inconnu.') + if not isinstance(args,dict): raise ValueError('Arguments invalides.') + allowed=set(next(t['inputSchema']['properties'] for t in TOOLS if t['name']==name)) + if any(key not in allowed for key in args): raise ValueError('Argument inattendu.') + if name == 'list_items': + # The GET endpoint always returns both lists; restrict tool answer if requested. + req = request.Request(BASE+'/api/alfred/lists',headers={'Accept':'application/json'}) + else: + body=json.dumps({'action':name,**args},ensure_ascii=False).encode('utf-8') + req=request.Request(BASE+'/api/alfred/lists/action',data=body,method='POST', + headers={'Content-Type':'application/json','X-Alfred-MCP-Local':'lists'}) + try: + with request.urlopen(req, timeout=8) as resp: data=json.loads(resp.read(1_000_000)) + except error.HTTPError as exc: + try: reason=json.loads(exc.read()).get('error',f'HTTP {exc.code}') + except Exception: reason=f'HTTP {exc.code}' + raise ValueError(reason) from exc + except (error.URLError,TimeoutError) as exc: + raise ValueError(f'API Alfred indisponible : {exc}') from exc + if not data.get('ok'): raise ValueError(data.get('error','Erreur API Alfred.')) + if name == 'list_items' and args.get('kind'): + kind = args['kind'] + if kind not in ('courses','taches'): raise ValueError('Liste invalide.') + data['lists']={kind:data['lists'][kind]} + data['counts']={kind:data['counts'][kind]} + return data + +def reply(obj): + sys.stdout.write(json.dumps(obj,ensure_ascii=False,separators=(',',':'))+'\n');sys.stdout.flush() + +def handle(obj): + if not isinstance(obj,dict) or 'id' not in obj:return + mid=obj['id'];method=obj.get('method','') + try: + if method=='initialize': + asked=(obj.get('params') or {}).get('protocolVersion','2025-06-18') + version=asked if asked in ('2025-06-18','2025-03-26','2024-11-05') else '2025-06-18' + out={'protocolVersion':version,'capabilities':{'tools':{'listChanged':False}}, + 'serverInfo':{'name':'alfred-listes','version':'1.0.0'}, + 'instructions':'Pour courses/tâches, utilise ces outils. Ne simule pas la création et confirme uniquement un succès API.'} + elif method=='tools/list':out={'tools':TOOLS} + elif method=='ping':out={} + elif method=='tools/call': + p=obj.get('params') or {} + try: + result=invoke(str(p.get('name','')),p.get('arguments') or {}) + out={'content':[{'type':'text','text':json.dumps(result,ensure_ascii=False)}],'isError':False} + except Exception as e: + out={'content':[{'type':'text','text':str(e)}],'isError':True} + else: + reply({'jsonrpc':'2.0','id':mid,'error':{'code':-32601,'message':'Method not found'}});return + reply({'jsonrpc':'2.0','id':mid,'result':out}) + except Exception as e: + reply({'jsonrpc':'2.0','id':mid,'error':{'code':-32603,'message':str(e)}}) + +if __name__=='__main__': + for line in sys.stdin: + if not line.strip():continue + try:handle(json.loads(line)) + except (ValueError,TypeError) as exc:print('Erreur message MCP :',exc,file=sys.stderr) diff --git a/alfred/app/services/__pycache__/admin_console.cpython-313.pyc b/alfred/app/services/__pycache__/admin_console.cpython-313.pyc index 5e2ce281ef6bc86d77e1dcaa7a54c279dfa6984c..5bf831fd25d27edc5e8898aa21301f221b37f7d2 100644 GIT binary patch delta 1103 zcmZWoOKcle6n$^T9)HG;2fGPvDfZNL(%5kfrEdAv6|IOJ+=2jOT45=cm5dV;XPmxC zLPKH_7A&AhMS2pdP^qeJXc0(3mVf{YQrB@7AZ$`d83j?56+0t$14V-S65Js17H7^q z_uPBmNAt@Y+*faLZB4t~ieU8)+PH0OUl_|Va|jyP@#&c9W5;>w#KNIRIsH!AEFtDi>?g1 z0tq`kfKiHCZ^MB zW=xs%KxG65dAn+6_YHCTx%jHs^|LK~qx+$SNWJ^W+egpF-;BRgtlK&poWY*PKS_Ho z#xED^zQ}dy@NF}a?mg#Q*j9Ub@Ou9AYW~%_W2R=F`J>rGi>I=4cWygT`>yjZ)m+_8 z#6k7CKX^I4>W|ce$~|x4G33z1;DbUBl@6F-Xk#V6=Eh--v2=KAb-tk2A-}s#8?njLIRXk z!tK$${IbOr_43QRFyme$!-LkC!Bj;&6jTixXtKuqeDxlC%*R*Vm~o$xVNmMd$5#*a z^auHsps{}+&6CgYHzl9;snJM2gjb@sP6zNu)IL3eZsJcKVRTVD3${uo= zK?Oi#e;Yi+6Z7OGFtSKJGeKN09iJJ)xI({}jfe}(TVk+CxndZvmmV&@i#v82XK|B9 z!ZrrJhJnS6N9eDwRfVsa`8&EW_a**-=I7n`M|x&{#Iw%i7ul2(4A?x~d}ypNG?`Zp Sliy0t*RNqqABQ~~`F{XQ+CH2B delta 518 zcmdnf#dN=wiSIKnFBbz4#NBw16(6>dFD;v;jG2L9@|{w(&GWMLm{g*9LDC=)%oxlV ztqNf?FqlFa!Q7V2lY{csGGuMg|5=;mPX@ir9;;0+ncTPR=PbVJ+qavL`<)3}h^uY*F+_>_r1X3`}4E-k8Wd^bj35&T}vK`W81Cob1 z#a!*#4lA>PNL@BJF^0p&`fjYuN0c~#>?5{pK=ntNMP1F9k4gx;8ZsZ%W(2VfS%Ks+ zRxUST_GA3aK$XXY!K4_Mn+5YR1ztB}=3`vCj3ACNtDD8-GbP&@4{dHK-Nwpsi!CQV zJw3H(vVFA{*vLnZ5u3 diff --git a/alfred/app/services/__pycache__/alfred_dashboard.cpython-313.pyc b/alfred/app/services/__pycache__/alfred_dashboard.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b375180834d1434b630ad3c0df1a8a2ef46801f4 GIT binary patch literal 14902 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 KmFx=r*8eZ)YlA2N literal 0 HcmV?d00001 diff --git a/alfred/app/services/__pycache__/alfred_home_glass.cpython-313.pyc b/alfred/app/services/__pycache__/alfred_home_glass.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99f192b486a2180e892f6cee509703dfa2a8237f GIT binary patch literal 14977 zcmd6OX>c3Yoo6@h8zjM-lmuQ9K}n!)iMnYXrYMP&&;%`;l3@^Nk}yGl?gk`_$vTXu zQl{)wL?w1aZL|~0@rE!}_|3@@My+kn- zM=>;`IZStJIE|Z>wTHEwR{GU(I{fMm>$?q{A=_r;j8dD4GfBT@&Mf^}I17IDhppW< z&em<`>=L};u%p|_IlBwE0_od$xUjp3E9!P}uI^&4SgJD}F6l1iO5M~+LoIc(Y&+IP zl5ey8o1bsbe)zWJ*UDwaOt!{Vo~&dn8?6+@SV_)RoizJ)eI2@%QpVy|F*Zia*u5HV zHDLFDBdmKf#W+T3u7+_k1x%q=!xS+t>9?3EA-~O9t`;z*fT?53Dk-j>DM#MGR3Kl= zR3cx;R3UF;o~v#9bFd; zg_3L}>V7yHR&ZaxtYjk4}=5OYU^WZa?1aFPp>)p#v;$)R{?(He}! z;t8pe@4>h3STHf-(TFBY)4_3ZPBeG*^&OLns5bi8r;;q65RFML8Xsq49<6A8D4Jx) zxkwDPPhw}0p|hfqli@I;JxJCcAp43(*}DW6M1#DV3CBo$j1Aye!d}vISh*=O6Z`Zy z_Q#6`9?N6-{Wj83I~0tGx+r!g>O=9QbqXc%WXi~wjB^Kke*6G?RuUX!qoQ3}7as^k<2)NqI?$^hV<&j`5X+$l zFBSnh9_IoHb_}ZyCXyVREC*O0zHeQXE8I9vlp`z}_iNF}ts+?s$!d{>vTq7SiZb92 zF!cQ&BY%k+P}}^}fJTBqwmn;F%vJ<3B|{HnsRu>*DdnB=nthtxyD2J&aT%!;wo_Cu z?WP8d%4m#MfoADuDn|P$7xggR^#h8EY4YaR449N2Uz@*#q6YAu<>c2W=kfs~HZjPV$~)36686kIy0N$K;zQO%4+ z*+Wd1w@2MTfik|*uC5%kXUQ4sd<2V@C53J&D_C;=R_P`xasHs?y=4P9H90Z1O3JTu zsZ>wttG26F8+QV8+U)?94k4L*989zTrI9<0f+$ zr`{7&u=_=d3Rk1GRN0@8Jc(tgmSPJ1hUKRmUb@qw+)n)lU)8c9@X5Fo2&R}UW&rcb zX$8C}QDAXyEaO}UKtJsR4_==9Dqgzr5{}~BtdtwD^^~Wyha+fum%QVN&21y`2p{1< z>=PX8-t1-*?yYW+nHZ?bcs$9uql8n0(I#^Wul)YWl$;xxXeJasaW)VMPq>eeeiCoV zr|+O1r-RW*nDug`n`n@GB*{km&W=kRq#4~Ya3XM-I2Yk>UG>W2Q+L+_ZV83ap<*kc zFoc=bVP7wSUbv_(U~`PlyL(zWEr!|+dUKk3N$;kP0%yrt4AVQcL$pV8*Ns64v(d)6 zFi5?|yGf)&JfTl+x45x6KAva}Mu))4nuFux&4e9-B=gz2fW$L~3B&Y`pB2q}huIi- z1-D;xOM3>f4{-4_ymv3yL@>(l_o@(H%)pb$?^2Ug>iF25;b>|w^q$Y3>VHJ=ok$Iw z`oMPzm0$8?zt2{!qLbJ()ix3Y(<2l)l1N5xU3G`!kpy9=2{wj%-0Z$J>)yJHaQ9#+ zA#n#Uw;KJp1~S#t$R?@=f*U7Jk!;h(mcXf|lU}vFexm-6#IuJYgz0yIBf498J_5Ff zPxT^2+RKp>N`f#`?eVc-1oyrSc*$k1j7y#Z(E{v?km}GdLebuoZSO*X>!R_3amF}XBox$7>B$5QuedVKhP1O`?)b0k z=Giw!UmyL;F=0#l`y0D&Yd+Z6{STq^@v-!9R5(7C=^0D+j7bCfgd!$oV;0J*U;WON z@BHH5rJdWQ*{632)~bb~l8fsuteaUs%LzqIQ*Dc`RWqSiwq{)G(yn#aw$Aad?V1n0 zw*T#-cdOs3PW41nv8OVvT)LH8EOn=99u`WwQUzUPUuVyqopHVJ%uF!tbYC+H&SnYV zlu-J3s^IYjcinVV+O}%JWIb;=XPMrXF_nK(M->#`tEX(v$?p3&dn!aSC6OBgS)=cN zg~ChJYHC1fp?q0^L#;remKq>WOG-Xfm{4j6vt%?aDiMiM17@YoPcd3TtQ7)NPt{NZ zl2Sw<28eVhr5v-*v7v1`BNg-jwWxt21B*YlM=nHbu*>nbgp*jOkYj>Lw!Hq7R+iaH|P zLOzTK!@Ow1L+U%!^DrZtIW`!E6!>&PLTu64%Sg|U+ktMP=`edXD}fV|Ef;O=9i6TI zBYgp1?;{MilMJsB4P;<8%yr;1OGV|^BTqmc9`^{^_L6pTdy~{XjZVLHv9RP~<&P^b zcV()*>1ywFW2)Mlsy-+bwoVxqoz*k_vz?b77n~bXrVU>#+FUO__DcD+mXvk-eTvp^ z{awRm!MWw-lQW?gb<@mG%`cz)@;&F4JEkr7v}pZ`Cw%wWZ9AzqYFl?}Z&sAFHXCkw z&7|ByBVWQ@$R$fi^k0E8OXXMPx^X=!I{8!-LceT!05Z`)CtO=j3e^xK$c+TaVR_1H zRgo$O!)Q0+BZp)@RrbrlKuXam>n6~Qp5#hROtYFuRN$P;6JubEjHyMVp<>#BVx`rm zN<>Nxc}9%cPcs&;26CDe+OEAt2U$bwtH{A9CB~u5EJ3Uw4=M5|PDYi4e1w55OSNQ% znq`&vRQl)UVPJ(|RE=ZFxV2yJk8w4OQ<3VhN{x?j&SfcgFS&UDNd&TG2}Gozg?57& zEnf$b#%ibys%8C zKl?N|ZXiC&5rfBFqPb1zyos@Kkv_v6#us;lWJFvf-Js#}(AOnG(s`HQEoyC0aiJCl zf_x|v5e?8LF|_)(jr9t*l0MyiMk`bZHGlO z^yHC1EI0;XK+26g3o8PaYEHyQ*;rtNeOlBJRiEo2gXqFZELMMp1J@SyL(wEZB5LA1 ziu~DFNHh#ZAWX!eosW#g!^93S9t}d!FeGEqNNm)jleQx2CB80dv2m^)!%6}dVfirM=4;uIAzF?lIf6+PXdbQ$8 z#Vb|U27fj3%aM8hjfvMM{_?x;dD}nNQ$?FUGf;Nt`L5@?rkRAJP=kjT&S=_9ah;3V^+pu!7v!> zVN16)rgejqgY43bjj$$=UpwPy(ZZ~uG7!IR07eHnDd+is8JxKp9)t}(V!c|HFoRn2 z%~-}1NNlu_u=3>?;@5o>Nq?B`sZfaegUCJ^Axo@_dN{(*If5Iap{q3jD=0@efk-!t zCVcnx9`5LwFpeY=W_DcK;hBk_UCbmIx>Q29*e^S47+Vevx>c?mOtn0skp(n>0jnJPb|UqZhTY_!&_* z7)00al4b%3E)z@yc|t1)Q^;}!hkOiyAuc`^7zuJOpt%o!yh~;c7RPz-^IpMHIobBH zxp>i9INg}GR;8`$=GuO}_IgEnOPjC(ic`f*W7<^*8f_^#Uv;hublT=P-|~FR^obcx zu-0a*=+!7#n=;nTY3t_sI>EYQvV9RShn_z){p4(|U~945Sj*qR*pC)Jf zUJYCc%s-lGIFN2QaAVuM2W}onF{d(ZkEh!nmlV;Hg5y-mbn17ytsh#8r~77(rb|5Y zB{%4_^&t$Nug6N@dj-e7lxZK?hRJc>e$GDAcWv9R_WyFfuzt_=!SworQ}(o}^>Z!u z@CU1N^6=+XDE+AFtItdE`@<@!?km0whjZE8QLcTvZhc3S_MLJ|$2#LXO_q)pV<<}^ z0YUVeto;223dFL*(0fn{;#h|@grG8dSvF;01C`}Xuf>Pf<>`bf^T7(NRa!yhR)EdP zrTbI@8OEyc#8rYq?6(Th3A#bn2u4WBuT_W+Y=l@oOrb>JB&ofAWeAEX z(m|E;xO(8sFeYQ8K**SAO~!(!gAv&DqpVBgu2#;45K~<6{udT8VKq}ulmtctt z#L%0B(3VINme(m6{F5dWkKy6NipWUb6|>HH-x~1L$WeM*FpSno0Im zDL6~;`6$?EKHjdZsVdm4{Q9*4%~9z0#Pkr;#o)z&OG3vqKlRt|TThR;8D6m%%EWsVX%3 z?JLe@Q+g@CF-LFL17=V?GU^PQAd0O#uByW`PU&oxu4UzY7bt5`ez6wK(M#BcDsuXB z;}%LnIm9%-WkuUWk>4uu1e`SE>ZO@tzhj$9WntsTEgXGU##g^Z-4W~w7*7={b4bUO zRKlm;Lc7tK0YIZxR(4a}Q3LSn~&=jnMuW3vKnb&uqP$XVOD^EeO5m%PLRN0c}SXY~DN zP{{M|piX*QMGxTNs#Qxlb&28;EYkS$DXM%4kSmn4gkQN!^$&sgNd6(Mn!T(ysuyz&2*k5P(5$R6WQvG5*l4mo!f+1F2vdkxj!Ax*iHiNDqJ=JxAbQ~i%- z`orn|aEcwv^v8ti9eF1+@|p}NCK9gU6>ZtLixBbaiPpT(2nh@BlI26}2u5KH0|}BXWJ}JT)Vc@?6X8V`VhLG+ zV5jMkx?H7To2Q>9t|Ka|QJD5dgJXl?;C^xS15hU0ehz(ja`%(e-0``me%U7p4H!>5 z%AOVJ;UsxL?ziUKxV0!qLKM){P0GJOCYn`Q(&Lb|AY!|hWh#QxaI7WAL_P6@@e-yJ z(^?#onl?Dda}N<%qUp$5mJTPDkYp*3Q!=MXN*zL16lF^m{%FL`MC0+%nnusVD zu9J+RI+Eq3Swe^&iV$Utm!1^&OR^pvxkHgo@Py;xb>%17MrX@EG4Ail$oS*+xQ9vV z!!5f6`?0@RJ9qpy-8Y{4hx*LkUcu4$!_aii#fA$FGghIfUT`$bK6S^@`1;z(&XoPw zg55dU38NcaUZs^YJ1#_L+vm!z9Gzkp3RX|);e?ysBUo227MEStrmIG87~Zwrw7u;R zsz!zKQK2~ce8+;la5^B^>$4#78=dbSxq0O69-%5Ol*fhQaS0?<;t}kgg|dpN?uCjq zKd-)2{imxh=%=*{C8Zbr7yL7Og_6chiFfXyX&nrMh1(X&Dla~B;hEVdGi968Wt$f( z*Uai(HC-{KJo^Rr0ip8XC3>Nv`sb@Jt)4SyDz>C6w#+Bqc>49HZ#aaVy(#7iVdoR1 z5B;kCihg$Z7Y?Da`4atMW$j{F<;>=voSxPGb$fGq-_hH_)ZSyM zV|}UJ{`rnC$T^%4?5h?W#pj(*uvl96O4}U$EB#gd-0*9T zOyl0`wL<;A>refcHFh&rTLy9MA483E+v(#(%?$ujW0`#DwpI8 zj6U*smnBA9ne$y_+laYtCHeSUkEvT>Ozi{KshS)A$T}glEAqMW8`C{tOx1MAKoVD> zp*yT3<>nlnh|Zuq<{q&)Yq-e;Sp-gRo+y>9P$c4ki$|MD+eQ+!7s@|Sa&;2@tw$mM z>T+tivK)qPp(VQPp(XkSvy=s$Bqt^JpGb+QWHOnt!0<$Z2NAp|uH)EYcu!a^KL$h% z^T+|dgx)+6awjP$iUnnt5B=?Bos}m|@Mh5n zt|U&LgmiZkj&7#6Cl{xJW<9ZCaFJ;oC@d-(_`2G#DA9#jkZ6Pr2O?u*Y&Zgj6Fuv( zaAZ-UiCi8rdgd|5U1*T;g7|OMWKGRc$RhJ)5IqTt2 zC*YN@xMrDOl&4)ya~P-jeOvRt!kKQ~^c7E>u{RBl)-}}4H3bjZwKtnx51F)YX=s$+ zGU<^|RDS)5;))^&1?HDLQ)0K0@VyxHR0`O@HOWbNo-v|#Yoz$DpAlOJyuL&VSeA%Q zP{DUN4strBE*FfUw5#ud=z}Z9K;~Q?jEOw0oSq}aX*e0|j+C%07hf=2$p-R`NdIB`#_>H#Ctj2Yq;H_#s3B8ELnmkdy6KGAk=c45BF+ z$Otj@XnUkFx!;pH5TFx+w_zwA4Re2miadpYyN+*h?>Dj;5-}H0o>rc6nbi~s`{H5p z0|}YkJllVzB~!OOUAJAR+j*V7UjL>sU3*aGZhsA!$Rco^EJzv8W0$-dGM(E22#)Y4 zj=Tt&6cXAe+hezq(m|57lI$Up-9qLu%08Y2ltm+ADan5y$Z84En7Nlpy-7KHp3sA= za4$1l`;rZcP2c`7{v1Q`Um`<%kwI&Y(~GVe!BscadCx!_H$~ff~qGmvOC4yVlOt{mOIIGvD@__j+NvY0uj`g{JNg3(6OY$}cuuXqq{R(6>y{ zrn$kXj>VGlnUbGZT&kEoAXIG4RP3JTg_1qj+phakB?qUv7HqEbJ?DC6UFoXrAJ}$$ zVyDVh-E&apg7da>wsgh8DI44;i_X$ne_<3ZuE!Bl8C6&_AKHj;KlQlqhyEB3xECei6Q{S{B#oX=X?tkf-Q zWxG*(tD&r2qrJ74M)_@x9{EIxM5nXitt(4XL{ybX^rtA~OHvAM$B1}9-h`CY+Pq-? z6SP8l!S+?+?bqbUG^2;17UpcFw`!O%wBW_S&_obVN;z_~QoNl+f@*+6Qa~^(Igz|tMAJd2SrSbLqgD*2tu+X~QH2a+59h|rlBueb`&$&3=m=!vKz{mu z6AE>hm=FZm<;|F|kfF$1$;KUH36fnY8+Xk8XEgOl0wySxWJ7ubHTfZPvi+##kKjBk zOVac1<^#uOL25CAR7L(*zIW?ATc0bOJr z+DA$&z5IP>$cE3z`o2MOyYM2-Epxiye^#<96xV*coe;9e6YuLJ^*`;UFAPjU`PG?& z>H}VMLG2Y>8x%37^BR-hbnLgK!lq-w#$##c@s#PfB+o2;JVE-yfq>w8;?6(-5!N$j zEEZIZ-&Fa=22pKGD-C(%6-bBjV zY2+YmxMI+*fW$BY0htaGbC2izmf3->t%8%Q$v?JL{1~({uw|&CeVc5A|}aPlyZ5l?Gnt}CLj8P z-uPVq_xn?}dO_c?s4vRsYts6fPc*dNb6-mtYDu>Co(`X&iU!kjk9_};l)XXFug!xs z={sl@c-=i6`9|Ciprw%;1-&N^+M(a10yo{$;q#tbg{>3x^`B^rdY?3418f>7-YcT? zri`vKt*e~b^1g1h#E?96k7vDT3$0)ZjK7T`n!9C>k4^p_k9fMB-7NHj9Y z=}7&4l8qokz}N_hXeEKK-y`2P{EryGyplQ~^DuI}q9a?M{U0%oupo{QJ!13<@}tr< z%j`~84wR?@PP%K{KM69!8WCM4*?GA~NoSp}x3^D@bkqBez}?BQWZ-9|HtGBOYr`GLE>vk>JYf>9J)4GT68FdAkN!zCu v%6eeZ_^F|kc7D1=PjCISz)lx_>UPovpVph{s?W+-(M9(U(wpct5_0@s$^K0P literal 0 HcmV?d00001 diff --git a/alfred/app/services/__pycache__/alfred_identity_page.cpython-313.pyc b/alfred/app/services/__pycache__/alfred_identity_page.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16944fc494d47254fa4dd81deaa14f60adda6037 GIT binary patch literal 14135 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 literal 0 HcmV?d00001 diff --git a/alfred/app/services/__pycache__/alfred_logs_page.cpython-313.pyc b/alfred/app/services/__pycache__/alfred_logs_page.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59f6e8c8590c3fed49da4a18e4429e3471be8013 GIT binary patch literal 7809 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 DoiV{I literal 0 HcmV?d00001 diff --git a/alfred/app/services/__pycache__/camera.cpython-313.pyc b/alfred/app/services/__pycache__/camera.cpython-313.pyc index adcb34bb9b6079054eca462c7ad4c958271dc92f..8ad100f2f0c726ad13f2f63b52f941ef8814de94 100644 GIT binary patch delta 594 zcmYLG&uJvdtc@?3Z@%}vFJC5a-k85lV`>;W!CJ3>ef` z3DOv{6jYR@=-hv+rPLDOC^>)1vm`?-R+H9jBt!Q8#S=1twzq1PdySn^wc=^eU{|RQZ&@y+^juGC*?qe^fDdega`?rn`gx2p z@VM{bzM4+ePOXREtom20K5lfz-ND2fez`cDL=MLM033CADU03_jg=W0CE|DUWVCu6 z`Nw>`OxOuw$WK_ZymS-{oN7Fc<~1&&@FP@=&toHkzOo>B60U`l+Br!%RKv@35uEU! zEI;!J`5hu8A%fuXw)^0P-L;!y9$p(Ox(LgWd^{W*8@Cd&yCT7lluZ7K$RoHKf@yAI ziAAJdc+hUzy@uNt_9I6Wk8+U~Ey+MH*nf&z6SpeXWUp@Hj%N=Sa#^B?a7dtv-T^ydS)F7NY*Q4b68>vWCHUV8Yu^REqmJBh?flGJ9Nwg?VjJj;ga2M`or)G0x z^$Gs3Qxh~kM2IQZ^X9SCAhwCK+X!T|7PpmL-&U35PuV(Ocub1jeurFu$5dl9>&tlB h{+H;4s)EpEJw(=zvSzlUpwpCdY7V9bD*7xce*lO1emDRC diff --git a/alfred/app/services/__pycache__/hermes_client.cpython-313.pyc b/alfred/app/services/__pycache__/hermes_client.cpython-313.pyc index ec74a92359d1d1739bc6cbbd87b30732f7edea29..efc2ede4f7892ca90ed1c0bb9ea7ff9bb95f4a5e 100644 GIT binary patch delta 1562 zcmZux%WoTH5clV`*NL6jZQ?weG)~gI<3xnCDODfj;iNC(#z}K;gs+2`MTPLVzLxmx3rWn->zSh?Sy0kgCqj`V47Q%Co%hjMU=Gv!1AUEuc4n9~1X5w^~!VVQco9k~cwF zEhxH`ip4EUGYuuL=^VOHr-&cRo@1R(TX~IN zt|k9^aVBz(#l@?UqbIs4N;0o4xss)+(2Rer073#E*i8vMtM286lW5e$qsXsp#CcHh zm7n#B*JGWLVN}C$f+6vFtS?H{xpH1Li`;;u*p3~O=_nWxjfbAuuQj{E_Y-H(eA`hE z{Uu#Fh2o9ifNtiNwOb6Mo<~Jb6jJztNd&k2x@IpbMz+A+03mqi1?V(4lvc(wsnI!C zPAi#-7c+1{d=?)#?cKDTaw^5Fy$ESaHiPh=`|h=nd_h5vFPz4j4OVl|@eYdZBxn(j zYU9CcDE=URs%>MPqO$J8!T`G~0;6*~0jbrZhRcUt-fSF*YGjguTP59wX7P7p?GbNgF~u~vwP@Ok&KI(|HMoh6 ze>#Il;*6D?iKY!n?xk~Xh@V@&W(DzO;=&Xi14)7b1UGhZHa$JTbEe8u04Bh*qz$NA z&UU2*Zo?3{#tHUk-xGq8NVoR05plD%zTPXP0=o?sSC%Z((BU@vcQ~K4K4h%dG1@IZ zTNYa#S88<1Qy?%1OoF1A>P)P^f}-{vaz{J%G4b6)_GL(QbC5I8xXKKuS;Hq-bP1qB>?<#DMa75#Y|s)t=IQ()>_^x z=Oc`~@2vNQS;I?|DM?U;;QE2@bZ$y)^pCE41gjxyJ6Qvli2)yw7|NN2B7IW4VS8gw zkRN?vb_(@sB(`{Sfd!JhQPA$WLAzAMXln4zm8d587mvs(Qg*svswJJDg-=BxdA~Zz Y)1{lx3&{kPD31rBu<-zRN<*UNF1j;qL?}fJy5ZAcPGg@>s|NX zb<-fE^aka?Wh8`@g3|WN0lE@M9FT}qi30+}ap3}@5?o3pDj^{t%s);F0_;cmUZ)!gdeh}}KNrjEdTkHqfvvMkkhvP+V7r0DdZyc0b*os)NRA%4~*ww+0_SKF?|4scI6h>&FY$SCPy zXClea02jQ(jL4HD@7;`~14DJSBAy^5TpCyvRbT^rWSprtX5{uR$!J*q{ohn%pw>=ZB*S=m_z4z4*$t-FEtfy;T*U4 z*xO3qAY*^mv>$!*2!eTBU8PW`j(Wkcs;~hn*L=s;y8B6ZRfnf|Kq`u;~I-2m#OMZN@K^=P^W3SkY$_M zuB_nKsoEBGR&7@`=}J}GfDLZ=tM`63MF@Dedv3_e5Z<%S>V0368hbkT>}k9PMi6ia z{m$nW%JVZ+vvpbmumK)HX;3#b*OynQ3*%^-M3_Lp))&G6?5o^SGRgkPWioR6t2Y;*DhY;Sz++ky zfLgAK+j|v5g)O>{fB)pzC3+F9I&1qBuXEE|w~j{1p%*du2m 120 or not re.fullmatch(r'[a-z][a-z0-9_]*\.[a-z0-9_]+', eid): + raise ValueError('Identifiant Home Assistant invalide.') + if eid.split('.')[0] not in DOMAINS: + raise ValueError('Domaine Home Assistant non autorisé pour le cadre Maison.') + item['entity_id'] = eid + out.append(item) + return out + + +def read_rows(): + try: + loaded = json.loads(SETTINGS.read_text(encoding='utf-8')) + return validate_rows(loaded['rows']) + except (OSError, ValueError, KeyError, TypeError): + return [dict(row) for row in DEFAULT_ROWS] + + +def save_rows(rows): + valid = validate_rows(rows) + SETTINGS.parent.mkdir(parents=True, exist_ok=True) + with LOCK: + tmp = SETTINGS.with_name(SETTINGS.name + '.' + secrets.token_hex(6) + '.tmp') + try: + with tmp.open('w', encoding='utf-8') as out: + json.dump({'version': 1, 'rows': valid}, out, ensure_ascii=False, indent=2) + out.write('\n'); out.flush(); os.fsync(out.fileno()) + os.chmod(tmp, 0o600) + tmp.replace(SETTINGS) + finally: + tmp.unlink(missing_ok=True) + return valid + + +def states_from_ha(): + now = time.monotonic() + with _CACHE_LOCK: + if _CACHE['states'] is not None and now < _CACHE['expires']: + return _CACHE['states'] + base = os.getenv('HA_URL', '').strip().rstrip('/') + token = os.getenv('HA_TOKEN', '').strip() + if not base or not token or not (base.startswith('http://') or base.startswith('https://')): + raise ValueError('Connexion Home Assistant non configurée pour le tableau de bord.') + from services.alfred_dashboard import _json_get + data = _json_get(base + '/api/states', token=token, timeout=3) + if not isinstance(data, list): + raise ValueError('Réponse Home Assistant invalide.') + with _CACHE_LOCK: + _CACHE.update(states=data, expires=time.monotonic() + 20) + return data + + +def _pretty_state(state, attrs): + if state in ('unknown','unavailable',''): + return 'Indisponible' + value = {'on':'Allumé', 'off':'Éteint', 'open':'Ouvert', 'closed':'Fermé', + 'home':'Présent', 'not_home':'Absent', 'locked':'Verrouillé', + 'unlocked':'Déverrouillé', 'heat':'Chauffage', 'cool':'Climatisation'}.get(state, state) + unit = str(attrs.get('unit_of_measurement') or '') + if unit and len(unit) <= 12: + return str(value) + ' ' + unit + return str(value)[:42] + + +def resolve_rows(rows, states, dashboard_home=None): + lookup = {str(s.get('entity_id')): s for s in states if isinstance(s, dict)} + output = [] + for item in rows: + kind, label = item['kind'], item['label'] + value = 'Indisponible' + if kind == 'lights_on': + value = str(sum(e.get('state') == 'on' for eid,e in lookup.items() if eid.startswith('light.'))) + elif kind == 'covers_closed': + value = str(sum(e.get('state') == 'closed' for eid,e in lookup.items() if eid.startswith('cover.'))) + elif kind == 'indoor_temperature': + candidates=[] + for eid,e in lookup.items(): + attr=e.get('attributes') or {} + try: + if eid.startswith('climate.'): + t=float(attr.get('current_temperature')) + elif eid.startswith('sensor.') and attr.get('device_class') == 'temperature': + t=float(e.get('state')) + else: continue + if abs(t) < 150: candidates.append((0 if 'salon' in eid else 1,t)) + except (TypeError,ValueError): pass + if candidates: + candidates.sort(key=lambda x:x[0]); value=f'{candidates[0][1]:g} °C' + elif kind == 'entity': + e=lookup.get(item['entity_id']) + if e: + value=_pretty_state(str(e.get('state') or ''), e.get('attributes') or {}) + output.append({'label':label,'value':value,'available':value!='Indisponible'}) + return output + + +def register_home_glass(app): + bp = Blueprint('alfred_home_glass_v1', __name__) + + @bp.get('/api/control/home-card') + @admin_console._require_admin + def get_config(): + return jsonify(rows=read_rows()) + + @bp.post('/api/control/home-card') + @admin_console._require_admin + def set_config(): + data=request.get_json(silent=True) + if not isinstance(data,dict) or set(data)!={'rows'}: + return jsonify(error='JSON invalide.'), 400 + try: + result=save_rows(data['rows']) + except (ValueError, OSError) as exc: + return jsonify(error=str(exc)), 400 + return jsonify(ok=True,rows=result,applied_immediately=True) + + @bp.get('/api/control/home-entities') + @admin_console._require_admin + def list_entities(): + try: + states=states_from_ha() + except (OSError, ValueError, HTTPError, URLError) as exc: + return jsonify(error='Home Assistant inaccessible ou non configuré.', detail=str(exc)[:150]), 503 + options=[] + for entity in states: + eid=entity.get('entity_id','') + if not isinstance(eid,str) or eid.split('.')[0] not in DOMAINS: + continue + attrs=entity.get('attributes') or {} + friendly=str(attrs.get('friendly_name') or eid)[:80] + options.append({'id':eid,'name':friendly}) + options.sort(key=lambda x:(x['name'].casefold(),x['id'])) + return jsonify(entities=options[:1500]) + + @bp.get('/api/alfred/home-card') + def live_card(): + rows=read_rows() + try: + states=states_from_ha() + values=resolve_rows(rows,states) + except (OSError, ValueError, HTTPError, URLError): + values=[{'label':r['label'],'value':'Indisponible','available':False} for r in rows] + return jsonify(rows=values,available=any(x['available'] for x in values)) + + app.register_blueprint(bp) diff --git a/alfred/app/services/alfred_identity_page.py b/alfred/app/services/alfred_identity_page.py new file mode 100644 index 0000000..8bdb8b7 --- /dev/null +++ b/alfred/app/services/alfred_identity_page.py @@ -0,0 +1,204 @@ +"""Identity editor and current assistant name, authenticated by Alfred's admin UI. + +Identity text stays in config/identity.md. All writes are atomic; dated revisions +are kept separately. Technical names/paths/API identifiers are never renamed. +""" +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +import re +import tempfile +import threading +from datetime import datetime + +from flask import Blueprint, jsonify, render_template, request +from services import admin_console + +ROOT = Path(__file__).resolve().parents[1] +CONFIG = ROOT / 'config' +IDENTITY = CONFIG / 'identity.md' +TRACKER = CONFIG / 'identity_name_state.json' +HISTORY = CONFIG / 'identity_history' +LOCK = threading.RLock() +MAX_SIZE = 40_000 +ASSISTANT_RE = re.compile(r"(? MAX_SIZE or '\x00' in text: + return jsonify(error='L’identité doit être non vide et ne pas dépasser 40 Ko.'), 400 + if not re.fullmatch(r'[0-9a-f]{64}', revision): + return jsonify(error='Révision invalide.'), 400 + try: + with LOCK: + path = identity_path() + old = _read_text() + if sha(old) != revision: + return jsonify(error='Le fichier a changé depuis ton chargement. Recharge avant d’enregistrer.'), 409 + if text != old: + _revision(old, 'avant_edition') + mode = (path.stat().st_mode & 0o777) if path.exists() else 0o600 + _save_atomic(path, text, mode) + res = jsonify(ok=True, revision=sha(text), name=active_name(), + updated=text != old, file='config/identity.md') + res.headers['Cache-Control'] = 'no-store, private' + return res + except (OSError, ValueError) as e: + return jsonify(error='Enregistrement impossible : '+str(e)), 500 + + app.register_blueprint(bp) + # If a renamed assistant was configured before installation, reconcile on startup. + try: + sync_identity_name() + except (OSError, ValueError) as e: + app.logger.warning('Synchronisation identité différée : %s', e) diff --git a/alfred/app/services/alfred_lists.py b/alfred/app/services/alfred_lists.py new file mode 100644 index 0000000..b1061ad --- /dev/null +++ b/alfred/app/services/alfred_lists.py @@ -0,0 +1,249 @@ +"""Alfred Listes V1 — courses et tâches locales, SQLite, commandes vocales sûres. + +Aucune dépendance externe. Le serveur MCP n'attend jamais et ne lance jamais de shell. +""" +from __future__ import annotations + +from datetime import datetime, timezone +from contextlib import contextmanager +import os +from pathlib import Path +import re +import sqlite3 +import threading +import unicodedata +from urllib.parse import urlparse + +from flask import Blueprint, jsonify, request + +DB_PATH = Path(os.getenv('ALFRED_LISTS_DB', str(Path(__file__).resolve().parents[1] / 'data' / 'alfred_lists.sqlite3'))) +LOCK = threading.RLock() +KINDS = {'courses': 'courses', 'course': 'courses', 'taches': 'taches', 'tache': 'taches', + 'tasks': 'taches', 'task': 'taches', 'todo': 'taches', 'shopping': 'courses'} +MAX_ITEMS = 500 +MAX_LEN = 160 + + +def fold(s): + return ''.join(c for c in unicodedata.normalize('NFKD', s.casefold()) if not unicodedata.combining(c)) + + +def normalize_kind(kind): + if not isinstance(kind, str) or fold(kind.strip()) not in KINDS: + raise ValueError('Liste inconnue : choisir courses ou taches.') + return KINDS[fold(kind.strip())] + + +def clean_label(label, kind=None): + if not isinstance(label, str): + raise ValueError('Le nom de l’élément doit être un texte.') + label = ' '.join(label.split()).strip(' .!?;,') + if kind == 'courses': + label = re.sub(r"^(?:du\s+|de\s+la\s+|des\s+|de\s+l['’])", '', label, flags=re.I).strip() + if not label or len(label) > MAX_LEN or any(ord(x) < 32 for x in label): + raise ValueError('Nom attendu : de 1 à 160 caractères.') + return label + + +def _conn(): + DB_PATH.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(str(DB_PATH), timeout=6) + conn.row_factory = sqlite3.Row + conn.execute('PRAGMA busy_timeout=6000') + conn.execute('PRAGMA journal_mode=WAL') + conn.execute('''CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + kind TEXT NOT NULL CHECK(kind IN ('courses','taches')), + label TEXT NOT NULL, + normalized TEXT NOT NULL, + done INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE(kind, normalized) + )''') + return conn + + +@contextmanager +def _db(): + with LOCK: + conn = _conn() + try: + with conn: + yield conn + finally: + conn.close() + + +def _row(row): + return {'id': row['id'], 'kind': row['kind'], 'label': row['label'], + 'done': bool(row['done']), 'created_at': row['created_at']} + + +def list_items(kind=None): + if kind is not None: kind = normalize_kind(kind) + with _db() as conn: + q = ('SELECT id, kind, label, done, created_at FROM items ' + + ('WHERE kind=? ' if kind else '') + + 'ORDER BY done ASC, id DESC LIMIT 1000') + rows = conn.execute(q, (kind,) if kind else ()).fetchall() + items = [_row(r) for r in rows] + result = {'courses': [], 'taches': []} + for item in items: result[item['kind']].append(item) + return {'ok': True, 'lists': result, + 'counts': {k: {'active': sum(not r['done'] for r in v), 'total': len(v)} + for k, v in result.items()}} + + +def add_item(kind, label): + kind = normalize_kind(kind) + label = clean_label(label, kind) + norm = fold(label) + now = datetime.now(timezone.utc).isoformat() + with _db() as conn: + row = conn.execute('SELECT * FROM items WHERE kind=? AND normalized=?', (kind, norm)).fetchone() + if row: + if row['done']: + conn.execute('UPDATE items SET done=0, label=?, updated_at=? WHERE id=?', (label, now, row['id'])) + existing = False + else: existing = True + item_id = row['id'] + else: + n = conn.execute('SELECT COUNT(*) FROM items WHERE kind=?', (kind,)).fetchone()[0] + if n >= MAX_ITEMS: raise ValueError('Liste pleine (500 éléments maximum).') + item_id = conn.execute('INSERT INTO items (kind,label,normalized,done,created_at,updated_at) VALUES (?,?,?,?,?,?)', + (kind, label, norm, 0, now, now)).lastrowid + existing = False + row = conn.execute('SELECT * FROM items WHERE id=?', (item_id,)).fetchone() + return {'ok': True, 'item': _row(row), 'already_present': existing} + + +def _resolve(conn, kind, item_id=None, label=None): + kind = normalize_kind(kind) + if item_id is not None: + try: n = int(item_id) + except (ValueError, TypeError): raise ValueError('Identifiant invalide.') + row = conn.execute('SELECT * FROM items WHERE id=? AND kind=?', (n, kind)).fetchone() + elif label is not None: + label = clean_label(label,kind) + row = conn.execute('SELECT * FROM items WHERE kind=? AND normalized=?', (kind, fold(label))).fetchone() + else: + raise ValueError('Indique l’identifiant ou le nom exact de l’élément.') + if row is None: raise ValueError('Élément introuvable dans la liste.') + return row + + +def change_item(action, kind, item_id=None, label=None): + if action not in ('complete_item', 'reopen_item', 'delete_item'): + raise ValueError('Action inconnue.') + with _db() as conn: + row = _resolve(conn, kind, item_id, label) + if action == 'delete_item': + conn.execute('DELETE FROM items WHERE id=?', (row['id'],)) + else: + conn.execute('UPDATE items SET done=?, updated_at=? WHERE id=?', + (int(action == 'complete_item'), datetime.now(timezone.utc).isoformat(), row['id'])) + return {'ok': True, 'item': _row(row), 'action': action} + + +def clear_completed(kind): + kind = normalize_kind(kind) + with _db() as conn: + count = conn.execute('DELETE FROM items WHERE kind=? AND done=1', (kind,)).rowcount + return {'ok': True, 'deleted': count} + + +def execute(action, data): + if action == 'list_items': return list_items(data.get('kind')) + if action == 'add_item': return add_item(data.get('kind'), data.get('label')) + if action in ('complete_item', 'reopen_item', 'delete_item'): + return change_item(action, data.get('kind'), data.get('id'), data.get('label')) + if action == 'clear_completed': return clear_completed(data.get('kind')) + raise ValueError('Action inconnue.') + + +# Routes vocales directes pour les cas courants : les LLM ne peuvent pas inventer le succès. +_ADD_RE = re.compile( + r"^\s*(?:ajoute|rajoute|mets|met|note|inscris|ajouter)\s+(?P

⌂ +

Cadre « Maison » de l’accueil

Personnalise jusqu’à trois informations affichées en haut de l’écran principal

+

Choisis un compteur global ou une entité précise de Home Assistant, et personnalise le libellé. Les valeurs sont actualisées automatiquement sans redémarrer Alfred.

+
+
+ + + +

`; + main.querySelector('.restart-panel')?.before(editor); + + const types = [ + ['lights_on','Nombre de lumières allumées'], + ['covers_closed','Nombre de volets fermés'], + ['indoor_temperature','Température intérieure'], + ['entity','Entité Home Assistant précise'] + ]; + const defaults = [ + {kind:'lights_on',label:'Lumières allumées'}, + {kind:'covers_closed',label:'Volets fermés'}, + {kind:'indoor_temperature',label:'Température intérieure'} + ]; + const list = editor.querySelector('#agMaisonRows'); + const message = editor.querySelector('#agMaisonMessage'); + const add = editor.querySelector('#agAddMaison'); + let entities = []; + let rows = defaults.map(x=>({...x})); + let busy=false; + const msg=(text,error=false)=>{message.textContent=text;message.classList.toggle('ag-error',error);}; + const el=(name,cls,text)=>{const node=document.createElement(name);if(cls)node.className=cls;if(text!==undefined)node.textContent=text;return node;}; + + function render() { + list.replaceChildren(); + rows.forEach((row,i)=>{ + const box=el('div','ag-row'); + const header=el('div','ag-row-header'); + header.append(el('strong','',`Ligne ${i+1}`)); + const nav=el('div','ag-row-buttons'); + const action=(name,symbol,title,fn)=>{ + const b=el('button','ag-small-button',symbol);b.type='button';b.title=title;b.setAttribute('aria-label',title); + b.disabled=busy;b.addEventListener('click',fn);nav.append(b); + }; + action('up','↑','Monter',()=>{if(i){[rows[i-1],rows[i]]=[rows[i],rows[i-1]];render();}}); + action('down','↓','Descendre',()=>{if(i{if(rows.length>1){rows.splice(i,1);render();}}); + header.append(nav);box.append(header); + const settings=el('div','ag-row-fields'); + const kindWrap=el('label','ag-field');kindWrap.append(el('span','', 'Information affichée')); + const kind=el('select'); + types.forEach(([value,txt])=>{const o=el('option','',txt);o.value=value;o.selected=row.kind===value;kind.append(o);}); + kind.addEventListener('change',()=>{row.kind=kind.value;if(row.kind==='entity'&&!row.entity_id)row.entity_id=entities[0]?.id||'';render();}); + kindWrap.append(kind);settings.append(kindWrap); + const labelWrap=el('label','ag-field');labelWrap.append(el('span','', 'Intitulé personnalisé')); + const text=el('input');text.type='text';text.maxLength=38;text.required=true;text.value=row.label||''; + text.addEventListener('input',()=>{row.label=text.value;});labelWrap.append(text);settings.append(labelWrap); + if(row.kind==='entity'){ + const entityWrap=el('label','ag-field ag-wide');entityWrap.append(el('span','', 'Entité Home Assistant')); + const field=el('input');field.type='text';field.maxLength=120; + field.value=row.entity_id||'';field.placeholder='sensor.salon_temperature'; + field.setAttribute('list',`agEntityList${i}`); + field.addEventListener('input',()=>{row.entity_id=field.value.trim();}); + const choices=el('datalist');choices.id=`agEntityList${i}`; + entities.forEach(entry=>{const o=el('option');o.value=entry.id;o.label=entry.name;choices.append(o);}); + entityWrap.append(field,choices); + const helper=el('small','',entities.length + ? 'Choisis une entité proposée ou saisis son identifiant.' + : 'Saisis un identifiant comme sensor.salon_temperature si Home Assistant est inaccessible.'); + entityWrap.append(helper);settings.append(entityWrap); + } + box.append(settings);list.append(box); + }); + add.disabled=rows.length>=3 || busy; + } + function tryAuthFetch(url,options={}){ + return fetch('/api/control/auth',{cache:'no-store',credentials:'same-origin'}).then(async r=>{ + const a=await r.json();if(!a.authenticated)throw new Error('Connexion administrateur nécessaire.'); + const headers=options.method==='POST'?{'Content-Type':'application/json','X-Alfred-CSRF':a.csrf}:{}; + const response=await fetch(url,{...options,headers,credentials:'same-origin',cache:'no-store'}); + const body=await response.json().catch(()=>({})); + if(!response.ok)throw new Error(body.error||`Erreur HTTP ${response.status}`); + return body; + }); + } + async function refresh(){ + if(busy)return; + busy=true; + try{ + const config=await tryAuthFetch('/api/control/home-card'); + rows=Array.isArray(config.rows)?config.rows:defaults.map(x=>({...x})); + const response=await tryAuthFetch('/api/control/home-entities').catch(err=>{msg('Liste des entités indisponible : '+err.message,true);return null;}); + entities=response?.entities||[]; + if(response)msg(`${entities.length} entités Home Assistant disponibles.`); + }catch(e){msg(e.message,true);} + finally{busy=false;render();} + } + editor.querySelector('#agDefaultMaison').addEventListener('click',()=>{rows=defaults.map(x=>({...x}));render();msg('Valeurs par défaut prêtes à enregistrer.');}); + add.addEventListener('click',()=>{if(rows.length>=3)return;rows.push({kind:'entity',label:'Information',entity_id:entities[0]?.id||''});render();}); + editor.querySelector('#agSaveMaison').addEventListener('click',async()=>{ + if(busy)return; + busy=true; + try{ + const result=await tryAuthFetch('/api/control/home-card',{method:'POST',body:JSON.stringify({rows})}); + rows=result.rows; + msg('Cadre Maison enregistré. Les changements apparaîtront sur l’accueil en quelques secondes.'); + }catch(e){msg('Impossible d’enregistrer : '+e.message,true);} + finally{busy=false;render();} + }); + let loaded=false; + async function checkLogin(){ + if(!main.hidden&&!loaded){loaded=true;await refresh();} + if(main.hidden)loaded=false; + } + new MutationObserver(checkLogin).observe(main,{attributes:true,attributeFilter:['hidden']}); + checkLogin(); +})(); diff --git a/alfred/app/static/js/alfred-admin.js b/alfred/app/static/js/alfred-admin.js index bf600d0..5f260f8 100644 --- a/alfred/app/static/js/alfred-admin.js +++ b/alfred/app/static/js/alfred-admin.js @@ -54,6 +54,7 @@ configDirty(); $('pendingNotice').hidden = false; setMessage(result.ok ? 'Configuration sauvegardée.' : 'Enregistrement inconnu.'); + if (result.identity_sync_warning) setMessage('Nom enregistré, mais identity.md non synchronisé : '+result.identity_sync_warning, true); } catch (error) { setMessage(error.message, true); } } function serviceStatus(el, data) { @@ -165,12 +166,12 @@ fields.forEach(el=>{el.addEventListener('change',markInput);el.addEventListener('input',markInput);}); $('saveConfig')?.addEventListener('click',saveConfig); $('restartButton')?.addEventListener('click',async ()=>{ - if(!confirm('Redémarrer Alfred maintenant ? Les opérations en cours seront interrompues.'))return; + if(!confirm('Redémarrer '+(document.querySelector('.brand strong')?.textContent || 'l’assistant')+' maintenant ? Les opérations en cours seront interrompues.'))return; // ALFRED_IDENTITY_GLASS_V1 try{const data=await fetchJSON('/api/control/restart',{method:'POST'});setMessage(data.message||'Demande transmise.');$('restartButton').disabled=true;setTimeout(()=>$('restartButton').disabled=false,5000);} catch(err){setMessage(err.message,true);} }); $('testVoice')?.addEventListener('click',async()=>{ - try{await fetchJSON('/api/tts/say',{method:'POST',body:JSON.stringify({text:'Bonjour, je suis Alfred. Voici un test de ma voix actuelle.'})});setMessage('Test vocal demandé.');} + try{await fetchJSON('/api/tts/say',{method:'POST',body:JSON.stringify({text:'Bonjour, je suis '+(document.querySelector('.brand strong')?.textContent || 'votre assistant')+'. Voici un test de ma voix actuelle.'})});setMessage('Test vocal demandé.');} catch(err){setMessage(err.message,true);} }); let volDelay, gainDelay; diff --git a/alfred/app/static/js/alfred-identity-glass.js b/alfred/app/static/js/alfred-identity-glass.js new file mode 100644 index 0000000..42b2a04 --- /dev/null +++ b/alfred/app/static/js/alfred-identity-glass.js @@ -0,0 +1,60 @@ +/* Identity editing: admin-protected API, optimistic revision checks, no HTML injection. */ +(() => { + 'use strict'; + const $=id=>document.getElementById(id); + let csrf='',revision='',dirty=false,busy=false; + const msg=(s,error=false,id='idMessage')=>{const e=$(id);e.textContent=s;e.classList.toggle('error',error);}; + const setName=name=>{ + const n=String(name||'Assistant').trim(); + const initial=Array.from(n)[0]?.toLocaleUpperCase('fr-FR')||'A'; + $('idName').textContent=n;$('idInitial').textContent=initial; + $('idActiveName').textContent=n;$('idAvatar').textContent=initial; + document.title='Identité · '+n; + }; + function updateCount(){$('idCount').textContent=$('idText').value.length.toLocaleString('fr-FR')+' caractères';} + async function jsonFetch(url,options={}){ + const r=await fetch(url,{credentials:'same-origin',cache:'no-store',...options}); + const d=await r.json().catch(()=>({})); + if(!r.ok)throw Error(d.error||'Erreur HTTP '+r.status); + return d; + } + async function auth(){ + const a=await jsonFetch('/api/control/auth'); + csrf=a.csrf||''; + $('idLoginPanel').hidden=!!a.authenticated; + $('idEditor').hidden=!a.authenticated; + $('idBadge').textContent=a.authenticated?'Administrateur connecté':'Connexion requise'; + $('idBadge').classList.toggle('ok',!!a.authenticated); + if(a.authenticated)await load(); + } + async function load(){ + try{ + const d=await jsonFetch('/api/control/identity'); + $('idText').value=d.text;revision=d.revision;dirty=false; + setName(d.name);updateCount();msg('Document chargé.'); + }catch(e){msg(e.message,true);} + } + $('idLoginForm').addEventListener('submit',async e=>{ + e.preventDefault(); + try{ + await jsonFetch('/api/control/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:$('idPassword').value})}); + $('idPassword').value='';await auth(); + }catch(err){msg(err.message,true,'idLoginMessage');} + }); + $('idText').addEventListener('input',()=>{dirty=true;updateCount();msg('Modifications non enregistrées.');}); + $('idSave').addEventListener('click',async()=>{ + if(busy||!revision)return; + busy=true;$('idSave').disabled=true; + try{ + const d=await jsonFetch('/api/control/identity',{ + method:'POST',headers:{'Content-Type':'application/json','X-Alfred-CSRF':csrf}, + body:JSON.stringify({text:$('idText').value,revision}) + }); + revision=d.revision;dirty=false;msg(d.updated?'Identité enregistrée. Nouvelle version sauvegardée.':'Aucun changement à enregistrer.'); + }catch(e){msg(e.message,true);} + finally{busy=false;$('idSave').disabled=false;} + }); + $('idReload').addEventListener('click',()=>{if(dirty&&!confirm('Abandonner les modifications non enregistrées ?'))return;load();}); + window.addEventListener('beforeunload',event=>{if(dirty){event.preventDefault();event.returnValue='';}}); + auth().catch(e=>{msg(e.message,true,'idLoginMessage');$('idLoginPanel').hidden=false;}); +})(); diff --git a/alfred/app/static/js/alfred-listes-glass.js b/alfred/app/static/js/alfred-listes-glass.js new file mode 100644 index 0000000..1064dc3 --- /dev/null +++ b/alfred/app/static/js/alfred-listes-glass.js @@ -0,0 +1,125 @@ +/* ALFRED_LISTES_GLASS_V1 — course/tâches, fenêtre Glass, pas de dépendance JS. */ +(() => { + 'use strict'; + if (document.getElementById('alfredListsGlassDialog')) return; + const $ = id => document.getElementById(id); + const icon = (d) => ``; + const shopping = icon(''); + const tasks = icon(''); + const shell = document.getElementById('alfredMainV1'); + if (!shell) return; // Le tableau de bord doit être installé avant ce module. + const modal = document.createElement('dialog'); + modal.id = 'alfredListsGlassDialog'; + modal.className = 'al-list-dialog'; + modal.innerHTML = `
+

ALFRED · ORGANISATION

Courses & tâches

Tes listes sont conservées sur Alfred et mises à jour en temps réel.

+ +
+
${shopping}

Liste de courses

— article(s) à acheter

+
+
+
${tasks}

Liste de tâches

— tâche(s) à faire

+
+
+
`; + document.body.append(modal); + + // Navigation: garder toutes les entrées existantes, y compris « Plus ». + const rail = shell.querySelector('.av-rail'); + if (rail && !rail.querySelector('[data-al-open]')) { + const btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'av-rail-link al-rail-link'; + btn.setAttribute('data-al-open', ''); + btn.innerHTML = `${tasks}Listes`; + rail.querySelector('[data-av-action="more"]')?.before(btn); + if (!btn.isConnected) rail.append(btn); + } + const shortcuts = shell.querySelector('.av-shortcuts'); + if (shortcuts && !shortcuts.querySelector('[data-al-open]')) { + const btn = document.createElement('button'); btn.type = 'button'; + btn.setAttribute('data-al-open', ''); + btn.innerHTML = `${shopping}Courses & tâches`; + shortcuts.querySelector('[data-av-action="more"]')?.before(btn); + if (!btn.isConnected) shortcuts.append(btn); + } + const more = $('avMoreDialog')?.querySelector('.av-more-grid'); + if (more && !more.querySelector('[data-al-open]')) { + const btn = document.createElement('button');btn.type='button';btn.setAttribute('data-al-open',''); + btn.innerHTML = `${shopping} Courses & tâches`; more.append(btn); + } + + function status(msg, error = false) { + const el = $('alListStatus'); if (!el) return; + el.textContent = msg;el.classList.toggle('al-error',error); + } + async function fetchLists() { + const res = await fetch('/api/alfred/lists', {cache:'no-store'}); + const data = await res.json(); if (!res.ok || !data.ok) throw Error(data.error || `HTTP ${res.status}`); + return data; + } + async function send(action, payload) { + const res = await fetch('/api/alfred/lists/action', { + method: 'POST', headers: {'Content-Type':'application/json','X-Alfred-Request':'1'}, + body: JSON.stringify({action,...payload}) + }); + const data = await res.json();if (!res.ok || !data.ok)throw Error(data.error || `HTTP ${res.status}`); + return data; + } + function element(tag, text, cls) { + const e=document.createElement(tag); if (cls)e.className=cls; if (text!==undefined)e.textContent=text;return e; + } + function render(items, kind) { + const target=$(kind==='courses'?'alItemsCourses':'alItemsTaches');if(!target)return; + target.replaceChildren(); + if(!items.length) { + const li=element('li', 'Ta liste est vide. Ajoute un premier élément !', 'al-empty');target.append(li);return; + } + for(const item of items) { + const li=element('li',undefined,'al-item'+(item.done?' is-done':'')); + const check=element('button',item.done?'✓':'','al-check');check.type='button'; + check.setAttribute('aria-label',item.done?'Réouvrir '+item.label:'Marquer comme terminé : '+item.label); + check.title=item.done?'Réouvrir':'Cocher'; + check.addEventListener('click',()=>modify(item.done?'reopen_item':'complete_item',kind,item.id)); + const label=element('span',item.label,'al-item-label'); + const remove=element('button','×','al-remove');remove.type='button';remove.title='Supprimer'; + remove.setAttribute('aria-label','Supprimer '+item.label); + remove.addEventListener('click',()=>modify('delete_item',kind,item.id)); + li.append(check,label,remove);target.append(li); + } + } + async function refresh() { + try { + const data = await fetchLists(); + for(const kind of ['courses','taches']) { + render(data.lists[kind] || [],kind); + const dest=$(kind==='courses'?'alCountCourses':'alCountTaches'); + if(dest)dest.textContent=data.counts[kind]?.active ?? 0; + } + const shortcut=$('alShortcutText'); + if(shortcut)shortcut.textContent=`Courses ${data.counts.courses.active} · Tâches ${data.counts.taches.active}`; + status('Listes à jour'); + } catch(e) {status(`Erreur : ${e.message}`,true);} + } + async function modify(action,kind,id) { + try {await send(action,{kind,id});await refresh();}catch(e){status(e.message,true);} + } + document.addEventListener('click',(ev)=>{ + if(!ev.target.closest('[data-al-open]'))return; + if($('avMoreDialog')?.open)$('avMoreDialog').close(); + modal.showModal();refresh(); + }); + modal.querySelector('[data-al-close]').addEventListener('click',()=>modal.close()); + modal.querySelector('[data-al-refresh]').addEventListener('click',refresh); + modal.querySelectorAll('.al-add-form').forEach(form=>form.addEventListener('submit',async ev=>{ + ev.preventDefault();const input=form.querySelector('input');const label=input.value.trim();if(!label)return; + try { + const result=await send('add_item',{kind:form.dataset.kind,label}); + input.value='';await refresh();status(result.already_present?'Cet élément est déjà présent.':'Élément ajouté.');input.focus(); + }catch(e){status(e.message,true);} + })); + window.alfredSocket?.on('alfred_lists_changed',()=>refresh()); + window.addEventListener('focus',refresh); + setInterval(refresh,30000); + refresh(); + window.AlfredLists={open:()=>{modal.showModal();refresh();},refresh}; +})(); diff --git a/alfred/app/static/js/alfred-logs-v2.js b/alfred/app/static/js/alfred-logs-v2.js new file mode 100644 index 0000000..618f517 --- /dev/null +++ b/alfred/app/static/js/alfred-logs-v2.js @@ -0,0 +1,45 @@ +/* ALFRED_GLASS_LOGS_V2 — aucun HTML de journal injecte, seulement textContent. */ +(() => { + 'use strict'; + const $=id=>document.getElementById(id); + let all=[];let authenticated=false;let pending=false; + const when=new Intl.DateTimeFormat('fr-FR',{dateStyle:'short',timeStyle:'medium',timeZone:'Europe/Paris'}); + const el=(tag,cls,text)=>{const e=document.createElement(tag);if(cls)e.className=cls;if(text!=null)e.textContent=String(text);return e;}; + async function identity(){try{const r=await fetch('/api/state',{cache:'no-store'});if(!r.ok)return;const d=await r.json();const n=String(d.assistant_name||d.title||'').trim();if(!n)return;$('lgName').textContent=n;$('lgLetter').textContent=Array.from(n)[0].toLocaleUpperCase('fr-FR');document.title='Journaux · '+n;}catch(_){}} + identity(); + function render(){ + const needle=$('lgSearch').value.trim().toLocaleLowerCase('fr-FR'); + const component=$('lgComponent').value,level=$('lgLevel').value; + const filtered=all.filter(x=>(!component||x.component===component)&&(!level||x.level===level)&&(!needle||(x.message||'').toLocaleLowerCase('fr-FR').includes(needle))); + const root=$('lgEvents');root.replaceChildren(); + $('lgCount').textContent=`${filtered.length} / ${all.length} événement(s)`; + if(!filtered.length){root.append(el('div','lg-empty',authenticated?'Aucun événement correspondant aux filtres.':'Connecte-toi via la page Configuration pour consulter les journaux.'));return;} + for(const x of filtered.slice().reverse()){ + const line=el('div','lg-event');line.dataset.level=x.level; + let date='—';try{date=when.format(new Date(x.at));}catch(_){} + const time=el('time','',date);time.dateTime=x.at; + line.append(time,el('span','lg-component',x.component),el('span','lg-level',x.level),el('span','lg-message',x.message)); + root.append(line); + } + } + async function refresh(){ + if(pending)return;pending=true;$('lgStatus').textContent='Actualisation…'; + try{const r=await fetch('/api/alfred/logs?limit='+encodeURIComponent($('lgLimit').value),{cache:'no-store'});const d=await r.json(); + if(r.status===401){authenticated=false;all=[];$('lgStatus').textContent='Accès administrateur requis · Connexion depuis /config';render();return;} + if(!r.ok||!d.ok)throw Error(d.error||`HTTP ${r.status}`); + authenticated=true;all=Array.isArray(d.items)?d.items:[]; + $('lgStatus').textContent='Dernière lecture : '+new Date().toLocaleTimeString('fr-FR',{timeZone:'Europe/Paris'}); + render(); + }catch(e){$('lgStatus').textContent='Journal indisponible : '+e.message;}finally{pending=false;} + } + for(const id of ['lgSearch','lgComponent','lgLevel'])$(id).addEventListener('input',render); + $('lgLimit').addEventListener('change',refresh); + $('lgRefresh').addEventListener('click',refresh); + $('lgExport').addEventListener('click',()=>{ + if(!authenticated||!all.length)return; + const file=new Blob([JSON.stringify({source:'alfred.service',exported_at:new Date().toISOString(),events:all},null,2)],{type:'application/json'}); + const url=URL.createObjectURL(file);const a=document.createElement('a');a.href=url;a.download='alfred-journaux-'+new Date().toISOString().slice(0,10)+'.json';a.click();URL.revokeObjectURL(url); + }); + setInterval(()=>{if($('lgAuto').checked&&!document.hidden)refresh();},10000); + window.addEventListener('focus',refresh);refresh(); +})(); diff --git a/alfred/app/static/js/alfred-main-v1.js b/alfred/app/static/js/alfred-main-v1.js new file mode 100644 index 0000000..d80f432 --- /dev/null +++ b/alfred/app/static/js/alfred-main-v1.js @@ -0,0 +1,319 @@ +/* ALFRED_MAIN_VISUAL_V1 — accueil vivant, conservation des contrôleurs existants. */ +(() => { + 'use strict'; + if (document.getElementById('alfredMainV1')) return; + const shell = document.querySelector('.alfred-shell'); + if (!shell || !document.getElementById('stage') || !document.getElementById('content')) return; + + const I = { + home:'', + bulb:'', + server:'', + brain:'', + media:'', + more:'', + mic:'', + micOff:'', + speaker:'', + speakerOff:'', + camera:'', + settings:'', + calendar:'', + clock:'', + cloud:'', + shield:'', + arrow:'', + chat:'', + close:'', + sun:'', + spark:'', + }; + const icon = (name, extra = '') => ``; + const initialName = (document.title || 'Alfred').trim(); + const dashboard = document.createElement('div'); + dashboard.id = 'alfredMainV1'; + dashboard.className = 'alfred-main-v1'; + dashboard.innerHTML = ` +
+
Assistant vocal
+
--:--—
+
+ + + + + ${icon('more')} +
+
+
+ +
+ +
${icon('server')}SystèmeMini-PC Alfred${icon('arrow','av-chevron')}

Charge : —

Mémoire : —

Stockage : —

+
${icon('brain')}Services IAStatuts en cours…${icon('arrow','av-chevron')}

Ollama : —

Hermes : —

ComfyUI : —

+ +
+
+
+ +
${icon('mic')}
En veille

Dites « Alfred » puis votre demande

Prêt à vous écouter
+ +
+
+
+
+ + + + + + + +
+
${icon('chat')} Interactions récentes
Aucune interaction depuis l’ouverture de cette page
+ + +

Créer une image avec ComfyUI

Décris l’image à générer. Alfred transmettra ta demande à Hermes.

+

Outils Alfred

${icon('settings')} Configuration${icon('server')} Surveillance
+ `; + shell.insertBefore(dashboard, shell.firstChild); + document.body.classList.add('alfred-glass-theme'); + document.getElementById('avName').textContent = initialName; + const wave = document.getElementById('wave'); + document.getElementById('avWaveHost').appendChild(wave); + window.dispatchEvent(new Event('resize')); + const $ = id => document.getElementById(id); + const timeZone = 'Europe/Paris'; + let lastStatus = 'idle'; + let mute = {mic_muted:false,speaker_muted:false}; + let refreshingAudio = false; + let toastTimer; + + function toast(message) { + const el = $('avToast'); el.textContent = String(message); el.hidden = false; + clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.hidden = true; }, 5500); + } + function clock() { + const now = new Date(); + $('avTime').textContent = new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit'}).format(now); + const date = new Intl.DateTimeFormat('fr-FR',{timeZone,weekday:'long',day:'numeric',month:'long',year:'numeric'}).format(now); + $('avDate').textContent = date.charAt(0).toUpperCase() + date.slice(1); + } + clock(); setInterval(clock, 15000); + function updateAudio(a) { + mute = {...mute,...a}; + const mic = Boolean(mute.mic_muted), speaker = Boolean(mute.speaker_muted); + for (const id of ['avTopMic','avMicButton']) { + const button = $(id); button.setAttribute('aria-pressed',String(mic)); + button.setAttribute('aria-label', mic ? 'Réactiver le microphone' : 'Couper le microphone'); + button.title = button.getAttribute('aria-label'); + button.querySelector('svg').outerHTML = icon(mic?'micOff':'mic'); + } + for (const id of ['avTopSpeaker','avSpeakerButton']) { + const button = $(id); button.setAttribute('aria-pressed',String(speaker)); + button.setAttribute('aria-label', speaker?'Réactiver la sortie audio':'Couper la sortie audio'); + button.title = button.getAttribute('aria-label'); + button.querySelector('svg').outerHTML = icon(speaker?'speakerOff':'speaker'); + } + $('avMicLabel').textContent = mic?'Coupé':'Actif'; + $('avSpeakerLabel').textContent = speaker?'Coupée':'Activée'; + dashboard.classList.toggle('av-mic-is-muted',mic); + dashboard.classList.toggle('av-speaker-is-muted',speaker); + renderMode(lastStatus); + } + async function refreshAudio() { + if (refreshingAudio) return; + refreshingAudio = true; + try { const r=await fetch('/api/audio/settings',{cache:'no-store'}); + if (!r.ok) throw Error('HTTP '+r.status); + updateAudio(await r.json()); + } catch (e) { console.warn('[ALFRED UI] Réglages audio :',e); } + finally {refreshingAudio=false;} + } + async function toggleAudio(kind) { + const prop = kind==='mic'?'mic_muted':'speaker_muted'; + try { + const r=await fetch('/api/audio/settings',{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({[prop]:!mute[prop]})}); + const data=await r.json(); + if (!r.ok) throw Error(data.error || 'HTTP '+r.status); + updateAudio(data); + toast(kind==='mic' ? (data.mic_muted?'Microphone coupé':'Microphone réactivé') : (data.speaker_muted?'Sortie audio coupée':'Sortie audio réactivée')); + } catch (e) { toast('Impossible de modifier le son : '+e.message); } + } + for (const id of ['avTopMic','avMicButton']) $(id).addEventListener('click',()=>toggleAudio('mic')); + for (const id of ['avTopSpeaker','avSpeakerButton']) $(id).addEventListener('click',()=>toggleAudio('speaker')); + $('avTopSettings').addEventListener('click',()=>{const btn=$('audioSettingsButton'); if(btn)btn.click();else location.assign('/config');}); + refreshAudio(); setInterval(()=>{if(!document.hidden)refreshAudio();},30000); + + function serviceText(state) {return state==='ok'?'Connecté':state==='offline'?'Hors ligne':state==='installed'?'Installé (non testé)':state==='unconfigured'?'Non configuré':'Non vérifié';} + function setService(id,dotid,label,state) { + $(id).textContent = `${label} : ${serviceText(state)}`; + $(dotid).className = `av-mini-dot ${state==='ok'?'is-ok':state==='offline'?'is-error':''}`; + } + async function refreshDashboard() { + try { + const r=await fetch('/api/alfred/dashboard',{cache:'no-store'}); + if(!r.ok) throw Error('HTTP '+r.status); + const d=await r.json(); + const h=d.home||{}, s=d.system||{}, ai=d.services||{}, w=d.weather||{}; + $('avHomeSummary').textContent=h.available?'Home Assistant connecté':(h.detail||'Données non disponibles'); + $('avLights').textContent = h.available?`${h.lights_on} lumière(s) allumée(s)`:'Lumières : indisponible'; + $('avCovers').textContent = h.available?`${h.covers_closed} volet(s) fermé(s)`:'Volets : indisponible'; + $('avIndoor').textContent = h.indoor_temperature!=null?`${h.indoor_temperature} °C · intérieur`:'Température : non disponible'; + if (window.alfredHomeCardCustomUpdate) window.alfredHomeCardCustomUpdate(); + $('avLoad').textContent = s.load_1m!=null?`Charge 1 min : ${s.load_1m}`:'Charge : non disponible'; + $('avMemory').textContent = s.memory_percent!=null?`Mémoire : ${s.memory_percent} %`:'Mémoire : non disponible'; + $('avDisk').textContent = s.disk_percent!=null?`Stockage : ${s.disk_percent} %`:'Stockage : non disponible'; + setService('avOllama','avOllamaDot','Ollama',ai.ollama); + setService('avHermes','avHermesDot','Hermes',ai.hermes); + setService('avComfy','avComfyDot','ComfyUI',ai.comfyui); + $('avAiSummary').textContent='États vérifiés / installation locale'; + $('avWeatherTemp').textContent=w.available&&w.temperature!=null?`${w.temperature} ${w.unit||'°C'}`:'—'; + $('avWeatherCondition').textContent=w.available?w.condition:'Non disponible'; + const sunny=['sunny','clear-night','partlycloudy'].includes(w.condition_id); + $('avWeatherIcon').textContent=sunny?'🌤️':w.condition_id==='rainy'?'🌧️':w.condition_id==='snowy'?'❄️':'☁️'; + } catch (e) { + $('avHomeSummary').textContent='Données indisponibles'; + $('avAiSummary').textContent='Surveillance indisponible'; + $('avWeatherCondition').textContent='Indisponible'; + console.warn('[ALFRED UI] Tableau de bord :',e); + } + } + refreshDashboard(); setInterval(()=>{if(!document.hidden)refreshDashboard();},30000); + async function refreshForecast() { + try { + const r=await fetch('/api/ha/weather',{cache:'no-store'}); + if(!r.ok)return; + const d=await r.json(); + const day=(d.daily||[])[0]; + const vals=[]; + if(day?.temperature!=null) vals.push('↑ '+day.temperature+'°'); + if(day?.templow!=null) vals.push('↓ '+day.templow+'°'); + $('avWeatherRange').textContent=vals.join(' '); + } catch (_) { /* Not all installations expose the weather forecast route. */ } + } + refreshForecast();setInterval(()=>{if(!document.hidden)refreshForecast();},180000); + + function renderMode(mode) { + lastStatus = mode||'idle'; + const states = {idle:['En veille','Dites « '+initialName+' » puis votre demande','Prêt'], + waiting:['En veille','Dites « '+initialName+' » puis votre demande','Prêt'], + listening:['Je vous écoute…','Quelle est votre demande ?','Écoute en cours'], + thinking:['Je réfléchis…','Traitement de la demande','Hermes travaille'], + speaking:['Je vous réponds…','Réponse vocale en cours','Piper parle'], + error:['Un problème est survenu','Consultez la transcription ou la surveillance','Erreur']}; + const item=states[mode]||states.idle; + $('avVoiceTitle').textContent=mute.mic_muted?'Microphone coupé':item[0]; + $('avVoiceSubtitle').textContent=mute.mic_muted?'Réactivez le micro pour utiliser le mot d’activation':item[1]; + $('avStatusText').textContent=mute.mic_muted?'Écoute désactivée':item[2]; + dashboard.dataset.mode=mode; + $('avOrb').setAttribute('aria-label',item[0]); + } + renderMode('idle'); + + const recent=[]; + let lastTranscript=''; + function pushRecent(text) { + const value=String(text||'').trim(); + if(!value || value===lastTranscript)return; + lastTranscript=value; + recent.unshift({text:value,at:new Date()}); + if(recent.length>5)recent.pop(); + const root=$('avHistoryList');root.replaceChildren(); + for(const e of recent) { + const node=document.createElement('span');node.className='av-history-item'; + const t=document.createElement('time');t.textContent=new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit'}).format(e.at); + const content=document.createElement('span');content.textContent=e.text; + node.append(t,content);root.append(node); + } + } + function focusScene(show) { + dashboard.classList.toggle('is-focused', Boolean(show)); + $('avBack').hidden = !show; + } + function onRender(d) { if(!d || typeof d!=='object')return; + if(d.type==='home')focusScene(false); + else if(d.type && d.type!=='text' || (d.text||d.title||d.url))focusScene(true); + } + const socket=window.alfredSocket; + socket?.on('alfred_state',state=>{ + renderMode(String(state.mode||'idle').toLowerCase()); + if(state.mode==='thinking' && state.transcript)pushRecent(state.transcript); + }); + socket?.on('alfred_render',onRender); + socket?.on('audio_settings',updateAudio); + fetch('/api/state',{cache:'no-store'}).then(r=>r.ok?r.json():null).then(s=>{if(s)renderMode(String(s.mode||'idle').toLowerCase());}).catch(()=>{}); + + async function goHome() { + focusScene(false); + if(window.AlfredViews?.goHome){try{await window.AlfredViews.goHome();}catch(_){}} + else if($('viewHome'))$('viewHome').click(); + } + $('avBack').addEventListener('click',goHome); + function openCamera() { + if($('cameraPopupButton')){ $('cameraPopupButton').click();return; } + const camera=document.querySelector('.camera-panel'); + if(!camera){ toast('Caméra non installée');return; } + const isOpen=document.body.classList.toggle('av-legacy-camera-open'); + const toggle=$('cameraToggle'); + if(toggle && (toggle.getAttribute('aria-pressed')==='true')!==isOpen)toggle.click(); + if(isOpen && !$('avLegacyCameraClose')){ + const close=document.createElement('button');close.id='avLegacyCameraClose';close.type='button';close.textContent='Fermer la caméra'; + close.addEventListener('click',openCamera);camera.prepend(close); + } + } + $('avTopCamera').addEventListener('click',openCamera); + + function openView(name) { + const f=window.AlfredViews?.[name]; + if(typeof f==='function'){ focusScene(true);f();return; } + const fallback={openWeather:'viewWeather',openTemperatures:'viewTemperatures',openCalendar:'viewCalendar'}[name]; + if(fallback && $(fallback)){focusScene(true);$(fallback).click();return;} + toast('Cette fonction n’est pas disponible sur cette installation.'); + } + async function quickCommand(action,description) { + try { + const r=await fetch('/api/alfred/dashboard/command',{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({action,description})}); + const data=await r.json(); if(!r.ok)throw Error(data.error||'HTTP '+r.status); + pushRecent(({proxmox:'Vérifier Proxmox',wazuh:'Consulter les alertes Wazuh',comfyui:'Générer une image'})[action]||action); + toast('Demande transmise à Hermes'); + } catch(e) {toast('Commande indisponible : '+e.message);} + } + function showTime() { + const current=new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit',second:'2-digit'}).format(new Date()); + const content=$('content');content.replaceChildren(); + const title=document.createElement('h1');title.textContent='Il est '+current; + const subtitle=document.createElement('p');subtitle.textContent='Heure de Paris · horloge locale du navigateur'; + content.append(title,subtitle); focusScene(true);pushRecent('Quelle heure est-il ?'); + } + async function act(name) { + if(name==='home'){await goHome();return;} + if(name==='house'){openView('openTemperatures');return;} + if(name==='weather'){openView('openWeather');return;} + if(name==='calendar'){ $('avMoreDialog').close();openView('openCalendar');return; } + if(name==='camera'){ $('avMoreDialog').close();openCamera();return; } + if(name==='media'){ $('avComfyDialog').showModal();$('avComfyPrompt').focus();return;} + if(name==='time'){showTime();return;} + if(name==='proxmox'||name==='wazuh'){await quickCommand(name);return;} + if(name==='comfyui'){ $('avComfyDialog').showModal();$('avComfyPrompt').focus();return;} + if(name==='more'){ $('avMoreDialog').showModal();return;} + if(name==='audio'){ $('avMoreDialog').close();$('avTopSettings').click();return; } + } + dashboard.addEventListener('click',event=>{ + const button=event.target.closest('[data-av-action]'); + if(button)act(button.dataset.avAction); + }); + $('avComfySubmit').addEventListener('click',()=>{ + const text=$('avComfyPrompt').value.trim(); + if(text.length<4){toast('Décris l’image à générer.');return;} + $('avComfyDialog').close();quickCommand('comfyui',text); + }); + window.addEventListener('resize',()=>{ /* legacy alfred.js recalculates the waveform canvas */ }); +})(); diff --git a/alfred/app/static/js/alfred-main-v2.js b/alfred/app/static/js/alfred-main-v2.js new file mode 100644 index 0000000..b9a0dc3 --- /dev/null +++ b/alfred/app/static/js/alfred-main-v2.js @@ -0,0 +1,71 @@ +/* ALFRED_GLASS_V2 — mise en page uniquement ; logique V1 et extensions conservees. */ +(() => { + 'use strict'; + const main = document.getElementById('alfredMainV1'); + if (!main || main.dataset.glassV2 === 'ready') return; + main.dataset.glassV2 = 'ready'; + const logo = main.querySelector('.av-logo'); + const name = document.getElementById('avName'); + const subtitle = document.getElementById('avVoiceSubtitle'); + if (!logo || !name) return; + let assistantName = String(name.textContent || '').trim() || 'Alfred'; + const cleanName = value => { + if(typeof value !== 'string')return ''; + const s = value.trim(); + return s.length >= 2 && s.length <= 30 ? s : ''; + }; + function setName(value) { + const s = cleanName(value); + if(!s) return; + assistantName=s; + name.textContent=s; + const first=Array.from(s)[0] || 'A'; + logo.textContent=first.toLocaleUpperCase('fr-FR'); + logo.setAttribute('aria-label',`Initiale de ${s}`); + // La V1 conserve son nom initial dans la phrase de veille : mise a jour apres chaque evenement. + const title=document.getElementById('avVoiceTitle'); + if(title && subtitle && (title.textContent === 'En veille' || title.textContent === 'En attente')) { + subtitle.textContent=`Dites « ${s} » puis votre demande`; + } + } + setName(assistantName); + async function fetchName(){ + try{ + const r=await fetch('/api/state',{cache:'no-store'}); + if(!r.ok)return; + const d=await r.json(); + setName(d.assistant_name || d.title || ''); + } catch(_){ /* La page reste utilisable si /api/state est indisponible. */ } + } + fetchName(); + setInterval(()=>{if(!document.hidden)fetchName();},20000); + window.addEventListener('focus',fetchName); + window.alfredSocket?.on('alfred_state',state=>{ + setName(state?.assistant_name || state?.title || assistantName); + }); + // Le menu ··· ouvre les options de la V1 au lieu de naviguer directement vers /config. + const oldMenu=main.querySelector('.av-head-actions > a[href="/config"]'); + if(oldMenu){ + const btn=document.createElement('button'); + btn.type='button';btn.id='avTopMenu';btn.className=oldMenu.className; + btn.setAttribute('data-av-action','more'); + btn.setAttribute('aria-label','Ouvrir le menu principal'); + btn.title='Menu : Configuration, Surveillance, Journaux'; + btn.innerHTML=oldMenu.innerHTML; + oldMenu.replaceWith(btn); + } + const grid=document.querySelector('#avMoreDialog .av-more-grid'); + if(grid&&!grid.querySelector('a[href="/logs"]')){ + const link=document.createElement('a'); + link.href='/logs';link.textContent='Journaux et historiques'; + link.setAttribute('aria-label','Ouvrir les journaux'); + const svg=document.createElementNS('http://www.w3.org/2000/svg','svg'); + svg.setAttribute('viewBox','0 0 24 24');svg.setAttribute('class','a-icon');svg.setAttribute('fill','none'); + svg.setAttribute('stroke','currentColor');svg.setAttribute('stroke-width','1.8'); + const d=document.createElementNS('http://www.w3.org/2000/svg','path'); + d.setAttribute('d','M6 3h9l4 4v14H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zM14 3v5h5M8 12h8M8 16h8'); + svg.appendChild(d);link.prepend(svg);grid.append(link); + } + // Rail et historique restes en DOM : les scripts des Listes et de la V1 les utilisent. + // Ne pas les supprimer au risque de casser les evenements ou le rafraichissement. +})(); diff --git a/alfred/app/static/js/alfred-maison-card.js b/alfred/app/static/js/alfred-maison-card.js new file mode 100644 index 0000000..b629e68 --- /dev/null +++ b/alfred/app/static/js/alfred-maison-card.js @@ -0,0 +1,31 @@ +/* Alfred Glass — aperçu Maison alimenté par les préférences de /config. */ +(() => { + 'use strict'; + let rows=null; + function render(){ + if(!rows)return; + const card=document.getElementById('avHomeSummary'); + if(!card)return; + const ids=['avLights','avCovers','avIndoor']; + rows.slice(0,3).forEach((r,i)=>{ + const node=document.getElementById(ids[i]); + if(!node)return; + node.textContent=`${r.label} : ${r.value}`; + node.closest('p').hidden=false; + }); + for(let i=rows.length;i{if(!document.hidden)update();},20000); +})(); diff --git a/alfred/app/static/js/alfred-name-dynamic.js b/alfred/app/static/js/alfred-name-dynamic.js new file mode 100644 index 0000000..92e8919 --- /dev/null +++ b/alfred/app/static/js/alfred-name-dynamic.js @@ -0,0 +1,93 @@ +/* ALFRED_IDENTITY_GLASS_V1 — dynamic user-facing assistant branding. + Technical keys, service names, tool IDs and logs remain unchanged. */ +(() => { + 'use strict'; + let active='',activeWake=''; + const replace=(el,oldName,newName)=>{ + if(!el || !el.firstChild || el.firstChild.nodeType!==3)return; + const value=el.firstChild.nodeValue||''; + const next=value.replace(/\bAlfred\b/g,newName); + if(next!==value)el.firstChild.nodeValue=next; + }; + function apply(n,wake){ + if(typeof n!=='string' || n.trim().length<2 || n.length>30)return; + active=n.trim(); + if(typeof wake==='string'&&wake.trim())activeWake=wake.trim(); + const letter=Array.from(active)[0]?.toLocaleUpperCase('fr-FR')||'A'; + const main=document.getElementById('alfredMainV1'); + if(main){ + const logo=main.querySelector('.av-logo'); + if(logo)logo.textContent=letter; + const title=document.getElementById('avName'); + if(title)title.textContent=active; + const info=document.getElementById('avVoiceSubtitle'); + if(info&&/Dites\s+[«"]/.test(info.textContent||''))info.textContent=`Dites « ${activeWake||active} » puis votre demande`; + const system=main.querySelector('.av-circle.system')?.parentElement?.querySelector('small'); + if(system)system.textContent='Mini-PC '+active; + replace(document.querySelector('#avMoreDialog h2'),'',active); + const moreTitle=document.querySelector('#avMoreDialog h2');if(moreTitle)moreTitle.textContent='Outils '+active; + const comfyDesc=document.querySelector('#avComfyDialog p');if(comfyDesc)comfyDesc.textContent='Décris l’image à générer. '+active+' transmettra ta demande à Hermes.'; + const submit=document.getElementById('avComfySubmit');if(submit)submit.textContent='Envoyer à '+active; + const hero=main.querySelector('.av-voice-hero');if(hero)hero.setAttribute('aria-label','Commande vocale '+active); + const oldConfig=main.querySelector('a[href="/config"][aria-label^="Configuration d"]');if(oldConfig)oldConfig.setAttribute('aria-label','Configuration de '+active); + const nav=document.querySelector('#avMoreDialog .av-more-grid'); + if(nav && !nav.querySelector('a[href="/identity"]')){ + const link=document.createElement('a');link.href='/identity';link.textContent='Identité de '+active;nav.append(link); + }else if(nav){const link=nav.querySelector('a[href="/identity"]');if(link)link.textContent='Identité de '+active;} + const legacyStage=document.getElementById('stage');if(legacyStage)legacyStage.setAttribute('aria-label','Réponse de '+active); + const legacyCamera=document.getElementById('cameraFeed');if(legacyCamera)legacyCamera.alt='Vidéo en direct de la caméra de '+active; + document.title=active+' · Accueil'; + } + const brand=document.querySelector('.admin-header .brand'); + if(brand){ + const b=brand.querySelector('strong'),o=brand.querySelector('.brand-orb'); + if(b)b.textContent=active;if(o)o.textContent=letter; + const loginOrb=document.querySelector('.auth-box .brand-orb');if(loginOrb)loginOrb.textContent=letter; + const authIntro=document.querySelector('.auth-box p');if(authIntro)authIntro.textContent=document.body.dataset.page==='surveillance'?('Connecte-toi pour consulter l’état de '+active+'.'):('Connecte-toi pour modifier la configuration de '+active+'.'); + const help=document.querySelector('.ag-help');if(help)help.textContent=help.textContent.replace(/redémarrer Alfred/g,'redémarrer '+active); + const restartInfo=document.querySelector('.restart-panel p');if(restartInfo)restartInfo.textContent='Redémarrer '+active+' pour appliquer les paramètres enregistrés. Action protégée par mot de passe administrateur.'; + const configLabel=document.getElementById('ALFRED_NAME');if(configLabel)configLabel.placeholder=active; + const hint=document.getElementById('pendingNotice'); + if(hint){const strong=hint.querySelector('strong');if(strong)strong.textContent='Redémarre '+active;} + const restartButton=document.getElementById('restartButton');if(restartButton)restartButton.textContent='Redémarrer '+active; + document.title=active+' · '+(document.body.dataset.page==='surveillance'?'Surveillance':'Configuration'); + const nav=brand.closest('.admin-header')?.querySelector('nav'); + if(nav && !nav.querySelector('a[href="/identity"]')){ + const a=document.createElement('a');a.href='/identity';a.textContent='Identité'; + const logout=nav.querySelector('button');if(logout)nav.insertBefore(a,logout);else nav.append(a); + } + const rail=document.querySelector('.ag-rail'); + if(rail && !rail.querySelector('a[href="/identity"]')){ + const a=document.createElement('a');a.href='/identity';a.innerHTML='✎Identité';rail.append(a); + } + } + const legacyAudio=document.querySelector('body > main .subtitle'); + if(legacyAudio){legacyAudio.textContent=active+' · Paramètres audio';document.title=active+' — Configuration audio'; + const hints=document.querySelectorAll('body > main p.hint');if(hints[0])hints[0].textContent='Désactive les réponses vocales de '+active+'.';if(hints[1])hints[1].textContent='Suspend l’écoute et la détection du mot « '+(activeWake||active)+' ».';} + const logsTitle=document.getElementById('lgName'); + if(logsTitle){ + logsTitle.textContent=active; + const letterEl=document.getElementById('lgLetter');if(letterEl)letterEl.textContent=letter; + const eyebrow=document.querySelector('.lg-eyebrow');if(eyebrow)eyebrow.textContent='OBSERVABILITÉ · '+active.toLocaleUpperCase('fr-FR'); + const nav=document.querySelector('.lg-nav'); + if(nav&&!nav.querySelector('a[href="/identity"]')){const a=document.createElement('a');a.href='/identity';a.textContent='Identité';nav.append(a);} + document.title='Journaux · '+active; + } + } + async function refresh(){ + try{const r=await fetch('/api/alfred/identity/name',{cache:'no-store'});if(!r.ok)return;const d=await r.json();apply(d.name,d.wake_word);}catch(_){} + } + const subtitle=document.getElementById('avVoiceSubtitle'); + if(subtitle){ + const observer=new MutationObserver(()=>{ + if(active&&/Dites\s+[«"]/.test(subtitle.textContent||'')){ + const desired=`Dites « ${activeWake||active} » puis votre demande`; + if(subtitle.textContent!==desired)subtitle.textContent=desired; + } + }); + observer.observe(subtitle,{childList:true,characterData:true,subtree:true}); + } + refresh();window.addEventListener('focus',refresh); + document.addEventListener('visibilitychange',()=>{if(!document.hidden)refresh();}); + setInterval(()=>{if(!document.hidden)refresh();},10000); +})(); diff --git a/alfred/app/templates/alfred_identity_glass.html b/alfred/app/templates/alfred_identity_glass.html new file mode 100644 index 0000000..ff0863c --- /dev/null +++ b/alfred/app/templates/alfred_identity_glass.html @@ -0,0 +1,41 @@ + + + + + +Identité · Assistant + + + + +
+ AAssistantIdentité et personnalité + +
+
+

PERSONNALISATION · HERMES

Identité de l'assistant

Personnalise la personnalité, le ton et les instructions permanentes transmises à Hermes.

Vérification…
+ + +
+ + diff --git a/alfred/app/templates/alfred_logs_v2.html b/alfred/app/templates/alfred_logs_v2.html new file mode 100644 index 0000000..c4dae03 --- /dev/null +++ b/alfred/app/templates/alfred_logs_v2.html @@ -0,0 +1,33 @@ + + + + + +Journaux · Alfred + + + + +
+ AAlfredJournaux de l'assistant + +
+
+

OBSERVABILITÉ · ALFRED

Journaux

Activité du service Alfred, reconnaissance vocale, Hermes et appels d'outils lorsqu'ils sont journalisés.

+
+ + + + + +
+
+
Connexion…
+
+

Source : journal systemd alfred.service. Les commandes Hermes exécutées dans un autre processus ne sont visibles ici que si Alfred les journalise. Les secrets courants sont masqués, mais vérifie les données avant tout partage.

+
+
+ + + + diff --git a/alfred/app/templates/config.html b/alfred/app/templates/config.html index e61a605..3a460fb 100644 --- a/alfred/app/templates/config.html +++ b/alfred/app/templates/config.html @@ -8,6 +8,8 @@ + +
@@ -71,5 +73,8 @@
A

Accès administrateur

Connecte-toi pour modifier la configuration d'Alfred.

Accès local / réseau privé uniquement.
+ + + diff --git a/alfred/app/templates/configuration.html b/alfred/app/templates/configuration.html index 6746c73..3fa41af 100644 --- a/alfred/app/templates/configuration.html +++ b/alfred/app/templates/configuration.html @@ -126,5 +126,7 @@ } })(); + + diff --git a/alfred/app/templates/index.html b/alfred/app/templates/index.html index 4422045..d6a4294 100644 --- a/alfred/app/templates/index.html +++ b/alfred/app/templates/index.html @@ -12,6 +12,12 @@ + + + + + +
@@ -183,5 +189,12 @@ + + + + + + + diff --git a/alfred/app/templates/surveillance.html b/alfred/app/templates/surveillance.html index 9b6f1f2..02ef60c 100644 --- a/alfred/app/templates/surveillance.html +++ b/alfred/app/templates/surveillance.html @@ -1,6 +1,8 @@ Alfred · Surveillance + +
@@ -41,4 +43,7 @@

A

Accès administrateur

Connecte-toi pour consulter l'état d'Alfred.

Accès local / réseau privé uniquement.
- + + + + diff --git a/hermes/MCP_proxmox/__pycache__/proxmox_mcp.cpython-313.pyc b/hermes/MCP_proxmox/__pycache__/proxmox_mcp.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0ebfe602117dc70808f54ee122dc6e88d4bd2ec4 GIT binary patch literal 10310 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 literal 0 HcmV?d00001 diff --git a/hermes/MCP_proxmox/proxmox_mcp.py b/hermes/MCP_proxmox/proxmox_mcp.py index 98583fb..79dff7e 100755 --- a/hermes/MCP_proxmox/proxmox_mcp.py +++ b/hermes/MCP_proxmox/proxmox_mcp.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv from proxmoxer import ProxmoxAPI from mcp.server.fastmcp import FastMCP -load_dotenv("/opt/hermes/proxmox-mcp/.env") +load_dotenv("/opt/hermes/MCP_proxmox/.env") mcp = FastMCP("proxmox-mcp")