From e2340807aa9ea4ad619782f9e0e48a56dcd4c98f Mon Sep 17 00:00:00 2001 From: pporcheret Date: Sun, 1 Nov 2026 15:01:22 +0100 Subject: [PATCH] v4 --- alfred/app/app.py | 63 +- alfred/app/cache/weather_outfit_cache.json | 116 +++ ...2b6c149e68e64c31f80b745eafde4bfeb9220c.jpg | Bin 0 -> 21143 bytes ...5278d0ef2328ae28cb5db15469120c7e86229c.jpg | Bin 0 -> 23477 bytes ...b6453d0577465b85dd35ce63b2b03c2f4db4b2.jpg | Bin 0 -> 27319 bytes ...11ba84bdac5f67ff83d3719c08b42ad0bb95e2.jpg | Bin 0 -> 22081 bytes alfred/app/data/alfred_features.sqlite3 | Bin 36864 -> 69632 bytes alfred/app/data/dona-learning-v93.sqlite3 | Bin 28672 -> 45056 bytes .../dona_clock_local.cpython-313.pyc | Bin 0 -> 4946 bytes .../dona_json_autorender_v1.cpython-313.pyc | Bin 0 -> 4394 bytes .../__pycache__/piper_tts.cpython-313.pyc | Bin 7305 -> 10612 bytes alfred/app/services/dona_clock_local.py | 66 ++ .../app/services/dona_json_autorender_v1.py | 81 ++ alfred/app/services/piper_tts.py | 55 ++ .../app/static/css/dona-reply-floating-v1.css | 119 +++ alfred/app/static/css/dona-waves-v1.css | 34 + alfred/app/static/js/dona-accueil-ecrit-v1.js | 2 + .../app/static/js/dona-reply-floating-v1.js | 49 + alfred/app/static/js/dona-waves-v1.js | 258 +++++ alfred/app/templates/index.html | 8 + .../manifest.json | 20 + .../static/js/dona-accueil-ecrit-v1.js | 104 ++ .../original/templates/index.html | 258 +++++ .../manifest.json | 23 + .../original/app.py | 887 +++++++++++++++++ .../original/static/css/dona-waves-v1.css | 22 + .../original/static/js/dona-waves-v1.js | 165 ++++ .../app.py | 917 ++++++++++++++++++ .../manifest.json | 14 + .../manifest.json | 19 + .../original/templates/index.html | 262 +++++ .../manifest.json | 22 + .../original/app.py | 882 +++++++++++++++++ .../original/services/piper_tts.py | 152 +++ .../static/js/dona-accueil-ecrit-v1.js | 104 ++ .../original/templates/index.html | 258 +++++ 36 files changed, 4952 insertions(+), 8 deletions(-) create mode 100644 alfred/app/cache/weather_outfit_images/3aa04ccb2c10320ee520ef45d52b6c149e68e64c31f80b745eafde4bfeb9220c.jpg create mode 100644 alfred/app/cache/weather_outfit_images/9d4b22e9c9a7b6cc7263ea7aab5278d0ef2328ae28cb5db15469120c7e86229c.jpg create mode 100644 alfred/app/cache/weather_outfit_images/9f4f50aa330ea2e933fdfa8071b6453d0577465b85dd35ce63b2b03c2f4db4b2.jpg create mode 100644 alfred/app/cache/weather_outfit_images/f0a4965a7e0789e79bb24c865a11ba84bdac5f67ff83d3719c08b42ad0bb95e2.jpg create mode 100644 alfred/app/services/__pycache__/dona_clock_local.cpython-313.pyc create mode 100644 alfred/app/services/__pycache__/dona_json_autorender_v1.cpython-313.pyc create mode 100644 alfred/app/services/dona_clock_local.py create mode 100644 alfred/app/services/dona_json_autorender_v1.py create mode 100644 alfred/app/static/css/dona-reply-floating-v1.css create mode 100644 alfred/app/static/css/dona-waves-v1.css create mode 100644 alfred/app/static/js/dona-reply-floating-v1.js create mode 100644 alfred/app/static/js/dona-waves-v1.js create mode 100644 alfred/backups/dona_avatar_2d_20261031_110849_457833/manifest.json create mode 100644 alfred/backups/dona_avatar_2d_20261031_110849_457833/original/static/js/dona-accueil-ecrit-v1.js create mode 100644 alfred/backups/dona_avatar_2d_20261031_110849_457833/original/templates/index.html create mode 100644 alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/manifest.json create mode 100644 alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/app.py create mode 100644 alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/static/css/dona-waves-v1.css create mode 100644 alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/static/js/dona-waves-v1.js create mode 100644 alfred/backups/dona_json_autorender_v1_20261101_141838_580140/app.py create mode 100644 alfred/backups/dona_json_autorender_v1_20261101_141838_580140/manifest.json create mode 100644 alfred/backups/dona_reply_floating_v1_20261101_132228_531763/manifest.json create mode 100644 alfred/backups/dona_reply_floating_v1_20261101_132228_531763/original/templates/index.html create mode 100644 alfred/backups/dona_waves_voice_v1_20261031_115646_870874/manifest.json create mode 100644 alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/app.py create mode 100644 alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/services/piper_tts.py create mode 100644 alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/static/js/dona-accueil-ecrit-v1.js create mode 100644 alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/templates/index.html diff --git a/alfred/app/app.py b/alfred/app/app.py index ed91612..8f219d9 100644 --- a/alfred/app/app.py +++ b/alfred/app/app.py @@ -26,6 +26,7 @@ from services.alfred_duplex import DuplexMonitor # ALFRED_DUPLEX_BARGE_IN_V1 from services.piper_tts import stop_playback as duplex_stop_piper from services import admin_console # ALFRED_ADMIN_PAGES_V1 from services.hermes_client import ask_hermes +from services import dona_clock_local as _dona_clock_local # DONA_CLOCK_SCULPTURE_V11 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 @@ -44,6 +45,11 @@ 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="*") +# DONA_WAVES_V1 : événement léger, pas de flux audio transmis au navigateur. +from services import piper_tts as _dona_waves_piper +_dona_waves_piper.dona_set_level_callback( + lambda level: socketio.emit('dona_piper_level', {'level': round(level, 4)}) +) 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. @@ -128,6 +134,24 @@ def conversation(score=1.0, injected_text=None): # ALFRED_DUPLEX_CONVERSATION_V content={"type": "text", "text": r.text} ) + # DONA_CLOCK_SCULPTURE_V11 : heure/date locales AVANT Hermes. + _dona_local_clock_reply = _dona_clock_local.answer(r.text) + if _dona_local_clock_reply is not None: + with STATE_LOCK: + STATE['content']={'type':'text','text':_dona_local_clock_reply} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('voice',_dona_v93_original_vosk,_dona_local_clock_reply)) + if not audio_settings.speaker_muted(): + emit_state('speaking') + _dona_clock_done=threading.Event() + _dona_clock_started=speak_async(_dona_local_clock_reply, + on_end=lambda: (_dona_clock_done.set(), + socketio.emit('alfred_tts_finished', {'source':'voice'}))) + if _dona_clock_started:_dona_clock_done.wait(timeout=120) + emit_state('idle',partial='') + return + # DONA_VISION_V95 : photo locale et compréhension par Hermes sur demande. if _dona_vision_v95.is_camera_question(r.text): try: @@ -200,7 +224,7 @@ def conversation(score=1.0, injected_text=None): # ALFRED_DUPLEX_CONVERSATION_V return # DONA_V8_DYNAMIC — creation visuelle explicite, source Hermes/MCP ou API directe. - if _dona_dynamic.is_dynamic_request(r.text): + if _dona_dynamic.is_dynamic_request(r.text) or _dona_json_autorender.is_monitor_request(r.text): _board = _dona_dynamic.build_dashboard(r.text, _dona_dashboard_agent.ask_dashboard) socketio.emit('dona_learning_turn',_dona_learning_v93.track('voice',_dona_v93_original_vosk,_board.get('title','Tableau de bord'))) _payload = {'type':'dona_dynamic', 'dashboard':_board} @@ -288,6 +312,7 @@ def conversation(score=1.0, injected_text=None): # ALFRED_DUPLEX_CONVERSATION_V else: speech = str(answer or "") display = {"type": "text", "text": speech} + display, speech = _dona_json_autorender.convert(answer, r.text, display, speech) # DONA_LEARNING_V93 : évaluation du tour Hermes, RAM seulement avant 👍 / 👎. socketio.emit('dona_learning_turn', @@ -642,13 +667,21 @@ def _alfred_dashboard_quick_command(prompt): _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} + # DONA_JSON_AUTORENDER_V1: operational shortcuts use the isolated + # dashboard session; ordinary shortcuts remain regular Hermes calls. + if _dona_dynamic.is_dynamic_request(prompt) or _dona_json_autorender.is_monitor_request(prompt): + _board = _dona_dynamic.build_dashboard(prompt, _dona_dashboard_agent.ask_dashboard) + _display = {'type':'dona_dynamic','dashboard':_board} + _speech = 'Voici le tableau de bord : ' + _board['title'] else: - _speech = str(_answer or '') - _display = {'type':'text','text':_speech} + _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} + _display, _speech = _dona_json_autorender.convert(_answer, prompt, _display, _speech) # Une image ComfyUI publiee pendant Hermes ne doit pas etre effacee. with STATE_LOCK: if IMAGE_REVISION == _image_revision_before: @@ -722,6 +755,17 @@ def _dona_submit_typed(prompt): _before_image = IMAGE_REVISION emit_state('thinking', partial='', transcript=prompt, content={'type': 'text', 'text': prompt}) + # DONA_CLOCK_SCULPTURE_V11 : même traitement depuis Écrire à Dona / raccourcis. + _dona_local_clock_reply = _dona_clock_local.answer(prompt) + if _dona_local_clock_reply is not None: + with STATE_LOCK: + STATE['content']={'type':'text','text':_dona_local_clock_reply} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('typed',prompt,_dona_local_clock_reply)) + emit_state('idle',partial='') + return + # DONA_VISION_V95 : demande écrite de vision depuis la caméra réelle. if _dona_vision_v95.is_camera_question(prompt): try: @@ -767,7 +811,7 @@ def _dona_submit_typed(prompt): return # DONA_V8_DYNAMIC — commandes visuelles saisies via le formulaire habituel. - if _dona_dynamic.is_dynamic_request(prompt): + if _dona_dynamic.is_dynamic_request(prompt) or _dona_json_autorender.is_monitor_request(prompt): _board = _dona_dynamic.build_dashboard(prompt, _dona_dashboard_agent.ask_dashboard) socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_board.get('title','Tableau de bord'))) _payload = {'type':'dona_dynamic','dashboard':_board} @@ -783,6 +827,7 @@ def _dona_submit_typed(prompt): else: _speech = str(_answer or '') _display = {'type':'text','text':_speech} + _display, _speech = _dona_json_autorender.convert(_answer, prompt, _display, _speech) with STATE_LOCK: if IMAGE_REVISION == _before_image: STATE['content'] = _display @@ -810,6 +855,8 @@ _dona_register_family(app, adult_command=_dona_submit_typed) # DONA_V8_DYNAMIC — tableau de bord choisi, généré et rendu en composants contrôlés. from services import dona_dynamic as _dona_dynamic +# DONA_JSON_AUTORENDER_V1 : rendu automatique JSON Hermes, demandes et raccourcis +from services import dona_json_autorender_v1 as _dona_json_autorender # DONA_V8_1_ISOLATION : tableaux isolés des conversations normales from services import dona_dashboard_agent as _dona_dashboard_agent diff --git a/alfred/app/cache/weather_outfit_cache.json b/alfred/app/cache/weather_outfit_cache.json index c700999..672df6f 100644 --- a/alfred/app/cache/weather_outfit_cache.json +++ b/alfred/app/cache/weather_outfit_cache.json @@ -1032,5 +1032,121 @@ }, "cached_at": 1793428787.6485865, "focus_label": "aujourd’hui" + }, + "9f4f50aa330ea2e933fdfa8071b6453d0577465b85dd35ce63b2b03c2f4db4b2": { + "file": "9f4f50aa330ea2e933fdfa8071b6453d0577465b85dd35ce63b2b03c2f4db4b2.jpg", + "prompt_ids": [ + "679937f6-bf42-4cc2-9c69-3d21fbd000d3", + "d49063bf-3ec9-41ce-bd9a-f27e94084647" + ], + "reviews": { + "child": { + "score": 0.0, + "ok": false, + "issues": [ + "review_unavailable" + ], + "reviewed": false, + "reference_applied": false + }, + "adult": { + "score": 0.0, + "ok": false, + "issues": [ + "review_unavailable" + ], + "reviewed": false, + "reference_applied": false + } + }, + "cached_at": 1793443488.2070868, + "focus_label": "aujourd’hui" + }, + "9d4b22e9c9a7b6cc7263ea7aab5278d0ef2328ae28cb5db15469120c7e86229c": { + "file": "9d4b22e9c9a7b6cc7263ea7aab5278d0ef2328ae28cb5db15469120c7e86229c.jpg", + "prompt_ids": [ + "15bd0716-ea47-4709-aba2-0da675d97017", + "dc1646e7-cd5f-4892-b2a0-e69d931688c0" + ], + "reviews": { + "child": { + "score": 0.0, + "ok": false, + "issues": [ + "review_unavailable" + ], + "reviewed": false, + "reference_applied": false + }, + "adult": { + "score": 0.0, + "ok": false, + "issues": [ + "review_unavailable" + ], + "reviewed": false, + "reference_applied": false + } + }, + "cached_at": 1793475062.3883624, + "focus_label": "aujourd’hui" + }, + "3aa04ccb2c10320ee520ef45d52b6c149e68e64c31f80b745eafde4bfeb9220c": { + "file": "3aa04ccb2c10320ee520ef45d52b6c149e68e64c31f80b745eafde4bfeb9220c.jpg", + "prompt_ids": [ + "401c7ee7-48ba-4614-ab21-bcbcec060e53", + "898173ea-bba0-4c35-a025-fa45c4b72140" + ], + "reviews": { + "child": { + "score": 0.0, + "ok": false, + "issues": [ + "review_unavailable" + ], + "reviewed": false, + "reference_applied": false + }, + "adult": { + "score": 0.0, + "ok": false, + "issues": [ + "review_unavailable" + ], + "reviewed": false, + "reference_applied": false + } + }, + "cached_at": 1793526148.471928, + "focus_label": "aujourd’hui" + }, + "f0a4965a7e0789e79bb24c865a11ba84bdac5f67ff83d3719c08b42ad0bb95e2": { + "file": "f0a4965a7e0789e79bb24c865a11ba84bdac5f67ff83d3719c08b42ad0bb95e2.jpg", + "prompt_ids": [ + "e1ccd704-a7b5-4375-8d4c-815b7366bdf5", + "9d61529b-b5d0-4853-8974-29113770531f" + ], + "reviews": { + "child": { + "score": 0.0, + "ok": false, + "issues": [ + "review_unavailable" + ], + "reviewed": false, + "reference_applied": false + }, + "adult": { + "score": 0.0, + "ok": false, + "issues": [ + "review_unavailable" + ], + "reviewed": false, + "reference_applied": false + } + }, + "cached_at": 1793535766.1160083, + "focus_label": "aujourd’hui" } } \ No newline at end of file diff --git a/alfred/app/cache/weather_outfit_images/3aa04ccb2c10320ee520ef45d52b6c149e68e64c31f80b745eafde4bfeb9220c.jpg b/alfred/app/cache/weather_outfit_images/3aa04ccb2c10320ee520ef45d52b6c149e68e64c31f80b745eafde4bfeb9220c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..247ecde9cc00fc986551bcdb4437cf1cb5e3ae58 GIT binary patch literal 21143 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 RoXDSt-f(RQmh5c&e*maYV1xhw literal 0 HcmV?d00001 diff --git a/alfred/app/cache/weather_outfit_images/9f4f50aa330ea2e933fdfa8071b6453d0577465b85dd35ce63b2b03c2f4db4b2.jpg b/alfred/app/cache/weather_outfit_images/9f4f50aa330ea2e933fdfa8071b6453d0577465b85dd35ce63b2b03c2f4db4b2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9a755eece3899e1c9fb43e58bdb3b8831a492cad GIT binary patch literal 27319 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 vT%Di2qCgl$IQ`S=U{%KFa4*{}%oq^HYKO literal 0 HcmV?d00001 diff --git a/alfred/app/cache/weather_outfit_images/f0a4965a7e0789e79bb24c865a11ba84bdac5f67ff83d3719c08b42ad0bb95e2.jpg b/alfred/app/cache/weather_outfit_images/f0a4965a7e0789e79bb24c865a11ba84bdac5f67ff83d3719c08b42ad0bb95e2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fca877fb6052d9a6943bd6eecfd6da1a9b53c089 GIT binary patch literal 22081 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

r> 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 literal 0 HcmV?d00001 diff --git a/alfred/app/data/alfred_features.sqlite3 b/alfred/app/data/alfred_features.sqlite3 index ed79a6f6a91cfc55b7f2d257647eeca1fdafd0dd..e54fec039db1a5123ad2d8a70d3a52574401433c 100644 GIT binary patch literal 69632 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 BIJ5u& delta 156 zcmZozz|ydQX@WGbAOiyfCloVJ)G%ih+?cRHUw{`V#K?V{f%`UN3;%EaZyOs=b8o(# zt;)i|1(M;v&M;ZPL1D9zgCqaM33-#R`|GMO@;_$af6RZBe>o89gCL(GI@~O%u$!Mn hhS{ETvc8PurUG_>#R3ZiIGKQEvI8;4X2yaK`~Z1mBq#s? diff --git a/alfred/app/data/dona-learning-v93.sqlite3 b/alfred/app/data/dona-learning-v93.sqlite3 index 5ec095a660465b3c2fc3db09b7a94b3c5cf0ce5a..ca4371d0980f7cd725f3749c8d8d0de0e0c46a0d 100644 GIT binary patch literal 45056 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 Gb^RZBV>WaE delta 99 zcmZp8z|`=7ae_1}D+2=q`$PqMM%Il93;4Nsfm}weUjg6jMn}e%1va&Jqe`etS zyjjrTB|k4WBeN`LT54)aQetxUCIc1)pzI$8{y#w3_xv0{&CJZ4oLrk34gT{30QiC# AQUCw| diff --git a/alfred/app/services/__pycache__/dona_clock_local.cpython-313.pyc b/alfred/app/services/__pycache__/dona_clock_local.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6bede123696680a506e36fcc3111f85ae37d99a1 GIT binary patch literal 4946 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 literal 0 HcmV?d00001 diff --git a/alfred/app/services/__pycache__/dona_json_autorender_v1.cpython-313.pyc b/alfred/app/services/__pycache__/dona_json_autorender_v1.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55dd606e24b85ee7c631ce6a525e862d105662f6 GIT binary patch literal 4394 zcmbtXO>7&-6`tiTxg?h|ZHbnDlG%W*4=PZgHjsl3J@l9$D9|Hmwvf6}dvS}PmqLLYpvb9jmLe%D zYJ#Ez?Cku!dGGDao9}(Q+|m+6P+}*(zL5(d^qh6-#Zzl=%XgshDUuOIGADZmxd9LL zVAkdbdCJ3E7!(G))a#CU2Ymw~6)_qShd0($HQzKKOR{e}x*^b@9Fj%1CCM$a-))8D z4RXM3wa8&P=(aXw`RK;t(rH6i;+PWKB-k1!nB3M(OVhIla9XjHgNB})gqArzW+*fb za(bFjLUG2R_#B~mV!~JsE&>Mg!IY{1E2HAfT*XKWM*b$3I_;Y-xG zoi}cGzpoVSajcFL^(Jhe(TI&z0nvZy-I#-lT_aU%Gqt z-udaVs(;(OzoX*ssQSC6`WK~^dwn1Gl{cUGy)rMoU6I~i2uBvB@I%j|z$bwv4;B^z>UgIW0*T78kYVWwqG zXz70J9S3nw9^3XU&&(7Ondw4Ve1Bb{5rl zRt~hGbAWETkXYAVLik^Ij=|}L{mtET`$yOgnvg2HkOW|xMbY4p<8{HGvi&&AV4Uax z8YHEds-`)DHCZ4}Jwwod96+~#P*Ln~S9C-@ZXa%Dfe>{(Dmp>9>5z&TJ~%>JQ!R(j z63Y>CkQg0-T|md1w(|v(!np#HIzB}=ZxQN<3f4DJ3LixIs8pBX|kZktk5u< z2&W)rr8P*eB<1?k38QR@`5jbKXi?lW{o3Q6c@bAcyeMs+&OW+%|K{v#)%Ja#ot~Et zSER$wLt7Twwmgd8kI!W1&P>P4ZSis_z96;UJO0t}>4~beV>VWm4lT59d$j-l{+Ud* zb@$y5hA#p=qZkMfa|ATi`3-Qc9LZSN)-A(^T$qjpi!CD~ zNjzd*-#0=}j_3{-EP6)nINY5mPZ=|X)MSr2B4cifN}`;51x&_SR$_Epitz$H#avG@ zLz-^axDt(mR1Z8RV2Y?sLv!Dv^?2bAM4>oq=45L?F7Fv z=uLhn%5YJkczT#vHidJ>F5jRBUCIiloo;fg1B?_884x>+Yy-25Bej@;RbA{tNwAQ!MBxyG_m}27nN`O$O~AsMEF%w|0V8 zSm>Rh)Syn_GGJ(}=^rGMZY%1gJ3%SMtYRL}T~IlJ6&obI7W({(nR0(5C?mmd!DAAr zfSm-l&FrZL_f4Jpo3!C!=d}7@@8he0S_|Q}C6C~ZEJpBrR z;k$vw@GJA-?q}idxj^~7YnAW^W&a2NUJSKA4*cd=Svs(cIB(=Hpn-??cP4|ECrA2U zMPIzSxxa`1;s^(|D!^#HH1OtEFtR!(6P_R7Fk2xXd44#*%AVs8nJf~ncJ=@VNs4uo zR~f~JaaJI23Ti_}<`aS}#5}SW-ah8qL3r5es8~FG%FyBe%EA|i&R@fDo;E-YOK2{> z#DKvQH?Y>mj9CMoypjb(p_@S=ZD$oz4^hMhoIvB}s`$zkLsK{yI6=Ufq4{^9|MALkr>dyT2&=cPxe?(^qCQ)zHDIGYewqmp`vhEJQlyHdmxK z%6(VL*M3?~Wh&RQmA-6Q%9e$!i$~zP>mP#%$4A{8N!g8$`f72_Vxtz^Zi}(7Vi2e` zF1hy-)-x*M(g4aPYlOhR?&5|eSF$KEVo}$#dPP$XGmWlcws$q%VMk1*(uSH!Il)va zV*`kgREn|yssXzXs1D>1cb(&}|2x#~euzQA@EQ5i#Uc3&1%iv*p9B{yToiDR&OL>v z$lakDVey}+{nNii-+?s{$jCei62X+i%5Wre7jZT#yQu(e*kpU`MdxC literal 0 HcmV?d00001 diff --git a/alfred/app/services/__pycache__/piper_tts.cpython-313.pyc b/alfred/app/services/__pycache__/piper_tts.cpython-313.pyc index 1e2b8862584755d108c8c5a0c7f69861f1d15ba0..5f93279b1152cc5283b9bd7bc9cd56a2e38ff0aa 100644 GIT binary patch delta 3621 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 lk0GKd2D str: + text = unicodedata.normalize('NFKD', str(text or '').lower()) + text = ''.join(c for c in text if not unicodedata.combining(c)) + text = re.sub(r'[^a-z0-9]+', ' ', text).strip() + return re.sub(r'^(?:dona|alfred)\s+', '', text) + + +def intent(text: str) -> str | None: + """Renvoie 'time', 'date', 'both' ou None; pas de faux positif trajet/durée.""" + s = _normalized(text) + if not s or len(s) > 140: + return None + # Ne pas répondre avec l'heure de Paris à une question sur une autre ville. + if re.search(r'\b(?:heure|est il|est elle)\s+(?:a|en|au|aux|dans)\s+[a-z]', s): + return None + # Ne pas renvoyer la date actuelle à une question concernant une date future/passée. + if re.search(r'\bdate\s+(?:du|de|pour|a)\s+(?:\d|demain|hier)', s): + return None + t = ( + bool(re.search(r'\b(?:quelle?|quel)\s+heure\s+(?:est\s+il|il\s+est|est\s+elle)\b', s)) + or bool(re.search(r'\bil\s+est\s+(?:quelle?|quel)\s+heure\b', s)) + or bool(re.search(r'\b(?:donne|dis|dites|affiche|montre)\s+(?:moi\s+)?(?:l\s+)?heure\b', s)) + or bool(re.search(r'\b(?:heure\s+actuelle|heure\s+maintenant|il\s+est\s+quelle\s+heure)\b', s)) + ) + d = ( + bool(re.search(r'\b(?:quelle?|quel)\s+(?:est\s+)?(?:la\s+)?date\s+(?:d\s+aujourd\s+hui|aujourd\s+hui|du\s+jour)?\b', s)) + or bool(re.search(r'\b(?:on\s+est|nous\s+sommes|sommes\s+nous)\s+(?:quel|quelle|le\s+combien)\s+(?:jour|date)?\b', s)) + or bool(re.search(r'\b(?:quel\s+jour\s+(?:est\s+on|sommes\s+nous|on\s+est)|quelle\s+date\s+sommes\s+nous)\b', s)) + or bool(re.search(r'\b(?:donne|dis|affiche|montre)\s+(?:moi\s+)?(?:la\s+)?date\s+(?:du\s+jour)?\b', s)) + ) + if t and d: + return 'both' + if t: + return 'time' + if d: + return 'date' + return None + + +def answer(text: str, *, now: datetime | None = None) -> str | None: + which = intent(text) + if which is None: + return None + if now is None: + try: + tz = ZoneInfo(os.getenv('DONA_TIMEZONE') or os.getenv('ALFRED_TIMEZONE') or 'Europe/Paris') + except (ZoneInfoNotFoundError, ValueError, KeyError): + tz = ZoneInfo('Europe/Paris') + now = datetime.now(tz) + hour = f'Il est {now.hour} heures {now.minute:02d}.' + day = f"Aujourd'hui, nous sommes le {DAYS[now.weekday()]} {now.day} {MONTHS[now.month-1]} {now.year}." + return (hour + ' ' + day) if which == 'both' else hour if which == 'time' else day diff --git a/alfred/app/services/dona_json_autorender_v1.py b/alfred/app/services/dona_json_autorender_v1.py new file mode 100644 index 0000000..cd5cd80 --- /dev/null +++ b/alfred/app/services/dona_json_autorender_v1.py @@ -0,0 +1,81 @@ +"""Dona: reuse the existing, data-only dashboard renderer for Hermes replies. + +This module never executes model-supplied HTML, JavaScript or instructions. Only +`dona_dynamic.normalize_dashboard` can build cards; invalid content remains text. +""" +from __future__ import annotations + +import re +from typing import Any + +from services import dona_dynamic + + +MAX_JSON = dona_dynamic.MAX_RESPONSE + + +def is_monitor_request(prompt: str) -> bool: + """Route clearly operational Proxmox/Wazuh checks to existing dashboard API.""" + q = str(prompt or '').casefold() + if not re.search(r'\b(proxmox|wazuh|cluster|clusters)\b', q): + return False + return bool(re.search( + r'\b(v[eé]rifi(?:e|es|er|ez)|contr[oô]le(?:r|s|z)?|surveille(?:r|s|z)?|' + r'montre|affiche|tableau|[eé]tat|statut|status|ressources?|' + r'performances?|noeuds?|nœuds?|serveurs?|alertes?|supervision|' + r'fonctionn(?:e|ement)|disponibilit[eé])\b', q)) + + +def _candidate_text(answer: Any, display: Any) -> str | None: + if isinstance(display, dict): + if isinstance(display.get('cards'), list): + # Raw dashboard object should not be converted via repr(). + import json + return json.dumps(display, ensure_ascii=False) + if display.get('type') in (None, 'text'): + raw = display.get('text') + if isinstance(raw, str) and '"cards"' in raw: + return raw + if isinstance(answer, str) and '"cards"' in answer: + return answer + if isinstance(answer, dict): + if isinstance(answer.get('cards'), list): + import json + return json.dumps(answer, ensure_ascii=False) + for key in ('text', 'speech'): + raw = answer.get(key) + if isinstance(raw, str) and '"cards"' in raw: + return raw + return None + + +def _clean_line_continuations(text: str) -> str: + """Repair two common harmless Hermes JSON formatting artefacts only. + + Trailing backslash + real newline (copied JSON), and invalid ``\\~`` + escaping in approximate values. Other malformed content is left untouched. + """ + text = re.sub(r'\\[ \t]*\r?\n', '\n', text) + return text.replace('\\~', '~') + + +def as_board(answer: Any, display: Any, prompt: str) -> dict | None: + """Return a server-validated dashboard, or None for an ordinary answer.""" + raw = _candidate_text(answer, display) + if not raw or len(raw) > MAX_JSON: + return None + try: + extracted = dona_dynamic.extract_dashboard(_clean_line_continuations(raw)) + return dona_dynamic.normalize_dashboard(extracted, prompt=prompt, origin='hermes') + except (ValueError, TypeError, KeyError): + return None + + +def convert(answer: Any, prompt: str, display: Any, speech: str): + """Convert ONLY actual card JSON. Preserve ordinary answers and images.""" + if isinstance(display, dict) and display.get('type') not in (None, 'text'): + return display, speech + board = as_board(answer, display, prompt) + if board is None: + return display, speech + return {'type': 'dona_dynamic', 'dashboard': board}, 'Voici le tableau de bord : ' + board['title'] diff --git a/alfred/app/services/piper_tts.py b/alfred/app/services/piper_tts.py index 91f0fb4..3678eb3 100644 --- a/alfred/app/services/piper_tts.py +++ b/alfred/app/services/piper_tts.py @@ -91,6 +91,58 @@ def stop_playback(): pass +# DONA_WAVES_V1 — volume réel du WAV Wyoming Piper (50 ms, hors micro). +from array import array as _dona_array +import sys as _dona_sys +_dona_level_callback = None + +def dona_set_level_callback(callback): + global _dona_level_callback + _dona_level_callback = callback + +def _dona_emit_level(value): + if _dona_level_callback is not None: + try: + _dona_level_callback(max(0.0, min(1.0, float(value)))) + except Exception: + pass # Une erreur côté interface ne doit jamais interrompre Piper. + +def _dona_waves_stream_envelope(wav_path, proc): + """Échantillonne le PCM pendant aplay, sans enregistrer ni exposer le son.""" + try: + with wave.open(wav_path, 'rb') as stream: + if stream.getsampwidth() != 2: + return # Autre format : état « speaking » en secours côté navigateur. + rate=stream.getframerate() + if rate <= 0: + return + frames=max(1,int(rate * .05)) + started=time.monotonic() + index=0 + while proc.poll() is None: + raw=stream.readframes(frames) + if not raw: + break + samples=_dona_array('h') + samples.frombytes(raw) + if _dona_sys.byteorder=='big': + samples.byteswap() + stride=max(1,len(samples)//420) + window=samples[::stride] + rms=(sum(s*s for s in window)/max(1,len(window)))**.5 + desired=started+index*frames/rate + remaining=desired-time.monotonic() + if remaining>0: + time.sleep(min(remaining,.07)) + if proc.poll() is not None: + break + _dona_emit_level(min(1.0,rms/6500)) + index+=1 + except Exception as exc: + print('[DONA WAVES] analyse PCM impossible :',type(exc).__name__,flush=True) + finally: + _dona_emit_level(0) + def speak(text): if not text or not text.strip(): return @@ -123,6 +175,9 @@ def speak(text): _playback_proc = proc if _playback_cancel.is_set() and proc.poll() is None: proc.terminate() + # DONA_WAVES_V1 : animation par l'amplitude du vrai son joué. + threading.Thread(target=_dona_waves_stream_envelope, + args=(wav_path,proc),daemon=True).start() try: result = proc.wait(timeout=120) except subprocess.TimeoutExpired: diff --git a/alfred/app/static/css/dona-reply-floating-v1.css b/alfred/app/static/css/dona-reply-floating-v1.css new file mode 100644 index 0000000..8f6da53 --- /dev/null +++ b/alfred/app/static/css/dona-reply-floating-v1.css @@ -0,0 +1,119 @@ +/* DONA_REPLY_FLOATING_V1 — Les réponses textuelles ne recouvrent plus les animations. */ +/* La carte occupe une bande latérale, sans voile de fond ni blocage de la scène. */ +/* La classe de l’écran principal est synchronisée en JS. */ +body.dona-family-v1 .alfred-main-v1.dona-reply-background-visible.is-focused .av-voice-hero, +body.dona-family-v1 .alfred-main-v1.dona-reply-background-visible.is-focused .dona-art-center { + opacity:1!important; visibility:visible!important; pointer-events:none!important; +} +body.dona-family-v1 .alfred-main-v1.dona-reply-background-visible.is-focused .dona-art-center > * { + visibility:visible!important; +} + +body.dona-family-v1.dona-holo-v1.alfred-glass-theme #stage.dona-reply-compact:not(.dona-reply-expanded) { + position:fixed!important; + left:auto!important; + top:clamp(140px,24vh,285px)!important; + right:clamp(16px,2.5vw,44px)!important; + bottom:auto!important; + transform:none!important; + width:min(410px,calc(100vw - 32px))!important; + max-width:calc(100vw - 32px)!important; + max-height:min(65dvh,580px)!important; + min-height:0!important; + padding:48px 15px 15px!important; + overflow:auto!important; + display:block!important; + border:1px solid rgba(149,177,232,.36)!important; + border-radius:22px!important; + background:linear-gradient(145deg,rgba(12,20,43,.88),rgba(19,20,39,.84))!important; + box-shadow:0 18px 50px rgba(0,0,0,.38),inset 0 1px rgba(255,255,255,.05)!important; + -webkit-backdrop-filter:blur(10px)!important; + backdrop-filter:blur(10px)!important; + z-index:28!important; + align-items:stretch!important; + justify-content:flex-start!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) #content { + width:100%!important; + max-width:100%!important; + min-width:0!important; + max-height:none!important; + overflow:visible!important; + margin:0!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .av-reponse-actions { + top:10px!important;right:10px!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .av-reponse-close { + width:33px!important;min-width:33px!important;min-height:33px!important; + padding:6px!important;border-radius:11px!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-response { + display:flex!important;flex-direction:column!important;gap:0!important; + margin:0!important;max-width:100%!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-primary { + padding:4px 2px 2px!important; + min-height:0!important; + border:0!important;border-radius:0!important; + box-shadow:none!important;background:transparent!important; + gap:10px!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-primary::before, +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-response::after { + display:none!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-kicker { + font-size:10px!important;letter-spacing:.09em!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-headline { + font-size:clamp(17px,1.45vw,21px)!important;line-height:1.25!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-excerpt { + font-size:13px!important;line-height:1.52!important; + display:-webkit-box!important;-webkit-box-orient:vertical!important;-webkit-line-clamp:4!important; + line-clamp:4!important;overflow:hidden!important; + white-space:normal!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-facts { + display:none!important; +} +body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-more { + min-height:38px!important;padding:9px 12px!important;font-size:12px!important; + margin-top:5px!important; +} +/* Mode développé : le contenu et les cartes d'origine réapparaissent. + L'utilisateur choisit volontairement d'occuper l'écran. */ +body.dona-family-v1.dona-holo-v1.alfred-glass-theme #stage.dona-reply-compact.dona-reply-expanded { + position:fixed!important; + left:clamp(8px,2vw,30px)!important;right:clamp(8px,2vw,30px)!important; + top:clamp(85px,10vh,125px)!important;bottom:clamp(95px,9vh,120px)!important; + width:auto!important;max-width:none!important;max-height:none!important; + padding:clamp(62px,8vh,92px) 20px 24px!important; + transform:none!important;overflow:auto!important; + background:linear-gradient(145deg,rgba(3,8,20,.97),rgba(10,16,35,.97))!important; + border:1px solid rgba(146,172,231,.38)!important;z-index:30!important; +} +/* L'état visuel compact ne s'applique pas aux cartes riches (météo, vidéo, tableaux...). */ +@media (max-width:1050px) { + body.dona-family-v1.dona-holo-v1.alfred-glass-theme #stage.dona-reply-compact:not(.dona-reply-expanded) { + left:clamp(12px,3vw,28px)!important; + right:clamp(12px,3vw,28px)!important; + top:auto!important; + bottom:clamp(110px,12vh,160px)!important; + width:auto!important; + max-width:none!important; + max-height:min(31dvh,275px)!important; + padding:43px 14px 12px!important; + } +} +@media (max-width:600px) { + body.dona-family-v1.dona-holo-v1.alfred-glass-theme #stage.dona-reply-compact:not(.dona-reply-expanded) { + left:9px!important;right:9px!important; + bottom:clamp(95px,13vh,125px)!important; + border-radius:15px!important;max-height:29dvh!important; + } + body.dona-family-v1.dona-holo-v1 #stage.dona-reply-compact:not(.dona-reply-expanded) .dh-excerpt { + -webkit-line-clamp:2!important;line-clamp:2!important; + } +} diff --git a/alfred/app/static/css/dona-waves-v1.css b/alfred/app/static/css/dona-waves-v1.css new file mode 100644 index 0000000..1a85b3a --- /dev/null +++ b/alfred/app/static/css/dona-waves-v1.css @@ -0,0 +1,34 @@ +/* Dona — vagues vocales, sélection réversible sans désinstaller. */ +body.dona-family-v1.dona-waves-selected .dona-organic-wrap { + width:min(93vw,1400px)!important; + height:clamp(155px,28vh,330px)!important; + min-width:0!important;min-height:155px!important; + max-width:100%!important;filter:none!important; + overflow:visible!important; +} +body.dona-family-v1.dona-waves-selected #donaOrganicCanvas, +body.dona-family-v1.dona-waves-selected .dona-orb-reflection {display:none!important} +body.dona-family-v1 .dona-organic-wrap #donaWavesCanvas { + display:none;position:absolute;inset:0;width:100%;height:100%;pointer-events:none; +} +body.dona-family-v1.dona-waves-selected .dona-organic-wrap #donaWavesCanvas {display:block} +body.dona-family-v1.dona-waves-selected .dona-art-name {margin-top:-5px!important} +.dona-waves-menu-title{color:#c7b8cc;font-size:11px;letter-spacing:.07em;padding:10px 10px 5px;text-transform:uppercase} +.dv92-menu-entry.dona-waves-option[aria-checked="true"] {background:rgba(94,148,208,.19);color:#ffe1b8} +.dona-waves-option .dona-waves-choice-icon{width:22px;min-width:22px;text-align:center;font-size:18px} +@media(max-width:900px){ + body.dona-family-v1.dona-waves-selected .dona-organic-wrap{height:clamp(145px,23vh,240px)!important;min-height:145px!important;width:min(96vw,740px)!important} + body.dona-family-v1.dona-waves-selected .dona-art-name{font-size:clamp(32px,8vw,50px)} +} + +/* DONA_SCULPTURE_V11 — Visuel organique plus concentré, mêmes events Piper. */ +body.dona-family-v1.dona-sculpture-selected .dona-organic-wrap { + width:min(94vw,990px)!important; + height:clamp(210px,39vh,440px)!important; + min-height:210px!important; +} +@media(max-width:900px){ + body.dona-family-v1.dona-sculpture-selected .dona-organic-wrap{ + width:min(97vw,820px)!important;height:clamp(180px,28vh,295px)!important;min-height:180px!important; + } +} diff --git a/alfred/app/static/js/dona-accueil-ecrit-v1.js b/alfred/app/static/js/dona-accueil-ecrit-v1.js index ab076b1..6c14bff 100644 --- a/alfred/app/static/js/dona-accueil-ecrit-v1.js +++ b/alfred/app/static/js/dona-accueil-ecrit-v1.js @@ -83,6 +83,8 @@ if(k===0)ctx.moveTo(x,y);else ctx.lineTo(x,y); }ctx.closePath();} function frame(now){raf=requestAnimationFrame(frame);if(document.hidden||now-last<30)return;last=now; + // DONA_WAVES_V1 : ne pas dessiner une orbe cachée; bascule possible à chaud. + if(window.DonaWaves && window.DonaWaves.isActive())return; const mode=root.dataset.familyMode||'idle';const speed=mode==='listening'?1.7:mode==='thinking'?1.38:mode==='speaking'?1.5:.75; if(!reduce)phase+=.024*speed; ctx.clearRect(0,0,w,h);ctx.save();ctx.translate(w/2,h/2);const r=Math.min(w,h)*.33; diff --git a/alfred/app/static/js/dona-reply-floating-v1.js b/alfred/app/static/js/dona-reply-floating-v1.js new file mode 100644 index 0000000..8c7e3d7 --- /dev/null +++ b/alfred/app/static/js/dona-reply-floating-v1.js @@ -0,0 +1,49 @@ +/* DONA_REPLY_FLOATING_V1: carte texte latérale sans recouvrir le moteur d'animations. */ +(() => { + 'use strict'; + if (window.__donaReplyFloatingV1) return; + window.__donaReplyFloatingV1 = true; + + function setup() { + const main = document.getElementById('alfredMainV1'); + const stage = document.getElementById('stage'); + const content = document.getElementById('content'); + if (!main || !stage || !content) return; + + let queued = false; + function isRich() { + // Les vues calendrier/météo, cartes, vidéo, images et modules spécialisés + // conservent la grande scène et tous leurs contrôles. + if (content.classList.contains('alfred-view')) return true; + if (content.querySelector('.alfred-view,.view-pane,.cards,.hero,iframe,video,canvas,img,table,.dona-dynamic-grid,.dona-dynamic-dashboard,[data-dona-rich]')) return true; + if (content.querySelector('.dh-response')) return false; + const nodes = [...content.children]; + return !nodes.length || nodes.some(n => !['H1','H2','H3','P','PRE','BLOCKQUOTE'].includes(n.tagName)); + } + function update() { + queued = false; + const focused = main.classList.contains('is-focused'); + const compact = focused && !isRich(); + const expanded = compact && content.querySelector('.dh-more[aria-expanded="true"]') !== null; + stage.classList.toggle('dona-reply-compact', compact); + stage.classList.toggle('dona-reply-expanded', expanded); + main.classList.toggle('dona-reply-background-visible', compact && !expanded); + stage.setAttribute('data-dona-reply-mode', compact ? (expanded ? 'expanded':'compact') : 'full'); + } + function queue() { + if (queued) return; + queued = true; + requestAnimationFrame(update); + } + new MutationObserver(queue).observe(content, {childList:true,subtree:true,attributes:true,attributeFilter:['aria-expanded','class']}); + new MutationObserver(queue).observe(main, {attributes:true,attributeFilter:['class']}); + // Le renderer transforme parfois la réponse brute en carte au frame suivant. + window.alfredSocket?.on('alfred_render', queue); + stage.addEventListener('click', e => { + if (e.target.closest('.dh-more')) setTimeout(queue, 0); + }); + queue(); + } + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', setup, {once:true}); + else setup(); +})(); diff --git a/alfred/app/static/js/dona-waves-v1.js b/alfred/app/static/js/dona-waves-v1.js new file mode 100644 index 0000000..19f3b6f --- /dev/null +++ b/alfred/app/static/js/dona-waves-v1.js @@ -0,0 +1,258 @@ +/* DONA_SCULPTURE_V11 */ +/* Dona Vagues Vocales V1 — Canvas 2D, amplitude PCM Piper envoyée par Socket.IO. + Le mode orb conserve l'ancien canvas et son animation. + Aucune capture micro et aucun audio client ne sont nécessaires. */ +(() => { + 'use strict'; + if (window.DonaWaves) return; + const KEY='dona_visual_voice_v1'; + let choice='waves'; + try {if (['waves','orb','sculpture'].includes(localStorage.getItem(KEY)))choice=localStorage.getItem(KEY);}catch (_) {} + const API={isActive:()=>choice!=='orb',select:selectMode,getChoice:()=>choice}; + window.DonaWaves=API; + document.body.classList.toggle('dona-waves-selected', choice!=='orb'); + document.body.classList.toggle('dona-sculpture-selected',choice==='sculpture'); + function selectMode(next){ + if(!['waves','orb','sculpture'].includes(next))return; + choice=next; + document.body.classList.toggle('dona-waves-selected',choice!=='orb'); + document.body.classList.toggle('dona-sculpture-selected',choice==='sculpture'); + try{localStorage.setItem(KEY,choice);}catch(_){} + syncMenu(); + } + function syncMenu(){ + const menu=document.getElementById('dv92ToolsMenu');if(!menu)return; + for(const el of menu.querySelectorAll('[data-dona-visual]')){ + const active=el.dataset.donaVisual===choice; + el.setAttribute('aria-checked',String(active)); + el.querySelector('.dona-waves-choice-icon').textContent=active?'◉':'○'; + } + } + function addMenuControls(){ + const menu=document.getElementById('dv92ToolsMenu'); + if(!menu||menu.querySelector('#donaWavesMenuTitle'))return; + const title=document.createElement('div');title.id='donaWavesMenuTitle'; + title.className='dona-waves-menu-title';title.textContent='Animation centrale'; + menu.append(title); + for(const [mode,label] of [['waves','Vagues horizontales (Piper)'],['sculpture','Sculpture de vagues (Piper)'],['orb','Orbe d’origine']]){ + const b=document.createElement('button');b.type='button'; + b.className='dv92-menu-entry dona-waves-option';b.dataset.donaVisual=mode; + b.setAttribute('role','menuitemradio'); + const mark=document.createElement('span');mark.className='dona-waves-choice-icon'; + b.append(mark,document.createTextNode(label)); + b.addEventListener('click',()=>{ + selectMode(mode); + menu.hidden=true; + document.getElementById('dv92Hamburger')?.setAttribute('aria-expanded','false'); + }); + menu.append(b); + } + syncMenu(); + } + let started=false; + function init(){ + if(started)return true; + const root=document.getElementById('alfredMainV1'); + const wrap=root?.querySelector('.dona-organic-wrap'); + if(!wrap)return false; + started=true; + const canvas=document.createElement('canvas');canvas.id='donaWavesCanvas'; + canvas.setAttribute('role','img');canvas.setAttribute('aria-label','Vagues animées de Dona réagissant à la voix Piper'); + wrap.append(canvas); + const ctx=canvas.getContext('2d',{alpha:true});if(!ctx)return true; + const reduce=window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches||false; + let w=0,h=0,dpr=1,mode=String(root.dataset.familyMode||'idle'); + let phase=0,smoothed=.02,pcm=0,lastPCM=0,voiceFinished=false,then=0,raf=0; + const socket=window.alfredSocket; + socket?.on('alfred_state',data=>{ + const next=String(data?.mode||'idle').toLowerCase(); + if(next==='speaking'&&mode!=='speaking')voiceFinished=false; + mode=next; + }); + socket?.on('dona_piper_level',data=>{ + const value=Number(data?.level); + if(!Number.isFinite(value))return; + pcm=Math.max(0,Math.min(1,value)); + lastPCM=performance.now(); + }); + socket?.on('alfred_tts_finished',()=>{pcm=0;voiceFinished=true;}); + const ro=typeof ResizeObserver!=='undefined'?new ResizeObserver(resize):null; + ro?.observe(wrap);window.addEventListener('resize',resize,{passive:true}); + function resize(){ + const box=wrap.getBoundingClientRect(); + if(!box.width||!box.height)return; + const ratio=Math.min(window.devicePixelRatio||1,1.7); + const nw=Math.round(box.width*ratio),nh=Math.round(box.height*ratio); + if(nw===canvas.width&&nh===canvas.height)return; + w=box.width;h=box.height;dpr=ratio; + canvas.width=nw;canvas.height=nh;ctx.setTransform(ratio,0,0,ratio,0,0); + } + // DONA_SCULPTURE_V11 : membranes de lumière, 2D pur, animées par PCM Piper. + function drawSculpture(now){ + const cx=w*.5, cy=h*.51; + const R=Math.min(w*.23,h*.30); + const strength=Math.min(1,smoothed); + const phi=phase*.74; + ctx.save(); + // Lueur ambiante derrière les rubans, sans rectangle de fond. + let halo=ctx.createRadialGradient(cx,cy,R*.12,cx,cy,R*2.25); + halo.addColorStop(0,'rgba(255,188,186,.13)'); + halo.addColorStop(.39,'rgba(120,82,249,.12)'); + halo.addColorStop(1,'rgba(12,43,138,0)'); + ctx.fillStyle=halo;ctx.fillRect(0,0,w,h); + const shell=(layer,behind)=>{ + const n=104, z=layer/8; + const width=Math.min(w*.40,h*1.16)*(1-z*.34); + const rise=h*(.225+.07*Math.sin(layer*.87+phi*.25))*(1-z*.24); + const yShift=h*(.015*Math.sin(layer*1.8+phi*.18)); + const steps=[]; + for(let i=0;i<=n;i++){ + const t=i/n, x=cx+(t-.5)*width*2; + const env=Math.pow(Math.max(0,Math.sin(Math.PI*t)),.65); + const y=cy+yShift+Math.sin(t*Math.PI*(2.7+layer*.16)+phi*.5+layer*.63)*rise*env + +Math.cos(t*Math.PI*4.8-phi*.38+layer)*rise*.29*env; + steps.push({x,y,t,env}); + } + const thickness=h*(.21+.045*Math.sin(layer*1.24+phi*.1)); + const g=ctx.createLinearGradient(cx-width,cy,cx+width,cy); + g.addColorStop(0,'rgba(26,119,250,.60)'); + g.addColorStop(.33,'rgba(104,98,253,.58)'); + g.addColorStop(.59,'rgba(249,167,209,.70)'); + g.addColorStop(1,'rgba(252,192,123,.62)'); + ctx.beginPath();ctx.moveTo(steps[0].x,steps[0].y); + for(let i=1;i=0;i--){ + const p=steps[i]; + ctx.lineTo(p.x,p.y+thickness*p.env*(.8+strength*.44)); + } + ctx.closePath();ctx.globalAlpha=(behind?.19:.30)+strength*.09; + ctx.fillStyle=g;ctx.fill();ctx.globalAlpha=.55+strength*.25; + ctx.beginPath();ctx.moveTo(steps[0].x,steps[0].y); + for(let i=1;i=4;layer--)shell(layer,true); + // Noyau irisé : diaphragme vivant, se dilate sur les syllabes. + const pulse=1+strength*.20+.035*Math.sin(phi*1.05); + const r=R*pulse; + const core=ctx.createRadialGradient(cx-r*.19,cy-r*.26,r*.1,cx,cy,r*1.15); + core.addColorStop(0,'rgba(255,214,214,.90)'); + core.addColorStop(.26,'rgba(244,147,194,.78)'); + core.addColorStop(.53,'rgba(117,113,244,.82)'); + core.addColorStop(.82,'rgba(41,125,253,.72)'); + core.addColorStop(1,'rgba(37,135,245,.025)'); + ctx.fillStyle=core;ctx.beginPath(); + for(let i=0;i<=180;i++){ + const a=i/180*Math.PI*2; + const rad=r*(1+.12*Math.sin(4*a+phi*.5)+.085*Math.cos(3*a-phi*.3)); + const x=cx+Math.cos(a)*rad,y=cy+Math.sin(a)*rad*.82; + if(!i)ctx.moveTo(x,y);else ctx.lineTo(x,y); + } + ctx.closePath();ctx.shadowColor='#b696ff';ctx.shadowBlur=32+strength*25;ctx.fill();ctx.shadowBlur=0; + for(let layer=3;layer>=0;layer--)shell(layer,false); + // Poussière irisée légère dans le volume. + for(let i=0;i<70;i++){ + const t=(i*.61803398875)%1; + const x=cx+(t-.5)*w*.70; + const env=Math.sin(Math.PI*t); + const y=cy+Math.cos(i*1.71+phi*.15)*h*.27*env; + ctx.globalAlpha=(.08+strength*.21)*env; + ctx.fillStyle=i%3?'#accaff':'#ffccbd'; + ctx.beginPath();ctx.arc(x,y,.65+(i%4)*.22,0,Math.PI*2);ctx.fill(); + } + ctx.restore(); + } + + function draw(now){ + raf=requestAnimationFrame(draw); + if(choice==='orb'||document.hidden||now-then<30)return; + then=now; + resize();if(!w||!h)return; + const speaking=mode==='speaking'&&!voiceFinished; + const current=(now-lastPCM<240&&speaking)?pcm:0; + const base=mode==='listening'?.16:mode==='thinking'?.115:speaking?.07:.025; + const target=Math.max(base,current*.88); + smoothed+=(target-smoothed)*(target>smoothed?.35:.19); + if(!reduce)phase+=mode==='listening'?.054:mode==='thinking'?.034:speaking?.047:.018; + ctx.clearRect(0,0,w,h); + if(choice==='sculpture'){drawSculpture(now);return;} + const mid=h*.51, amplitude=h*(.245+smoothed*.35); + const palette=[ + ['#1362ff','#11c8fc','#6b56e9'], + ['#1e92ef','#9274ff','#e47dbd'], + ['#6c4bd9','#ef86b5','#ffd29b'], + ['#4a5ad8','#ffb3c2','#ffc282'], + ['#16b8ed','#8c66f5','#f596b5'], + ['#7854e5','#d891ed','#fdad8b'] + ]; + // Contours en membranes translucides, superposés pour former une onde lumineuse. + for(let layer=0;layer=0;i--){const [x,y]=points[i];ctx.lineTo(x,y+thickness);} + ctx.closePath();ctx.fillStyle=gradient;ctx.globalAlpha=.095+(layer%3)*.058+smoothed*.1;ctx.fill(); + ctx.beginPath();points.forEach(([x,y],i)=>i?ctx.lineTo(x,y):ctx.moveTo(x,y)); + ctx.lineWidth=(layer%2?1.5:1.9)+smoothed*1.35; + ctx.strokeStyle=gradient;ctx.globalAlpha=.60+smoothed*.30; + ctx.shadowBlur=15+smoothed*25;ctx.shadowColor=center;ctx.stroke(); + ctx.shadowBlur=0;ctx.globalAlpha=1; + } + // Lueur centrale très diffuse, avec énergie liée à l'amplitude Piper. + const glow=ctx.createRadialGradient(w*.5,mid,0,w*.5,mid,w*.34); + glow.addColorStop(0,`rgba(255,194,205,${.06+smoothed*.18})`); + glow.addColorStop(.45,`rgba(107,99,255,${.025+smoothed*.045})`); + glow.addColorStop(1,'rgba(56,138,244,0)'); + ctx.fillStyle=glow;ctx.fillRect(0,0,w,h); + // Petits points lumineux déterministes, très discrets au repos. + for(let i=0;i<22;i++){ + const nx=(i*0.61803398875)%1,ny=.5+.33*Math.sin(i*2.18+phase*.14); + const x=nx*w,y=ny*h; + ctx.globalAlpha=(.11+smoothed*.37)*Math.pow(Math.sin(nx*Math.PI),2); + ctx.fillStyle=i%3===0?'#ffb5cc':'#83ceff';ctx.beginPath();ctx.arc(x,y,.8+(i%3)*.35,0,Math.PI*2);ctx.fill(); + } + ctx.globalAlpha=1; + } + resize();raf=requestAnimationFrame(draw); + window.addEventListener('pagehide',()=>{cancelAnimationFrame(raf);ro?.disconnect();},{once:true}); + return true; + } + let attempts=0; + function ready(){ + if(!init()&&attempts++<50)setTimeout(ready,120); + addMenuControls(); + const menu=document.getElementById('dv92ToolsMenu'); + if(menu&&!menu.dataset.donaWavesObserved){ + menu.dataset.donaWavesObserved='1'; + new MutationObserver(()=>addMenuControls()).observe(menu,{childList:true}); + } + } + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',ready,{once:true});else ready(); +})(); diff --git a/alfred/app/templates/index.html b/alfred/app/templates/index.html index 112b220..b57194a 100644 --- a/alfred/app/templates/index.html +++ b/alfred/app/templates/index.html @@ -51,6 +51,10 @@ + + + +

@@ -254,5 +258,9 @@ + + + + diff --git a/alfred/backups/dona_avatar_2d_20261031_110849_457833/manifest.json b/alfred/backups/dona_avatar_2d_20261031_110849_457833/manifest.json new file mode 100644 index 0000000..4f29137 --- /dev/null +++ b/alfred/backups/dona_avatar_2d_20261031_110849_457833/manifest.json @@ -0,0 +1,20 @@ +{ + "kind": "dona-avatar-v1", + "app": "/opt/alfred/app", + "original_files": [ + "templates/index.html", + "static/js/dona-accueil-ecrit-v1.js" + ], + "created_files": [ + "static/css/dona-avatar-v1.css", + "static/js/dona-avatar-v1.js", + "static/img/dona-avatar-portrait.png" + ], + "installed_sha256": { + "templates/index.html": "eb039ad650db5f3a735baf702749a555c539c577020c77d9d3de89168dc3ade2", + "static/js/dona-accueil-ecrit-v1.js": "5f85b649d20270a1be55ca4dbbab30918e1d0ea792052d880613a5a76db7f834", + "static/css/dona-avatar-v1.css": "e79daa023147bcca69a3bba2f06927b79e3d1426fc24c57a9daff7314dc95d9c", + "static/js/dona-avatar-v1.js": "2399c2679693abddbc8a483bb5e6b14bc5119e0ef38222ea7d684b8fcbc31d57", + "static/img/dona-avatar-portrait.png": "ffaba09cd4528f48d93ab1aadf5e662d44872563ccf657961d08f681afee80b8" + } +} \ No newline at end of file diff --git a/alfred/backups/dona_avatar_2d_20261031_110849_457833/original/static/js/dona-accueil-ecrit-v1.js b/alfred/backups/dona_avatar_2d_20261031_110849_457833/original/static/js/dona-accueil-ecrit-v1.js new file mode 100644 index 0000000..ab076b1 --- /dev/null +++ b/alfred/backups/dona_avatar_2d_20261031_110849_457833/original/static/js/dona-accueil-ecrit-v1.js @@ -0,0 +1,104 @@ +/* DONA_FAMILY_UI_V1: non-destructive augmentation of the installed Glass V1 dashboard. */ +(() => { + 'use strict'; + let attempts=0; + function init(){ + const root=document.getElementById('alfredMainV1'); + if(!root){if(++attempts<40)setTimeout(init,150);return;} + if(root.dataset.donaFamily==='ready')return; + const $=id=>document.getElementById(id); + const head=root.querySelector('.av-header-right'); + const weather=root.querySelector('.av-weather'); + const hero=root.querySelector('.av-voice-hero'); + const shortcuts=root.querySelector('.av-shortcuts'); + if(!head||!weather||!hero||!shortcuts)return; + root.dataset.donaFamily='ready';document.body.classList.add('dona-family-v1'); + + // Move the EXISTING weather widget: old JS keeps updating the SAME IDs. + weather.classList.add('dona-inline-weather'); + weather.setAttribute('title','Ouvrir les détails météo'); + head.prepend(weather); + + // Child link is supplementary; all old admin/navigation functions remain. + const kid=document.createElement('a');kid.href='/enfant';kid.className='dona-child-link';kid.textContent='⭐ Espace enfant'; + kid.setAttribute('aria-label','Ouvrir le chat enfant, écrit uniquement'); + head.insertBefore(kid,head.querySelector('.av-head-actions')); + const toolsGrid=root.querySelector('#avMoreDialog .av-more-grid'); + if(toolsGrid&&!toolsGrid.querySelector('a[href="/enfant"]')){ + const a=document.createElement('a');a.href='/enfant';a.textContent='⭐ Espace enfant';toolsGrid.append(a); + } + + // New expressive centre; old controls stay in DOM so audio event listeners work. + const display=document.createElement('div');display.className='dona-art-center'; + display.innerHTML='
Prêt à vous aider'; + hero.prepend(display); + const oldVoice=root.querySelector('.av-voice-center'); + if(oldVoice){display.append(oldVoice);} + + // Move shortcuts into a collapsed tray, instead of removing their controls. + const tools=document.createElement('details');tools.className='dona-tools-tray'; + const summary=document.createElement('summary');summary.textContent='☷ Raccourcis'; + shortcuts.parentNode.insertBefore(tools,shortcuts);tools.append(summary,shortcuts); + + // Minimal permanent one-row text field: no expandable tray, send at right. + const compose=document.createElement('div');compose.className='dona-compose dona-compose-compact'; + compose.innerHTML=`
+ +
+ +
`; + root.append(compose); + const body=$('donaComposeBody'),input=$('donaComposeInput'),send=$('donaComposeSend'),status=$('donaComposeStatus'); + input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();body.requestSubmit();}}); + body.addEventListener('submit',async e=>{ + e.preventDefault();let message=input.value.trim();if(!message)return; + send.disabled=true;status.textContent='Envoi à Hermes…'; + try{ + const r=await fetch('/api/dona/texte',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({message})}); + let d={};try{d=await r.json();}catch(_){} + if(!r.ok)throw Error(d.error||'Erreur '+r.status); + input.value='';status.textContent='Hermes prépare sa réponse…'; + }catch(err){status.textContent=err.message;} + finally{send.disabled=false;} + }); + const modeText={idle:'Prêt à vous aider',waiting:'Prêt à vous aider',listening:'Je vous écoute…',thinking:'Je réfléchis…',speaking:'Je vous réponds…',error:'Un problème est survenu'}; + function setMode(mode){root.dataset.familyMode=mode;const t=$('donaArtSubtitle');if(t)t.textContent=modeText[mode]||'Prêt à vous aider';if(mode==='idle'&&status.textContent==='Hermes prépare sa réponse…')status.textContent='Réponse reçue';} + function setName(value){const name=String(value||$('avName')?.textContent||'Dona').trim();$('donaArtName').textContent=name;} + setName();setMode(root.dataset.mode||'idle'); + const socket=window.alfredSocket; + socket?.on('alfred_state',s=>{setMode(String(s.mode||'idle').toLowerCase());setName(s.assistant_name||s.title||$('avName')?.textContent);}); + socket?.on('alfred_render',()=>{if(status.textContent==='Hermes prépare sa réponse…')status.textContent='Réponse reçue';}); + fetch('/api/state',{cache:'no-store'}).then(r=>r.ok?r.json():null).then(s=>{if(s){setMode(s.mode||'idle');setName(s.assistant_name||s.title);}}).catch(()=>{}); + + // Animated multi-layer liquid orb using canvas, independent of server state. + const canvas=$('donaOrganicCanvas'),ctx=canvas.getContext('2d'); + if(!ctx)return; + let w=320,h=320,phase=0,raf=0,last=0; + const reduce=matchMedia('(prefers-reduced-motion: reduce)').matches; + function resize(){const rect=canvas.getBoundingClientRect();const dpr=Math.min(devicePixelRatio||1,2);w=Math.max(150,rect.width);h=Math.max(150,rect.height);canvas.width=Math.floor(w*dpr);canvas.height=Math.floor(h*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);} + window.addEventListener('resize',resize,{passive:true});resize(); + function path(radius,layer){const n=130;ctx.beginPath();for(let k=0;k<=n;k++){ + const a=k/n*Math.PI*2;const modulation=1+0.067*Math.sin(a*3+phase*.72+layer*.64)+0.044*Math.cos(a*5-phase*.39+layer)+0.021*Math.sin(a*8+phase*.55); + const r=radius*modulation;const x=Math.cos(a)*r,y=Math.sin(a)*r; + if(k===0)ctx.moveTo(x,y);else ctx.lineTo(x,y); + }ctx.closePath();} + function frame(now){raf=requestAnimationFrame(frame);if(document.hidden||now-last<30)return;last=now; + const mode=root.dataset.familyMode||'idle';const speed=mode==='listening'?1.7:mode==='thinking'?1.38:mode==='speaking'?1.5:.75; + if(!reduce)phase+=.024*speed; + ctx.clearRect(0,0,w,h);ctx.save();ctx.translate(w/2,h/2);const r=Math.min(w,h)*.33; + const bloom=ctx.createRadialGradient(-r*.2,-r*.15,r*.1,0,0,r*1.45); + bloom.addColorStop(0,'rgba(238,117,160,.11)');bloom.addColorStop(.7,'rgba(139,112,246,.14)');bloom.addColorStop(1,'rgba(37,180,255,0)');ctx.fillStyle=bloom;ctx.beginPath();ctx.arc(0,0,r*1.48,0,Math.PI*2);ctx.fill(); + for(let layer=3;layer>=0;layer--){const waveR=r*(1+layer*.025);path(waveR,layer); + const g=ctx.createLinearGradient(-r*.9,r*.8,r*.9,-r*.85); + if(layer%2===0){g.addColorStop(0,'rgba(47,196,244,.76)');g.addColorStop(.36,'rgba(68,94,198,.88)');g.addColorStop(.62,'rgba(244,138,173,.74)');g.addColorStop(1,'rgba(255,176,71,.9)');} + else {g.addColorStop(0,'rgba(59,164,255,.65)');g.addColorStop(.4,'rgba(196,119,245,.48)');g.addColorStop(.7,'rgba(241,125,146,.52)');g.addColorStop(1,'rgba(255,185,95,.76)');} + ctx.globalAlpha=layer===0?.76:.25;ctx.fillStyle=g;ctx.fill(); + ctx.globalAlpha=layer===0?.92:.31;ctx.strokeStyle=g;ctx.lineWidth=layer===0?2.5:1.8;ctx.shadowBlur=layer===0?23:13;ctx.shadowColor=layer%2?'#bd80ec':'#ffc685';ctx.stroke();ctx.shadowBlur=0; + } + const ribbon=ctx.createLinearGradient(-r,r*.4,r,-r*.3);ribbon.addColorStop(0,'rgba(67,219,255,.0)');ribbon.addColorStop(.48,'rgba(255,231,197,.56)');ribbon.addColorStop(1,'rgba(255,181,103,.01)'); + ctx.beginPath();for(let i=0;i<=100;i++){const x=-r+i/100*r*2;const y=Math.sin(i/100*Math.PI*2+phase*.35)*r*.25+Math.cos(i/100*Math.PI*3-phase*.45)*r*.11;if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);}ctx.lineWidth=r*.26;ctx.strokeStyle=ribbon;ctx.globalAlpha=.38;ctx.stroke();ctx.restore(); + } + raf=requestAnimationFrame(frame);window.addEventListener('pagehide',()=>cancelAnimationFrame(raf),{once:true}); + } + init(); +})(); diff --git a/alfred/backups/dona_avatar_2d_20261031_110849_457833/original/templates/index.html b/alfred/backups/dona_avatar_2d_20261031_110849_457833/original/templates/index.html new file mode 100644 index 0000000..112b220 --- /dev/null +++ b/alfred/backups/dona_avatar_2d_20261031_110849_457833/original/templates/index.html @@ -0,0 +1,258 @@ + + + + + + + {{ name }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + En attente +
+ +
+ + ⚙ + ▤ + + + + + + + + + + + +
+
+ + + + + + +
+

Bonjour.

À votre écoute.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/manifest.json b/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/manifest.json new file mode 100644 index 0000000..3ef807e --- /dev/null +++ b/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/manifest.json @@ -0,0 +1,23 @@ +{ + "kind": "dona-clock-sculpture-v1.1", + "app": "/opt/alfred/app", + "original": [ + "app.py", + "static/js/dona-waves-v1.js", + "static/css/dona-waves-v1.css" + ], + "created": [ + "services/dona_clock_local.py" + ], + "installed_sha256": { + "app.py": "246913f10e751bbd35f71dbcfa553f8284cdd1c050d3eadf1d2f276ce33ef184", + "static/js/dona-waves-v1.js": "3a87d35280436920024f550feb9040450ff057e9c3e6b75e5d7f75f4cf194ffc", + "static/css/dona-waves-v1.css": "47df09de7549762feda8d95cfc3bd9763bd7f01c3bfb10a07844f000867b2a53", + "services/dona_clock_local.py": "705a67d28387a57bff92ab44bf0ac9195422bb47c1d311cecf13bad6050a0ee8" + }, + "original_sha256": { + "app.py": "472959331cfece1d75100f32d77c57b4b1838fafff45f0d49e13fd17837d942a", + "static/js/dona-waves-v1.js": "8c33a71da919da54533ecdffc1a4368c9ea16daff893a082a14f05170ac9b3c5", + "static/css/dona-waves-v1.css": "9c9586493b6b6ec506928f6ebac79f28053b9df36b680ce1c0835aa75bf3f1cb" + } +} \ No newline at end of file diff --git a/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/app.py b/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/app.py new file mode 100644 index 0000000..1b78773 --- /dev/null +++ b/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/app.py @@ -0,0 +1,887 @@ + +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 + +# Charger la configuration AVANT les imports des services +load_dotenv("/opt/alfred/.env") +load_dotenv( + "/opt/alfred/app/config/control_center.env", + override=True +) + +from services.audio import listen_once, speak_async, tts_backend_detect +from services.wakeword import WakeWordListener + + + + + + + +from services.alfred_duplex import DuplexMonitor # ALFRED_DUPLEX_BARGE_IN_V1 +from services.piper_tts import stop_playback as duplex_stop_piper +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="*") +# DONA_WAVES_V1 : événement léger, pas de flux audio transmis au navigateur. +from services import piper_tts as _dona_waves_piper +_dona_waves_piper.dona_set_level_callback( + lambda level: socketio.emit('dona_piper_level', {'level': round(level, 4)}) +) +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}) +# ALFRED_DUPLEX_BARGE_IN_V1: interruption audio sans interruption des tâches Hermes +duplex_monitor = DuplexMonitor(muted=audio_settings.mic_muted, + on_interrupt=duplex_stop_piper, + on_status=lambda event: socketio.emit("alfred_duplex_interrupted" if event.get("interrupted") else "alfred_duplex_activity", event)) + +# 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, injected_text=None): # ALFRED_DUPLEX_CONVERSATION_V2 + if not conversation_lock.acquire(blocking=False): + return + pending_duplex_text = None # ALFRED_DUPLEX_CONVERSATION_V2 + + try: + emit_state("listening", partial="", transcript="", wake_score=score) + + print("[ALFRED] Début écoute STT", flush=True) + + if injected_text is not None: # ALFRED_DUPLEX_CONVERSATION_V2 + from services.audio import STTResult + r = STTResult(text=injected_text) + else: + 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 + + # DONA_LEARNING_V93 : évaluation vocale explicite, puis correction Vosk approuvée. + _dona_v93_original_vosk = r.text + _dona_v93_ack = _dona_learning_v93.voice_feedback(r.text) + if _dona_v93_ack is not None: + socketio.emit('alfred_render', {'type':'text','text':_dona_v93_ack}) + if not audio_settings.speaker_muted(): + _dona_v93_done = threading.Event() + if speak_async(_dona_v93_ack,on_end=_dona_v93_done.set): + _dona_v93_done.wait(timeout=120) + emit_state('idle',partial='') + return + r.text = _dona_learning_v93.correct_transcript(r.text) + if r.text != _dona_v93_original_vosk: + socketio.emit('dona_learning_stt', {'raw':_dona_v93_original_vosk,'used':r.text}) + + emit_state( + "thinking", + partial="", + transcript=r.text, + content={"type": "text", "text": r.text} + ) + + # DONA_VISION_V95 : photo locale et compréhension par Hermes sur demande. + if _dona_vision_v95.is_camera_question(r.text): + try: + _vision_answer = _dona_vision_monitor.analyze(r.text) + except RuntimeError as _vision_exc: + _vision_answer = str(_vision_exc) + except Exception as _vision_exc: + print('[DONA VISION] Analyse vocale :',type(_vision_exc).__name__,flush=True) + _vision_answer = 'Analyse visuelle indisponible. Vérifie le modèle vision Hermes.' + with STATE_LOCK: + STATE['content']={'type':'text','text':_vision_answer} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('voice',_dona_v93_original_vosk,_vision_answer)) + if _vision_answer and not audio_settings.speaker_muted(): + emit_state('speaking') + _vision_done=threading.Event() + if speak_async(_vision_answer[:850],on_end=_vision_done.set):_vision_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_FILES_V91 : questions explicites sur la pièce jointe active. + if _dona_files_v91.is_file_question(r.text): + _file_speech = _dona_files_v91.handle_voice(r.text, socketio, STATE, STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_file_speech)) + if _file_speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _file_done = threading.Event() + if speak_async(_file_speech, on_end=_file_done.set): _file_done.wait(timeout=120) + emit_state('idle', partial='') + return + + # DONA_SUITE_V9: intents locaux explicites, sinon continuer vers Hermes. + if _dona_suite_v9.is_request(r.text): + _v9_speech=_dona_suite_v9.handle_and_emit(r.text,socketio) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_v9_speech)) + if _v9_speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _v9_done=threading.Event() + _v9_ok=speak_async(_v9_speech,on_end=_v9_done.set) + if _v9_ok:_v9_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_NAV_V83: cartes et itinéraires à la demande, sans LLM. + if _dona_nav.is_request(r.text): + _nav_reply=_dona_nav.handle_and_emit(r.text,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_nav_reply)) + if _nav_reply and not audio_settings.speaker_muted(): + emit_state('speaking') + _nav_done=threading.Event() + _nav_ok=speak_async(_nav_reply,on_end=lambda: (_nav_done.set(),socketio.emit('alfred_tts_finished',{'source':'voice'}))) + if _nav_ok:_nav_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_YOUTUBE_V82 : recherche publique et contrôle du lecteur à la voix. + if _dona_youtube.is_search(r.text): + _speech=_dona_youtube.search_and_emit(r.text, socketio, STATE, STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_speech)) + if _speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _evt=threading.Event() + _ok=speak_async(_speech,on_end=_evt.set) + if _ok: _evt.wait(timeout=120) + emit_state('idle',partial='') + return + if _dona_youtube.is_control(r.text,_dona_youtube.player_active()): + _dona_youtube.control_and_emit(r.text,socketio) + emit_state('idle',partial='') + return + + # DONA_V8_DYNAMIC — creation visuelle explicite, source Hermes/MCP ou API directe. + if _dona_dynamic.is_dynamic_request(r.text): + _board = _dona_dynamic.build_dashboard(r.text, _dona_dashboard_agent.ask_dashboard) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('voice',_dona_v93_original_vosk,_board.get('title','Tableau de bord'))) + _payload = {'type':'dona_dynamic', 'dashboard':_board} + with STATE_LOCK: + STATE['content'] = _payload + socketio.emit('alfred_render', _payload) + if not audio_settings.speaker_muted(): + emit_state('speaking') + _finished = threading.Event() + _started = speak_async('Voici ton tableau de bord : ' + _board['title'], on_end=_finished.set) + if _started: _finished.wait(timeout=120) + emit_state('idle', partial='') + return + + # 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 + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,speech)) + 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=lambda: (done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + 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: + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_lists_reply)) + _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=lambda: (_lists_done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + _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 + # DONA_V7_RELEASE: reconnaissance sur ordre vocal explicite, sinon Hermes normal. + answer = _dona_face_recognize(ask_hermes) if _dona_face_request(r.text) else ask_hermes(_dona_learning_v93.enhance_prompt(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} + + # DONA_LEARNING_V93 : évaluation du tour Hermes, RAM seulement avant 👍 / 👎. + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('voice',_dona_v93_original_vosk,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() + duplex_monitor.start(speech) # ALFRED_DUPLEX_BARGE_IN_V1 + try: + started = speak_async(speech, on_end=lambda: (done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + if started: + done.wait(timeout=120) + finally: + if not duplex_monitor.interrupted(): + duplex_monitor.stop() + # ALFRED_DUPLEX_CONVERSATION_V2: stop-only or keep the rest of the spoken follow-up. + if duplex_monitor.interrupted(): + pending_duplex_text = duplex_monitor.wait_for_followup(timeout=30) + duplex_monitor.stop() + else: + # A few wake-free seconds after the assistant finishes speaking. + pending_duplex_text = duplex_monitor.listen_conversation() + if pending_duplex_text: + print('[ALFRED DUPLEX V2] Question enchaînée :', repr(pending_duplex_text), flush=True) + + 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 pending_duplex_text and not audio_settings.mic_muted(): # ALFRED_DUPLEX_CONVERSATION_V2 + # Only the voice is cancelled; the original Hermes job is untouched. + wake.pause() + threading.Thread(target=conversation, kwargs={'injected_text': pending_duplex_text}, + daemon=True, name='dona-duplex-followup').start() + elif 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/duplex/status') +def duplex_status(): + return jsonify(duplex_monitor.status()) + +@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=lambda: (_done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + _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 + + +# ALFRED_IDENTITY_GLASS_V1: page /identity et synchronisation du nom. +from services.alfred_identity_page import register_identity_page as _alfred_identity_register +_alfred_identity_register(app) + + +# ALFRED_DEVELOPER_V1 : interface /maintenance (administrateur). +from services.alfred_developer_page import register_developer_page as _alfred_developer_register +_alfred_developer_register(app) + + +# ALFRED_HERMES_NEW_SESSION_V1 : session Hermes neuve via /config admin. +from services.alfred_hermes_session_page import register_hermes_session_page as _register_hermes_new_session +_register_hermes_new_session(app) + + +# DONA_FAMILY_UI_V1_START +# Text entered in the adult UI uses the existing Hermes session, not microphone/TTS. +def _dona_submit_typed(prompt): + if not conversation_lock.acquire(blocking=False): + return False + def _dona_run_typed(): + try: + wake.pause() + with STATE_LOCK: + _before_image = IMAGE_REVISION + emit_state('thinking', partial='', transcript=prompt, + content={'type': 'text', 'text': prompt}) + # DONA_VISION_V95 : demande écrite de vision depuis la caméra réelle. + if _dona_vision_v95.is_camera_question(prompt): + try: + _vision_answer=_dona_vision_monitor.analyze(prompt) + except Exception as _vision_exc: + _vision_answer=str(_vision_exc)[:190] if isinstance(_vision_exc,RuntimeError) else 'Analyse visuelle indisponible.' + with STATE_LOCK: + STATE['content']={'type':'text','text':_vision_answer} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_vision_answer)) + emit_state('idle',partial='') + return + + # DONA_FILES_V91 : question écrite à propos de la dernière pièce jointe. + if _dona_files_v91.is_file_question(prompt): + _dona_files_v91.handle_voice(prompt, socketio, STATE, STATE_LOCK) + emit_state('idle',partial='') + return + + # DONA_SUITE_V9: commandes quotidiennes écrites, sans perturber Hermes. + if _dona_suite_v9.is_request(prompt): + _v93_local=_dona_suite_v9.handle_and_emit(prompt,socketio) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + + # DONA_NAV_V83: commande écrite navigation, sans polluer la session Hermes. + if _dona_nav.is_request(prompt): + _v93_local=_dona_nav.handle_and_emit(prompt,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + + # DONA_YOUTUBE_V82 : vidéos et commandes écrites hors conversation Hermes. + if _dona_youtube.is_search(prompt): + _v93_local=_dona_youtube.search_and_emit(prompt,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + if _dona_youtube.is_control(prompt,_dona_youtube.player_active()): + _dona_youtube.control_and_emit(prompt,socketio) + emit_state('idle',partial='') + return + + # DONA_V8_DYNAMIC — commandes visuelles saisies via le formulaire habituel. + if _dona_dynamic.is_dynamic_request(prompt): + _board = _dona_dynamic.build_dashboard(prompt, _dona_dashboard_agent.ask_dashboard) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_board.get('title','Tableau de bord'))) + _payload = {'type':'dona_dynamic','dashboard':_board} + with STATE_LOCK: + STATE['content'] = _payload + socketio.emit('alfred_render',_payload) + emit_state('idle', partial='') + return + _answer = ask_hermes(_dona_learning_v93.enhance_prompt(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} + with STATE_LOCK: + if IMAGE_REVISION == _before_image: + STATE['content'] = _display + socketio.emit('alfred_render', _display) + # DONA_LEARNING_V93 : évaluation de l’échange écrit. + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('typed',prompt,_speech)) + # Deliberately no speak_async(): typed interaction is silent. + emit_state('idle', partial='') + except Exception as exc: + print('[DONA TEXT] Echec de la demande:', type(exc).__name__, str(exc), flush=True) + import traceback + traceback.print_exc() + emit_state('error', content={'type':'text', 'text':'Hermes n’a pas pu répondre.'}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + threading.Thread(target=_dona_run_typed, daemon=True, name='dona-typed-hermes').start() + return True + +from services.dona_family_ui import register_dona_family as _dona_register_family +_dona_register_family(app, adult_command=_dona_submit_typed) +# DONA_FAMILY_UI_V1_END + +# DONA_V8_DYNAMIC — tableau de bord choisi, généré et rendu en composants contrôlés. +from services import dona_dynamic as _dona_dynamic +# DONA_V8_1_ISOLATION : tableaux isolés des conversations normales +from services import dona_dashboard_agent as _dona_dashboard_agent + +def _dona_dynamic_submit(prompt): + if not conversation_lock.acquire(blocking=False): + return False + def _work(): + try: + wake.pause() + emit_state('thinking',transcript=prompt,partial='') + _board=_dona_dynamic.build_dashboard(prompt,_dona_dashboard_agent.ask_dashboard) + _payload={'type':'dona_dynamic','dashboard':_board} + with STATE_LOCK: + STATE['content']=_payload + socketio.emit('alfred_render',_payload) + emit_state('idle',partial='') + except Exception as exc: + print('[DONA DYNAMIC V8] ',type(exc).__name__,str(exc),flush=True) + emit_state('error',content={'type':'text','text':'Impossible de préparer ce tableau de bord.'}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + threading.Thread(target=_work,daemon=True,name='dona-dynamic-v8').start() + return True + +_dona_dynamic.register_dynamic(app,_dona_dynamic_submit) + +# DONA_VISION_V95 : API vision/YOLOv8, surveillance opt-in. +from services import dona_vision_v95 as _dona_vision_v95 +_dona_vision_monitor = _dona_vision_v95.register(app, socketio) + +# DONA_LEARNING_V93 : retours explicites et corrections approuvées. +from services import dona_learning_v93 as _dona_learning_v93 +_dona_learning_v93.register(app) + +# DONA_UI_V92 : préférences de raccourcis persistantes, menus gérés côté navigateur. +from services import dona_shortcuts_v92 as _dona_shortcuts_v92 +_dona_shortcuts_v92.register(app) + +# DONA_FILES_V91 : fichiers temporaires et analyse Hermes isolée. +from services import dona_files_v91 as _dona_files_v91 +_dona_files_v91.register_files(app) + +# DONA_SUITE_V9 : routeur local, cockpit, mémoire, cuisine, documents, routines. +from services import dona_suite_v9 as _dona_suite_v9 +_dona_suite_v9.register_suite(app,socketio) + +# DONA_NAV_V83: géocodage, carte et transfert Waze. +from services import dona_navigation as _dona_nav +_dona_nav.register_navigation(app,socketio) + +# DONA_YOUTUBE_V82 : lecture publique, sans clé API, moteur Hermes isolé. +from services import dona_youtube as _dona_youtube +_dona_youtube.register_youtube(app,socketio) + +# DONA_SPOTIFY_V6 — player read/control API, reuses Hermes OAuth (no LLM polling). +from services.dona_spotify import register_spotify_panel as _register_dona_spotify +_register_dona_spotify(app) + +# DONA_V7_RELEASE : route manuelle caméra + MCP existant; aucune base de visage modifiée. +from services.dona_face import (register_face as _dona_register_face, + recognize_once as _dona_face_recognize, is_face_request as _dona_face_request) +_dona_register_face(app, socketio, ask_hermes, conversation_lock) + + +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/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/static/css/dona-waves-v1.css b/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/static/css/dona-waves-v1.css new file mode 100644 index 0000000..3fa89b4 --- /dev/null +++ b/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/static/css/dona-waves-v1.css @@ -0,0 +1,22 @@ +/* Dona — vagues vocales, sélection réversible sans désinstaller. */ +body.dona-family-v1.dona-waves-selected .dona-organic-wrap { + width:min(93vw,1400px)!important; + height:clamp(155px,28vh,330px)!important; + min-width:0!important;min-height:155px!important; + max-width:100%!important;filter:none!important; + overflow:visible!important; +} +body.dona-family-v1.dona-waves-selected #donaOrganicCanvas, +body.dona-family-v1.dona-waves-selected .dona-orb-reflection {display:none!important} +body.dona-family-v1 .dona-organic-wrap #donaWavesCanvas { + display:none;position:absolute;inset:0;width:100%;height:100%;pointer-events:none; +} +body.dona-family-v1.dona-waves-selected .dona-organic-wrap #donaWavesCanvas {display:block} +body.dona-family-v1.dona-waves-selected .dona-art-name {margin-top:-5px!important} +.dona-waves-menu-title{color:#c7b8cc;font-size:11px;letter-spacing:.07em;padding:10px 10px 5px;text-transform:uppercase} +.dv92-menu-entry.dona-waves-option[aria-checked="true"] {background:rgba(94,148,208,.19);color:#ffe1b8} +.dona-waves-option .dona-waves-choice-icon{width:22px;min-width:22px;text-align:center;font-size:18px} +@media(max-width:650px){ + body.dona-family-v1.dona-waves-selected .dona-organic-wrap{height:clamp(145px,23vh,240px)!important;min-height:145px!important;width:min(96vw,740px)!important} + body.dona-family-v1.dona-waves-selected .dona-art-name{font-size:clamp(32px,8vw,50px)} +} diff --git a/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/static/js/dona-waves-v1.js b/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/static/js/dona-waves-v1.js new file mode 100644 index 0000000..bee400e --- /dev/null +++ b/alfred/backups/dona_clock_sculpture_v1_1_20261031_121433_905681/original/static/js/dona-waves-v1.js @@ -0,0 +1,165 @@ +/* Dona Vagues Vocales V1 — Canvas 2D, amplitude PCM Piper envoyée par Socket.IO. + Le mode orb conserve l'ancien canvas et son animation. + Aucune capture micro et aucun audio client ne sont nécessaires. */ +(() => { + 'use strict'; + if (window.DonaWaves) return; + const KEY='dona_visual_voice_v1'; + let choice='waves'; + try {if (localStorage.getItem(KEY)==='orb')choice='orb';}catch (_) {} + const API={isActive:()=>choice==='waves',select:selectMode,getChoice:()=>choice}; + window.DonaWaves=API; + document.body.classList.toggle('dona-waves-selected', choice==='waves'); + function selectMode(next){ + if(next!=='waves'&&next!=='orb')return; + choice=next; + document.body.classList.toggle('dona-waves-selected',choice==='waves'); + try{localStorage.setItem(KEY,choice);}catch(_){} + syncMenu(); + } + function syncMenu(){ + const menu=document.getElementById('dv92ToolsMenu');if(!menu)return; + for(const el of menu.querySelectorAll('[data-dona-visual]')){ + const active=el.dataset.donaVisual===choice; + el.setAttribute('aria-checked',String(active)); + el.querySelector('.dona-waves-choice-icon').textContent=active?'◉':'○'; + } + } + function addMenuControls(){ + const menu=document.getElementById('dv92ToolsMenu'); + if(!menu||menu.querySelector('#donaWavesMenuTitle'))return; + const title=document.createElement('div');title.id='donaWavesMenuTitle'; + title.className='dona-waves-menu-title';title.textContent='Animation centrale'; + menu.append(title); + for(const [mode,label] of [['waves','Vagues colorées (Piper)'],['orb','Orbe d’origine']]){ + const b=document.createElement('button');b.type='button'; + b.className='dv92-menu-entry dona-waves-option';b.dataset.donaVisual=mode; + b.setAttribute('role','menuitemradio'); + const mark=document.createElement('span');mark.className='dona-waves-choice-icon'; + b.append(mark,document.createTextNode(label)); + b.addEventListener('click',()=>{ + selectMode(mode); + menu.hidden=true; + document.getElementById('dv92Hamburger')?.setAttribute('aria-expanded','false'); + }); + menu.append(b); + } + syncMenu(); + } + let started=false; + function init(){ + if(started)return true; + const root=document.getElementById('alfredMainV1'); + const wrap=root?.querySelector('.dona-organic-wrap'); + if(!wrap)return false; + started=true; + const canvas=document.createElement('canvas');canvas.id='donaWavesCanvas'; + canvas.setAttribute('role','img');canvas.setAttribute('aria-label','Vagues animées de Dona réagissant à la voix Piper'); + wrap.append(canvas); + const ctx=canvas.getContext('2d',{alpha:true});if(!ctx)return true; + const reduce=window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches||false; + let w=0,h=0,dpr=1,mode=String(root.dataset.familyMode||'idle'); + let phase=0,smoothed=.02,pcm=0,lastPCM=0,voiceFinished=false,then=0,raf=0; + const socket=window.alfredSocket; + socket?.on('alfred_state',data=>{ + const next=String(data?.mode||'idle').toLowerCase(); + if(next==='speaking'&&mode!=='speaking')voiceFinished=false; + mode=next; + }); + socket?.on('dona_piper_level',data=>{ + const value=Number(data?.level); + if(!Number.isFinite(value))return; + pcm=Math.max(0,Math.min(1,value)); + lastPCM=performance.now(); + }); + socket?.on('alfred_tts_finished',()=>{pcm=0;voiceFinished=true;}); + const ro=typeof ResizeObserver!=='undefined'?new ResizeObserver(resize):null; + ro?.observe(wrap);window.addEventListener('resize',resize,{passive:true}); + function resize(){ + const box=wrap.getBoundingClientRect(); + if(!box.width||!box.height)return; + const ratio=Math.min(window.devicePixelRatio||1,1.7); + const nw=Math.round(box.width*ratio),nh=Math.round(box.height*ratio); + if(nw===canvas.width&&nh===canvas.height)return; + w=box.width;h=box.height;dpr=ratio; + canvas.width=nw;canvas.height=nh;ctx.setTransform(ratio,0,0,ratio,0,0); + } + function draw(now){ + raf=requestAnimationFrame(draw); + if(choice!=='waves'||document.hidden||now-then<30)return; + then=now; + resize();if(!w||!h)return; + const speaking=mode==='speaking'&&!voiceFinished; + const current=(now-lastPCM<240&&speaking)?pcm:0; + const base=mode==='listening'?.16:mode==='thinking'?.115:speaking?.07:.025; + const target=Math.max(base,current*.88); + smoothed+=(target-smoothed)*(target>smoothed?.35:.19); + if(!reduce)phase+=mode==='listening'?.054:mode==='thinking'?.034:speaking?.047:.018; + ctx.clearRect(0,0,w,h); + const mid=h*.51, amplitude=h*(.245+smoothed*.35); + const palette=[ + ['#1362ff','#11c8fc','#6b56e9'], + ['#1e92ef','#9274ff','#e47dbd'], + ['#6c4bd9','#ef86b5','#ffd29b'], + ['#4a5ad8','#ffb3c2','#ffc282'], + ['#16b8ed','#8c66f5','#f596b5'], + ['#7854e5','#d891ed','#fdad8b'] + ]; + // Contours en membranes translucides, superposés pour former une onde lumineuse. + for(let layer=0;layer=0;i--){const [x,y]=points[i];ctx.lineTo(x,y+thickness);} + ctx.closePath();ctx.fillStyle=gradient;ctx.globalAlpha=.095+(layer%3)*.058+smoothed*.1;ctx.fill(); + ctx.beginPath();points.forEach(([x,y],i)=>i?ctx.lineTo(x,y):ctx.moveTo(x,y)); + ctx.lineWidth=(layer%2?1.5:1.9)+smoothed*1.35; + ctx.strokeStyle=gradient;ctx.globalAlpha=.60+smoothed*.30; + ctx.shadowBlur=15+smoothed*25;ctx.shadowColor=center;ctx.stroke(); + ctx.shadowBlur=0;ctx.globalAlpha=1; + } + // Lueur centrale très diffuse, avec énergie liée à l'amplitude Piper. + const glow=ctx.createRadialGradient(w*.5,mid,0,w*.5,mid,w*.34); + glow.addColorStop(0,`rgba(255,194,205,${.06+smoothed*.18})`); + glow.addColorStop(.45,`rgba(107,99,255,${.025+smoothed*.045})`); + glow.addColorStop(1,'rgba(56,138,244,0)'); + ctx.fillStyle=glow;ctx.fillRect(0,0,w,h); + // Petits points lumineux déterministes, très discrets au repos. + for(let i=0;i<22;i++){ + const nx=(i*0.61803398875)%1,ny=.5+.33*Math.sin(i*2.18+phase*.14); + const x=nx*w,y=ny*h; + ctx.globalAlpha=(.11+smoothed*.37)*Math.pow(Math.sin(nx*Math.PI),2); + ctx.fillStyle=i%3===0?'#ffb5cc':'#83ceff';ctx.beginPath();ctx.arc(x,y,.8+(i%3)*.35,0,Math.PI*2);ctx.fill(); + } + ctx.globalAlpha=1; + } + resize();raf=requestAnimationFrame(draw); + window.addEventListener('pagehide',()=>{cancelAnimationFrame(raf);ro?.disconnect();},{once:true}); + return true; + } + let attempts=0; + function ready(){ + if(!init()&&attempts++<50)setTimeout(ready,120); + addMenuControls(); + const menu=document.getElementById('dv92ToolsMenu'); + if(menu&&!menu.dataset.donaWavesObserved){ + menu.dataset.donaWavesObserved='1'; + new MutationObserver(()=>addMenuControls()).observe(menu,{childList:true}); + } + } + if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',ready,{once:true});else ready(); +})(); diff --git a/alfred/backups/dona_json_autorender_v1_20261101_141838_580140/app.py b/alfred/backups/dona_json_autorender_v1_20261101_141838_580140/app.py new file mode 100644 index 0000000..46561ac --- /dev/null +++ b/alfred/backups/dona_json_autorender_v1_20261101_141838_580140/app.py @@ -0,0 +1,917 @@ + +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 + +# Charger la configuration AVANT les imports des services +load_dotenv("/opt/alfred/.env") +load_dotenv( + "/opt/alfred/app/config/control_center.env", + override=True +) + +from services.audio import listen_once, speak_async, tts_backend_detect +from services.wakeword import WakeWordListener + + + + + + + +from services.alfred_duplex import DuplexMonitor # ALFRED_DUPLEX_BARGE_IN_V1 +from services.piper_tts import stop_playback as duplex_stop_piper +from services import admin_console # ALFRED_ADMIN_PAGES_V1 +from services.hermes_client import ask_hermes +from services import dona_clock_local as _dona_clock_local # DONA_CLOCK_SCULPTURE_V11 +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="*") +# DONA_WAVES_V1 : événement léger, pas de flux audio transmis au navigateur. +from services import piper_tts as _dona_waves_piper +_dona_waves_piper.dona_set_level_callback( + lambda level: socketio.emit('dona_piper_level', {'level': round(level, 4)}) +) +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}) +# ALFRED_DUPLEX_BARGE_IN_V1: interruption audio sans interruption des tâches Hermes +duplex_monitor = DuplexMonitor(muted=audio_settings.mic_muted, + on_interrupt=duplex_stop_piper, + on_status=lambda event: socketio.emit("alfred_duplex_interrupted" if event.get("interrupted") else "alfred_duplex_activity", event)) + +# 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, injected_text=None): # ALFRED_DUPLEX_CONVERSATION_V2 + if not conversation_lock.acquire(blocking=False): + return + pending_duplex_text = None # ALFRED_DUPLEX_CONVERSATION_V2 + + try: + emit_state("listening", partial="", transcript="", wake_score=score) + + print("[ALFRED] Début écoute STT", flush=True) + + if injected_text is not None: # ALFRED_DUPLEX_CONVERSATION_V2 + from services.audio import STTResult + r = STTResult(text=injected_text) + else: + 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 + + # DONA_LEARNING_V93 : évaluation vocale explicite, puis correction Vosk approuvée. + _dona_v93_original_vosk = r.text + _dona_v93_ack = _dona_learning_v93.voice_feedback(r.text) + if _dona_v93_ack is not None: + socketio.emit('alfred_render', {'type':'text','text':_dona_v93_ack}) + if not audio_settings.speaker_muted(): + _dona_v93_done = threading.Event() + if speak_async(_dona_v93_ack,on_end=_dona_v93_done.set): + _dona_v93_done.wait(timeout=120) + emit_state('idle',partial='') + return + r.text = _dona_learning_v93.correct_transcript(r.text) + if r.text != _dona_v93_original_vosk: + socketio.emit('dona_learning_stt', {'raw':_dona_v93_original_vosk,'used':r.text}) + + emit_state( + "thinking", + partial="", + transcript=r.text, + content={"type": "text", "text": r.text} + ) + + # DONA_CLOCK_SCULPTURE_V11 : heure/date locales AVANT Hermes. + _dona_local_clock_reply = _dona_clock_local.answer(r.text) + if _dona_local_clock_reply is not None: + with STATE_LOCK: + STATE['content']={'type':'text','text':_dona_local_clock_reply} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('voice',_dona_v93_original_vosk,_dona_local_clock_reply)) + if not audio_settings.speaker_muted(): + emit_state('speaking') + _dona_clock_done=threading.Event() + _dona_clock_started=speak_async(_dona_local_clock_reply, + on_end=lambda: (_dona_clock_done.set(), + socketio.emit('alfred_tts_finished', {'source':'voice'}))) + if _dona_clock_started:_dona_clock_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_VISION_V95 : photo locale et compréhension par Hermes sur demande. + if _dona_vision_v95.is_camera_question(r.text): + try: + _vision_answer = _dona_vision_monitor.analyze(r.text) + except RuntimeError as _vision_exc: + _vision_answer = str(_vision_exc) + except Exception as _vision_exc: + print('[DONA VISION] Analyse vocale :',type(_vision_exc).__name__,flush=True) + _vision_answer = 'Analyse visuelle indisponible. Vérifie le modèle vision Hermes.' + with STATE_LOCK: + STATE['content']={'type':'text','text':_vision_answer} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('voice',_dona_v93_original_vosk,_vision_answer)) + if _vision_answer and not audio_settings.speaker_muted(): + emit_state('speaking') + _vision_done=threading.Event() + if speak_async(_vision_answer[:850],on_end=_vision_done.set):_vision_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_FILES_V91 : questions explicites sur la pièce jointe active. + if _dona_files_v91.is_file_question(r.text): + _file_speech = _dona_files_v91.handle_voice(r.text, socketio, STATE, STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_file_speech)) + if _file_speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _file_done = threading.Event() + if speak_async(_file_speech, on_end=_file_done.set): _file_done.wait(timeout=120) + emit_state('idle', partial='') + return + + # DONA_SUITE_V9: intents locaux explicites, sinon continuer vers Hermes. + if _dona_suite_v9.is_request(r.text): + _v9_speech=_dona_suite_v9.handle_and_emit(r.text,socketio) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_v9_speech)) + if _v9_speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _v9_done=threading.Event() + _v9_ok=speak_async(_v9_speech,on_end=_v9_done.set) + if _v9_ok:_v9_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_NAV_V83: cartes et itinéraires à la demande, sans LLM. + if _dona_nav.is_request(r.text): + _nav_reply=_dona_nav.handle_and_emit(r.text,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_nav_reply)) + if _nav_reply and not audio_settings.speaker_muted(): + emit_state('speaking') + _nav_done=threading.Event() + _nav_ok=speak_async(_nav_reply,on_end=lambda: (_nav_done.set(),socketio.emit('alfred_tts_finished',{'source':'voice'}))) + if _nav_ok:_nav_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_YOUTUBE_V82 : recherche publique et contrôle du lecteur à la voix. + if _dona_youtube.is_search(r.text): + _speech=_dona_youtube.search_and_emit(r.text, socketio, STATE, STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_speech)) + if _speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _evt=threading.Event() + _ok=speak_async(_speech,on_end=_evt.set) + if _ok: _evt.wait(timeout=120) + emit_state('idle',partial='') + return + if _dona_youtube.is_control(r.text,_dona_youtube.player_active()): + _dona_youtube.control_and_emit(r.text,socketio) + emit_state('idle',partial='') + return + + # DONA_V8_DYNAMIC — creation visuelle explicite, source Hermes/MCP ou API directe. + if _dona_dynamic.is_dynamic_request(r.text): + _board = _dona_dynamic.build_dashboard(r.text, _dona_dashboard_agent.ask_dashboard) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('voice',_dona_v93_original_vosk,_board.get('title','Tableau de bord'))) + _payload = {'type':'dona_dynamic', 'dashboard':_board} + with STATE_LOCK: + STATE['content'] = _payload + socketio.emit('alfred_render', _payload) + if not audio_settings.speaker_muted(): + emit_state('speaking') + _finished = threading.Event() + _started = speak_async('Voici ton tableau de bord : ' + _board['title'], on_end=_finished.set) + if _started: _finished.wait(timeout=120) + emit_state('idle', partial='') + return + + # 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 + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,speech)) + 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=lambda: (done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + 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: + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_lists_reply)) + _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=lambda: (_lists_done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + _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 + # DONA_V7_RELEASE: reconnaissance sur ordre vocal explicite, sinon Hermes normal. + answer = _dona_face_recognize(ask_hermes) if _dona_face_request(r.text) else ask_hermes(_dona_learning_v93.enhance_prompt(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} + + # DONA_LEARNING_V93 : évaluation du tour Hermes, RAM seulement avant 👍 / 👎. + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('voice',_dona_v93_original_vosk,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() + duplex_monitor.start(speech) # ALFRED_DUPLEX_BARGE_IN_V1 + try: + started = speak_async(speech, on_end=lambda: (done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + if started: + done.wait(timeout=120) + finally: + if not duplex_monitor.interrupted(): + duplex_monitor.stop() + # ALFRED_DUPLEX_CONVERSATION_V2: stop-only or keep the rest of the spoken follow-up. + if duplex_monitor.interrupted(): + pending_duplex_text = duplex_monitor.wait_for_followup(timeout=30) + duplex_monitor.stop() + else: + # A few wake-free seconds after the assistant finishes speaking. + pending_duplex_text = duplex_monitor.listen_conversation() + if pending_duplex_text: + print('[ALFRED DUPLEX V2] Question enchaînée :', repr(pending_duplex_text), flush=True) + + 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 pending_duplex_text and not audio_settings.mic_muted(): # ALFRED_DUPLEX_CONVERSATION_V2 + # Only the voice is cancelled; the original Hermes job is untouched. + wake.pause() + threading.Thread(target=conversation, kwargs={'injected_text': pending_duplex_text}, + daemon=True, name='dona-duplex-followup').start() + elif 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/duplex/status') +def duplex_status(): + return jsonify(duplex_monitor.status()) + +@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=lambda: (_done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + _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 + + +# ALFRED_IDENTITY_GLASS_V1: page /identity et synchronisation du nom. +from services.alfred_identity_page import register_identity_page as _alfred_identity_register +_alfred_identity_register(app) + + +# ALFRED_DEVELOPER_V1 : interface /maintenance (administrateur). +from services.alfred_developer_page import register_developer_page as _alfred_developer_register +_alfred_developer_register(app) + + +# ALFRED_HERMES_NEW_SESSION_V1 : session Hermes neuve via /config admin. +from services.alfred_hermes_session_page import register_hermes_session_page as _register_hermes_new_session +_register_hermes_new_session(app) + + +# DONA_FAMILY_UI_V1_START +# Text entered in the adult UI uses the existing Hermes session, not microphone/TTS. +def _dona_submit_typed(prompt): + if not conversation_lock.acquire(blocking=False): + return False + def _dona_run_typed(): + try: + wake.pause() + with STATE_LOCK: + _before_image = IMAGE_REVISION + emit_state('thinking', partial='', transcript=prompt, + content={'type': 'text', 'text': prompt}) + # DONA_CLOCK_SCULPTURE_V11 : même traitement depuis Écrire à Dona / raccourcis. + _dona_local_clock_reply = _dona_clock_local.answer(prompt) + if _dona_local_clock_reply is not None: + with STATE_LOCK: + STATE['content']={'type':'text','text':_dona_local_clock_reply} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('typed',prompt,_dona_local_clock_reply)) + emit_state('idle',partial='') + return + + # DONA_VISION_V95 : demande écrite de vision depuis la caméra réelle. + if _dona_vision_v95.is_camera_question(prompt): + try: + _vision_answer=_dona_vision_monitor.analyze(prompt) + except Exception as _vision_exc: + _vision_answer=str(_vision_exc)[:190] if isinstance(_vision_exc,RuntimeError) else 'Analyse visuelle indisponible.' + with STATE_LOCK: + STATE['content']={'type':'text','text':_vision_answer} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_vision_answer)) + emit_state('idle',partial='') + return + + # DONA_FILES_V91 : question écrite à propos de la dernière pièce jointe. + if _dona_files_v91.is_file_question(prompt): + _dona_files_v91.handle_voice(prompt, socketio, STATE, STATE_LOCK) + emit_state('idle',partial='') + return + + # DONA_SUITE_V9: commandes quotidiennes écrites, sans perturber Hermes. + if _dona_suite_v9.is_request(prompt): + _v93_local=_dona_suite_v9.handle_and_emit(prompt,socketio) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + + # DONA_NAV_V83: commande écrite navigation, sans polluer la session Hermes. + if _dona_nav.is_request(prompt): + _v93_local=_dona_nav.handle_and_emit(prompt,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + + # DONA_YOUTUBE_V82 : vidéos et commandes écrites hors conversation Hermes. + if _dona_youtube.is_search(prompt): + _v93_local=_dona_youtube.search_and_emit(prompt,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + if _dona_youtube.is_control(prompt,_dona_youtube.player_active()): + _dona_youtube.control_and_emit(prompt,socketio) + emit_state('idle',partial='') + return + + # DONA_V8_DYNAMIC — commandes visuelles saisies via le formulaire habituel. + if _dona_dynamic.is_dynamic_request(prompt): + _board = _dona_dynamic.build_dashboard(prompt, _dona_dashboard_agent.ask_dashboard) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_board.get('title','Tableau de bord'))) + _payload = {'type':'dona_dynamic','dashboard':_board} + with STATE_LOCK: + STATE['content'] = _payload + socketio.emit('alfred_render',_payload) + emit_state('idle', partial='') + return + _answer = ask_hermes(_dona_learning_v93.enhance_prompt(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} + with STATE_LOCK: + if IMAGE_REVISION == _before_image: + STATE['content'] = _display + socketio.emit('alfred_render', _display) + # DONA_LEARNING_V93 : évaluation de l’échange écrit. + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('typed',prompt,_speech)) + # Deliberately no speak_async(): typed interaction is silent. + emit_state('idle', partial='') + except Exception as exc: + print('[DONA TEXT] Echec de la demande:', type(exc).__name__, str(exc), flush=True) + import traceback + traceback.print_exc() + emit_state('error', content={'type':'text', 'text':'Hermes n’a pas pu répondre.'}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + threading.Thread(target=_dona_run_typed, daemon=True, name='dona-typed-hermes').start() + return True + +from services.dona_family_ui import register_dona_family as _dona_register_family +_dona_register_family(app, adult_command=_dona_submit_typed) +# DONA_FAMILY_UI_V1_END + +# DONA_V8_DYNAMIC — tableau de bord choisi, généré et rendu en composants contrôlés. +from services import dona_dynamic as _dona_dynamic +# DONA_V8_1_ISOLATION : tableaux isolés des conversations normales +from services import dona_dashboard_agent as _dona_dashboard_agent + +def _dona_dynamic_submit(prompt): + if not conversation_lock.acquire(blocking=False): + return False + def _work(): + try: + wake.pause() + emit_state('thinking',transcript=prompt,partial='') + _board=_dona_dynamic.build_dashboard(prompt,_dona_dashboard_agent.ask_dashboard) + _payload={'type':'dona_dynamic','dashboard':_board} + with STATE_LOCK: + STATE['content']=_payload + socketio.emit('alfred_render',_payload) + emit_state('idle',partial='') + except Exception as exc: + print('[DONA DYNAMIC V8] ',type(exc).__name__,str(exc),flush=True) + emit_state('error',content={'type':'text','text':'Impossible de préparer ce tableau de bord.'}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + threading.Thread(target=_work,daemon=True,name='dona-dynamic-v8').start() + return True + +_dona_dynamic.register_dynamic(app,_dona_dynamic_submit) + +# DONA_VISION_V95 : API vision/YOLOv8, surveillance opt-in. +from services import dona_vision_v95 as _dona_vision_v95 +_dona_vision_monitor = _dona_vision_v95.register(app, socketio) + +# DONA_LEARNING_V93 : retours explicites et corrections approuvées. +from services import dona_learning_v93 as _dona_learning_v93 +_dona_learning_v93.register(app) + +# DONA_UI_V92 : préférences de raccourcis persistantes, menus gérés côté navigateur. +from services import dona_shortcuts_v92 as _dona_shortcuts_v92 +_dona_shortcuts_v92.register(app) + +# DONA_FILES_V91 : fichiers temporaires et analyse Hermes isolée. +from services import dona_files_v91 as _dona_files_v91 +_dona_files_v91.register_files(app) + +# DONA_SUITE_V9 : routeur local, cockpit, mémoire, cuisine, documents, routines. +from services import dona_suite_v9 as _dona_suite_v9 +_dona_suite_v9.register_suite(app,socketio) + +# DONA_NAV_V83: géocodage, carte et transfert Waze. +from services import dona_navigation as _dona_nav +_dona_nav.register_navigation(app,socketio) + +# DONA_YOUTUBE_V82 : lecture publique, sans clé API, moteur Hermes isolé. +from services import dona_youtube as _dona_youtube +_dona_youtube.register_youtube(app,socketio) + +# DONA_SPOTIFY_V6 — player read/control API, reuses Hermes OAuth (no LLM polling). +from services.dona_spotify import register_spotify_panel as _register_dona_spotify +_register_dona_spotify(app) + +# DONA_V7_RELEASE : route manuelle caméra + MCP existant; aucune base de visage modifiée. +from services.dona_face import (register_face as _dona_register_face, + recognize_once as _dona_face_recognize, is_face_request as _dona_face_request) +_dona_register_face(app, socketio, ask_hermes, conversation_lock) + + +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/backups/dona_json_autorender_v1_20261101_141838_580140/manifest.json b/alfred/backups/dona_json_autorender_v1_20261101_141838_580140/manifest.json new file mode 100644 index 0000000..3fa2374 --- /dev/null +++ b/alfred/backups/dona_json_autorender_v1_20261101_141838_580140/manifest.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "appdir": "/opt/alfred/app", + "files": { + "app.py": { + "old": "246913f10e751bbd35f71dbcfa553f8284cdd1c050d3eadf1d2f276ce33ef184", + "new": "367380be744b53e3cb478b359726e18964971dac16540f88b922e90f932eafa7" + }, + "services/dona_json_autorender_v1.py": { + "old": null, + "new": "3cb1e5147af521a3a133c3568b33aa5eb6a0e5d38eee24a89a852eb575fbbf18" + } + } +} \ No newline at end of file diff --git a/alfred/backups/dona_reply_floating_v1_20261101_132228_531763/manifest.json b/alfred/backups/dona_reply_floating_v1_20261101_132228_531763/manifest.json new file mode 100644 index 0000000..61d5f5f --- /dev/null +++ b/alfred/backups/dona_reply_floating_v1_20261101_132228_531763/manifest.json @@ -0,0 +1,19 @@ +{ + "kind": "dona-reply-floating-v1", + "app": "/opt/alfred/app", + "modified": [ + "templates/index.html" + ], + "created": [ + "static/css/dona-reply-floating-v1.css", + "static/js/dona-reply-floating-v1.js" + ], + "original_sha256": { + "templates/index.html": "5033a7cdccf4ca47307afea1f70ca648d9c91d3e310969b384b23d9648f0a854" + }, + "installed_sha256": { + "templates/index.html": "8f6a1a7d9fa934d1fefa646976b6cf2b27e0ecc9e1dc0d2adf5bbf9ab3e1c390", + "static/css/dona-reply-floating-v1.css": "c3411ba829e1217885a9f32e6122099c12e921119e65ad6586339b84a3f8ee40", + "static/js/dona-reply-floating-v1.js": "d0feb5ca2152ba3b7291a2c26e81ec1541aca37051d10e2d883c9db0d7a237b3" + } +} \ No newline at end of file diff --git a/alfred/backups/dona_reply_floating_v1_20261101_132228_531763/original/templates/index.html b/alfred/backups/dona_reply_floating_v1_20261101_132228_531763/original/templates/index.html new file mode 100644 index 0000000..5342a49 --- /dev/null +++ b/alfred/backups/dona_reply_floating_v1_20261101_132228_531763/original/templates/index.html @@ -0,0 +1,262 @@ + + + + + + + {{ name }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + En attente +
+ +
+ + ⚙ + ▤ + + + + + + + + + + + +
+
+ + + + + + +
+

Bonjour.

À votre écoute.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/manifest.json b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/manifest.json new file mode 100644 index 0000000..95727db --- /dev/null +++ b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/manifest.json @@ -0,0 +1,22 @@ +{ + "kind": "dona-waves-voice-v1", + "app": "/opt/alfred/app", + "original_files": [ + "templates/index.html", + "static/js/dona-accueil-ecrit-v1.js", + "services/piper_tts.py", + "app.py" + ], + "created_files": [ + "static/css/dona-waves-v1.css", + "static/js/dona-waves-v1.js" + ], + "installed_sha256": { + "templates/index.html": "5033a7cdccf4ca47307afea1f70ca648d9c91d3e310969b384b23d9648f0a854", + "static/js/dona-accueil-ecrit-v1.js": "aba0a743b74d8fd5f2e5e91b129db69885a350a40c095f90d15ef28c0bc20c8e", + "services/piper_tts.py": "a80aecfa38ceeeb1779da502801878ab435b6b0721ec2ffc26b020871115dd71", + "app.py": "472959331cfece1d75100f32d77c57b4b1838fafff45f0d49e13fd17837d942a", + "static/css/dona-waves-v1.css": "9c9586493b6b6ec506928f6ebac79f28053b9df36b680ce1c0835aa75bf3f1cb", + "static/js/dona-waves-v1.js": "8c33a71da919da54533ecdffc1a4368c9ea16daff893a082a14f05170ac9b3c5" + } +} \ No newline at end of file diff --git a/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/app.py b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/app.py new file mode 100644 index 0000000..ed91612 --- /dev/null +++ b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/app.py @@ -0,0 +1,882 @@ + +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 + +# Charger la configuration AVANT les imports des services +load_dotenv("/opt/alfred/.env") +load_dotenv( + "/opt/alfred/app/config/control_center.env", + override=True +) + +from services.audio import listen_once, speak_async, tts_backend_detect +from services.wakeword import WakeWordListener + + + + + + + +from services.alfred_duplex import DuplexMonitor # ALFRED_DUPLEX_BARGE_IN_V1 +from services.piper_tts import stop_playback as duplex_stop_piper +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}) +# ALFRED_DUPLEX_BARGE_IN_V1: interruption audio sans interruption des tâches Hermes +duplex_monitor = DuplexMonitor(muted=audio_settings.mic_muted, + on_interrupt=duplex_stop_piper, + on_status=lambda event: socketio.emit("alfred_duplex_interrupted" if event.get("interrupted") else "alfred_duplex_activity", event)) + +# 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, injected_text=None): # ALFRED_DUPLEX_CONVERSATION_V2 + if not conversation_lock.acquire(blocking=False): + return + pending_duplex_text = None # ALFRED_DUPLEX_CONVERSATION_V2 + + try: + emit_state("listening", partial="", transcript="", wake_score=score) + + print("[ALFRED] Début écoute STT", flush=True) + + if injected_text is not None: # ALFRED_DUPLEX_CONVERSATION_V2 + from services.audio import STTResult + r = STTResult(text=injected_text) + else: + 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 + + # DONA_LEARNING_V93 : évaluation vocale explicite, puis correction Vosk approuvée. + _dona_v93_original_vosk = r.text + _dona_v93_ack = _dona_learning_v93.voice_feedback(r.text) + if _dona_v93_ack is not None: + socketio.emit('alfred_render', {'type':'text','text':_dona_v93_ack}) + if not audio_settings.speaker_muted(): + _dona_v93_done = threading.Event() + if speak_async(_dona_v93_ack,on_end=_dona_v93_done.set): + _dona_v93_done.wait(timeout=120) + emit_state('idle',partial='') + return + r.text = _dona_learning_v93.correct_transcript(r.text) + if r.text != _dona_v93_original_vosk: + socketio.emit('dona_learning_stt', {'raw':_dona_v93_original_vosk,'used':r.text}) + + emit_state( + "thinking", + partial="", + transcript=r.text, + content={"type": "text", "text": r.text} + ) + + # DONA_VISION_V95 : photo locale et compréhension par Hermes sur demande. + if _dona_vision_v95.is_camera_question(r.text): + try: + _vision_answer = _dona_vision_monitor.analyze(r.text) + except RuntimeError as _vision_exc: + _vision_answer = str(_vision_exc) + except Exception as _vision_exc: + print('[DONA VISION] Analyse vocale :',type(_vision_exc).__name__,flush=True) + _vision_answer = 'Analyse visuelle indisponible. Vérifie le modèle vision Hermes.' + with STATE_LOCK: + STATE['content']={'type':'text','text':_vision_answer} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('voice',_dona_v93_original_vosk,_vision_answer)) + if _vision_answer and not audio_settings.speaker_muted(): + emit_state('speaking') + _vision_done=threading.Event() + if speak_async(_vision_answer[:850],on_end=_vision_done.set):_vision_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_FILES_V91 : questions explicites sur la pièce jointe active. + if _dona_files_v91.is_file_question(r.text): + _file_speech = _dona_files_v91.handle_voice(r.text, socketio, STATE, STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_file_speech)) + if _file_speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _file_done = threading.Event() + if speak_async(_file_speech, on_end=_file_done.set): _file_done.wait(timeout=120) + emit_state('idle', partial='') + return + + # DONA_SUITE_V9: intents locaux explicites, sinon continuer vers Hermes. + if _dona_suite_v9.is_request(r.text): + _v9_speech=_dona_suite_v9.handle_and_emit(r.text,socketio) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_v9_speech)) + if _v9_speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _v9_done=threading.Event() + _v9_ok=speak_async(_v9_speech,on_end=_v9_done.set) + if _v9_ok:_v9_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_NAV_V83: cartes et itinéraires à la demande, sans LLM. + if _dona_nav.is_request(r.text): + _nav_reply=_dona_nav.handle_and_emit(r.text,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_nav_reply)) + if _nav_reply and not audio_settings.speaker_muted(): + emit_state('speaking') + _nav_done=threading.Event() + _nav_ok=speak_async(_nav_reply,on_end=lambda: (_nav_done.set(),socketio.emit('alfred_tts_finished',{'source':'voice'}))) + if _nav_ok:_nav_done.wait(timeout=120) + emit_state('idle',partial='') + return + + # DONA_YOUTUBE_V82 : recherche publique et contrôle du lecteur à la voix. + if _dona_youtube.is_search(r.text): + _speech=_dona_youtube.search_and_emit(r.text, socketio, STATE, STATE_LOCK) + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_speech)) + if _speech and not audio_settings.speaker_muted(): + emit_state('speaking') + _evt=threading.Event() + _ok=speak_async(_speech,on_end=_evt.set) + if _ok: _evt.wait(timeout=120) + emit_state('idle',partial='') + return + if _dona_youtube.is_control(r.text,_dona_youtube.player_active()): + _dona_youtube.control_and_emit(r.text,socketio) + emit_state('idle',partial='') + return + + # DONA_V8_DYNAMIC — creation visuelle explicite, source Hermes/MCP ou API directe. + if _dona_dynamic.is_dynamic_request(r.text): + _board = _dona_dynamic.build_dashboard(r.text, _dona_dashboard_agent.ask_dashboard) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('voice',_dona_v93_original_vosk,_board.get('title','Tableau de bord'))) + _payload = {'type':'dona_dynamic', 'dashboard':_board} + with STATE_LOCK: + STATE['content'] = _payload + socketio.emit('alfred_render', _payload) + if not audio_settings.speaker_muted(): + emit_state('speaking') + _finished = threading.Event() + _started = speak_async('Voici ton tableau de bord : ' + _board['title'], on_end=_finished.set) + if _started: _finished.wait(timeout=120) + emit_state('idle', partial='') + return + + # 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 + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,speech)) + 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=lambda: (done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + 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: + socketio.emit('dona_learning_turn', _dona_learning_v93.track('voice',_dona_v93_original_vosk,_lists_reply)) + _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=lambda: (_lists_done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + _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 + # DONA_V7_RELEASE: reconnaissance sur ordre vocal explicite, sinon Hermes normal. + answer = _dona_face_recognize(ask_hermes) if _dona_face_request(r.text) else ask_hermes(_dona_learning_v93.enhance_prompt(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} + + # DONA_LEARNING_V93 : évaluation du tour Hermes, RAM seulement avant 👍 / 👎. + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('voice',_dona_v93_original_vosk,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() + duplex_monitor.start(speech) # ALFRED_DUPLEX_BARGE_IN_V1 + try: + started = speak_async(speech, on_end=lambda: (done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + if started: + done.wait(timeout=120) + finally: + if not duplex_monitor.interrupted(): + duplex_monitor.stop() + # ALFRED_DUPLEX_CONVERSATION_V2: stop-only or keep the rest of the spoken follow-up. + if duplex_monitor.interrupted(): + pending_duplex_text = duplex_monitor.wait_for_followup(timeout=30) + duplex_monitor.stop() + else: + # A few wake-free seconds after the assistant finishes speaking. + pending_duplex_text = duplex_monitor.listen_conversation() + if pending_duplex_text: + print('[ALFRED DUPLEX V2] Question enchaînée :', repr(pending_duplex_text), flush=True) + + 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 pending_duplex_text and not audio_settings.mic_muted(): # ALFRED_DUPLEX_CONVERSATION_V2 + # Only the voice is cancelled; the original Hermes job is untouched. + wake.pause() + threading.Thread(target=conversation, kwargs={'injected_text': pending_duplex_text}, + daemon=True, name='dona-duplex-followup').start() + elif 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/duplex/status') +def duplex_status(): + return jsonify(duplex_monitor.status()) + +@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=lambda: (_done.set(), socketio.emit('alfred_tts_finished', {'source':'voice'}))) + _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 + + +# ALFRED_IDENTITY_GLASS_V1: page /identity et synchronisation du nom. +from services.alfred_identity_page import register_identity_page as _alfred_identity_register +_alfred_identity_register(app) + + +# ALFRED_DEVELOPER_V1 : interface /maintenance (administrateur). +from services.alfred_developer_page import register_developer_page as _alfred_developer_register +_alfred_developer_register(app) + + +# ALFRED_HERMES_NEW_SESSION_V1 : session Hermes neuve via /config admin. +from services.alfred_hermes_session_page import register_hermes_session_page as _register_hermes_new_session +_register_hermes_new_session(app) + + +# DONA_FAMILY_UI_V1_START +# Text entered in the adult UI uses the existing Hermes session, not microphone/TTS. +def _dona_submit_typed(prompt): + if not conversation_lock.acquire(blocking=False): + return False + def _dona_run_typed(): + try: + wake.pause() + with STATE_LOCK: + _before_image = IMAGE_REVISION + emit_state('thinking', partial='', transcript=prompt, + content={'type': 'text', 'text': prompt}) + # DONA_VISION_V95 : demande écrite de vision depuis la caméra réelle. + if _dona_vision_v95.is_camera_question(prompt): + try: + _vision_answer=_dona_vision_monitor.analyze(prompt) + except Exception as _vision_exc: + _vision_answer=str(_vision_exc)[:190] if isinstance(_vision_exc,RuntimeError) else 'Analyse visuelle indisponible.' + with STATE_LOCK: + STATE['content']={'type':'text','text':_vision_answer} + socketio.emit('alfred_render',STATE['content']) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_vision_answer)) + emit_state('idle',partial='') + return + + # DONA_FILES_V91 : question écrite à propos de la dernière pièce jointe. + if _dona_files_v91.is_file_question(prompt): + _dona_files_v91.handle_voice(prompt, socketio, STATE, STATE_LOCK) + emit_state('idle',partial='') + return + + # DONA_SUITE_V9: commandes quotidiennes écrites, sans perturber Hermes. + if _dona_suite_v9.is_request(prompt): + _v93_local=_dona_suite_v9.handle_and_emit(prompt,socketio) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + + # DONA_NAV_V83: commande écrite navigation, sans polluer la session Hermes. + if _dona_nav.is_request(prompt): + _v93_local=_dona_nav.handle_and_emit(prompt,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + + # DONA_YOUTUBE_V82 : vidéos et commandes écrites hors conversation Hermes. + if _dona_youtube.is_search(prompt): + _v93_local=_dona_youtube.search_and_emit(prompt,socketio,STATE,STATE_LOCK) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_v93_local)) + emit_state('idle',partial='') + return + if _dona_youtube.is_control(prompt,_dona_youtube.player_active()): + _dona_youtube.control_and_emit(prompt,socketio) + emit_state('idle',partial='') + return + + # DONA_V8_DYNAMIC — commandes visuelles saisies via le formulaire habituel. + if _dona_dynamic.is_dynamic_request(prompt): + _board = _dona_dynamic.build_dashboard(prompt, _dona_dashboard_agent.ask_dashboard) + socketio.emit('dona_learning_turn',_dona_learning_v93.track('typed',prompt,_board.get('title','Tableau de bord'))) + _payload = {'type':'dona_dynamic','dashboard':_board} + with STATE_LOCK: + STATE['content'] = _payload + socketio.emit('alfred_render',_payload) + emit_state('idle', partial='') + return + _answer = ask_hermes(_dona_learning_v93.enhance_prompt(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} + with STATE_LOCK: + if IMAGE_REVISION == _before_image: + STATE['content'] = _display + socketio.emit('alfred_render', _display) + # DONA_LEARNING_V93 : évaluation de l’échange écrit. + socketio.emit('dona_learning_turn', + _dona_learning_v93.track('typed',prompt,_speech)) + # Deliberately no speak_async(): typed interaction is silent. + emit_state('idle', partial='') + except Exception as exc: + print('[DONA TEXT] Echec de la demande:', type(exc).__name__, str(exc), flush=True) + import traceback + traceback.print_exc() + emit_state('error', content={'type':'text', 'text':'Hermes n’a pas pu répondre.'}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + threading.Thread(target=_dona_run_typed, daemon=True, name='dona-typed-hermes').start() + return True + +from services.dona_family_ui import register_dona_family as _dona_register_family +_dona_register_family(app, adult_command=_dona_submit_typed) +# DONA_FAMILY_UI_V1_END + +# DONA_V8_DYNAMIC — tableau de bord choisi, généré et rendu en composants contrôlés. +from services import dona_dynamic as _dona_dynamic +# DONA_V8_1_ISOLATION : tableaux isolés des conversations normales +from services import dona_dashboard_agent as _dona_dashboard_agent + +def _dona_dynamic_submit(prompt): + if not conversation_lock.acquire(blocking=False): + return False + def _work(): + try: + wake.pause() + emit_state('thinking',transcript=prompt,partial='') + _board=_dona_dynamic.build_dashboard(prompt,_dona_dashboard_agent.ask_dashboard) + _payload={'type':'dona_dynamic','dashboard':_board} + with STATE_LOCK: + STATE['content']=_payload + socketio.emit('alfred_render',_payload) + emit_state('idle',partial='') + except Exception as exc: + print('[DONA DYNAMIC V8] ',type(exc).__name__,str(exc),flush=True) + emit_state('error',content={'type':'text','text':'Impossible de préparer ce tableau de bord.'}) + finally: + conversation_lock.release() + if not audio_settings.mic_muted(): + wake.resume() + threading.Thread(target=_work,daemon=True,name='dona-dynamic-v8').start() + return True + +_dona_dynamic.register_dynamic(app,_dona_dynamic_submit) + +# DONA_VISION_V95 : API vision/YOLOv8, surveillance opt-in. +from services import dona_vision_v95 as _dona_vision_v95 +_dona_vision_monitor = _dona_vision_v95.register(app, socketio) + +# DONA_LEARNING_V93 : retours explicites et corrections approuvées. +from services import dona_learning_v93 as _dona_learning_v93 +_dona_learning_v93.register(app) + +# DONA_UI_V92 : préférences de raccourcis persistantes, menus gérés côté navigateur. +from services import dona_shortcuts_v92 as _dona_shortcuts_v92 +_dona_shortcuts_v92.register(app) + +# DONA_FILES_V91 : fichiers temporaires et analyse Hermes isolée. +from services import dona_files_v91 as _dona_files_v91 +_dona_files_v91.register_files(app) + +# DONA_SUITE_V9 : routeur local, cockpit, mémoire, cuisine, documents, routines. +from services import dona_suite_v9 as _dona_suite_v9 +_dona_suite_v9.register_suite(app,socketio) + +# DONA_NAV_V83: géocodage, carte et transfert Waze. +from services import dona_navigation as _dona_nav +_dona_nav.register_navigation(app,socketio) + +# DONA_YOUTUBE_V82 : lecture publique, sans clé API, moteur Hermes isolé. +from services import dona_youtube as _dona_youtube +_dona_youtube.register_youtube(app,socketio) + +# DONA_SPOTIFY_V6 — player read/control API, reuses Hermes OAuth (no LLM polling). +from services.dona_spotify import register_spotify_panel as _register_dona_spotify +_register_dona_spotify(app) + +# DONA_V7_RELEASE : route manuelle caméra + MCP existant; aucune base de visage modifiée. +from services.dona_face import (register_face as _dona_register_face, + recognize_once as _dona_face_recognize, is_face_request as _dona_face_request) +_dona_register_face(app, socketio, ask_hermes, conversation_lock) + + +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/backups/dona_waves_voice_v1_20261031_115646_870874/original/services/piper_tts.py b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/services/piper_tts.py new file mode 100644 index 0000000..91f0fb4 --- /dev/null +++ b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/services/piper_tts.py @@ -0,0 +1,152 @@ +import asyncio +import os +import subprocess +import tempfile +import time +import threading # ALFRED_DUPLEX_BARGE_IN_V1 +import wave + +from wyoming.audio import AudioChunk, AudioStart, AudioStop +from wyoming.client import AsyncTcpClient +from wyoming.tts import Synthesize, SynthesizeVoice + + +PIPER_HOST = os.getenv("PIPER_HOST", "127.0.0.1") +PIPER_PORT = int(os.getenv("PIPER_PORT", "10200")) +AUDIO_DEVICE = os.getenv("AUDIO_OUTPUT_DEVICE", "plughw:1,0") + + +async def synthesize(text, wav_path): + client = AsyncTcpClient(PIPER_HOST, PIPER_PORT) + + await client.connect() + + try: + await client.write_event( + Synthesize( + text=text, + voice=SynthesizeVoice( + name=os.getenv("PIPER_VOICE", "fr_FR-tom-medium") + )).event() + ) + + wav_file = None + + try: + while True: + event = await asyncio.wait_for( + client.read_event(), + timeout=30 + ) + + if event is None: + raise RuntimeError("Connexion Piper interrompue") + + if AudioStart.is_type(event.type): + start = AudioStart.from_event(event) + + wav_file = wave.open(wav_path, "wb") + wav_file.setnchannels(start.channels) + wav_file.setsampwidth(start.width) + wav_file.setframerate(start.rate) + + elif AudioChunk.is_type(event.type): + chunk = AudioChunk.from_event(event) + + if wav_file is None: + wav_file = wave.open(wav_path, "wb") + wav_file.setnchannels(chunk.channels) + wav_file.setsampwidth(chunk.width) + wav_file.setframerate(chunk.rate) + + wav_file.writeframes(chunk.audio) + + elif AudioStop.is_type(event.type): + break + + finally: + if wav_file is not None: + wav_file.close() + + finally: + await client.disconnect() + + +# ALFRED_DUPLEX_BARGE_IN_V1: control only this module's aplay process. +_playback_lock = threading.RLock() +_playback_cancel = threading.Event() +_playback_proc = None + + +def stop_playback(): + """Interrupts Piper playback or marks pending synth for cancellation.""" + global _playback_proc + _playback_cancel.set() + with _playback_lock: + process = _playback_proc + if process is not None and process.poll() is None: + try: + process.terminate() + except OSError: + pass + + +def speak(text): + if not text or not text.strip(): + return + + _playback_cancel.clear() # ALFRED_DUPLEX_BARGE_IN_V1 + + fd, wav_path = tempfile.mkstemp( + prefix="alfred_piper_", + suffix=".wav" + ) + os.close(fd) + + start = time.monotonic() + + try: + print("[ALFRED PIPER] Synthèse...", flush=True) + + asyncio.run(synthesize(text, wav_path)) + + print("[ALFRED PIPER] Lecture...", flush=True) + + # ALFRED_DUPLEX_BARGE_IN_V1: allow a concurrent microphone detector + # to terminate playback, without killing Hermes or the entire service. + global _playback_proc + if _playback_cancel.is_set(): + print("[ALFRED PIPER] Annulé avant lecture", flush=True) + return + proc = subprocess.Popen(["aplay", "-q", "-D", AUDIO_DEVICE, wav_path]) + with _playback_lock: + _playback_proc = proc + if _playback_cancel.is_set() and proc.poll() is None: + proc.terminate() + try: + result = proc.wait(timeout=120) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + raise + finally: + with _playback_lock: + if _playback_proc is proc: + _playback_proc = None + if _playback_cancel.is_set(): + print("[ALFRED PIPER] Lecture interrompue par la voix", flush=True) + return + if result: + raise subprocess.CalledProcessError(result, proc.args) + + print( + f"[ALFRED PIPER] Terminé en {time.monotonic() - start:.2f}s", + flush=True + ) + + except Exception as e: + print(f"[ALFRED PIPER] Erreur : {e}", flush=True) + raise + finally: + if os.path.exists(wav_path): + os.remove(wav_path) diff --git a/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/static/js/dona-accueil-ecrit-v1.js b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/static/js/dona-accueil-ecrit-v1.js new file mode 100644 index 0000000..ab076b1 --- /dev/null +++ b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/static/js/dona-accueil-ecrit-v1.js @@ -0,0 +1,104 @@ +/* DONA_FAMILY_UI_V1: non-destructive augmentation of the installed Glass V1 dashboard. */ +(() => { + 'use strict'; + let attempts=0; + function init(){ + const root=document.getElementById('alfredMainV1'); + if(!root){if(++attempts<40)setTimeout(init,150);return;} + if(root.dataset.donaFamily==='ready')return; + const $=id=>document.getElementById(id); + const head=root.querySelector('.av-header-right'); + const weather=root.querySelector('.av-weather'); + const hero=root.querySelector('.av-voice-hero'); + const shortcuts=root.querySelector('.av-shortcuts'); + if(!head||!weather||!hero||!shortcuts)return; + root.dataset.donaFamily='ready';document.body.classList.add('dona-family-v1'); + + // Move the EXISTING weather widget: old JS keeps updating the SAME IDs. + weather.classList.add('dona-inline-weather'); + weather.setAttribute('title','Ouvrir les détails météo'); + head.prepend(weather); + + // Child link is supplementary; all old admin/navigation functions remain. + const kid=document.createElement('a');kid.href='/enfant';kid.className='dona-child-link';kid.textContent='⭐ Espace enfant'; + kid.setAttribute('aria-label','Ouvrir le chat enfant, écrit uniquement'); + head.insertBefore(kid,head.querySelector('.av-head-actions')); + const toolsGrid=root.querySelector('#avMoreDialog .av-more-grid'); + if(toolsGrid&&!toolsGrid.querySelector('a[href="/enfant"]')){ + const a=document.createElement('a');a.href='/enfant';a.textContent='⭐ Espace enfant';toolsGrid.append(a); + } + + // New expressive centre; old controls stay in DOM so audio event listeners work. + const display=document.createElement('div');display.className='dona-art-center'; + display.innerHTML='
Prêt à vous aider'; + hero.prepend(display); + const oldVoice=root.querySelector('.av-voice-center'); + if(oldVoice){display.append(oldVoice);} + + // Move shortcuts into a collapsed tray, instead of removing their controls. + const tools=document.createElement('details');tools.className='dona-tools-tray'; + const summary=document.createElement('summary');summary.textContent='☷ Raccourcis'; + shortcuts.parentNode.insertBefore(tools,shortcuts);tools.append(summary,shortcuts); + + // Minimal permanent one-row text field: no expandable tray, send at right. + const compose=document.createElement('div');compose.className='dona-compose dona-compose-compact'; + compose.innerHTML=`
+ +
+ +
`; + root.append(compose); + const body=$('donaComposeBody'),input=$('donaComposeInput'),send=$('donaComposeSend'),status=$('donaComposeStatus'); + input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();body.requestSubmit();}}); + body.addEventListener('submit',async e=>{ + e.preventDefault();let message=input.value.trim();if(!message)return; + send.disabled=true;status.textContent='Envoi à Hermes…'; + try{ + const r=await fetch('/api/dona/texte',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({message})}); + let d={};try{d=await r.json();}catch(_){} + if(!r.ok)throw Error(d.error||'Erreur '+r.status); + input.value='';status.textContent='Hermes prépare sa réponse…'; + }catch(err){status.textContent=err.message;} + finally{send.disabled=false;} + }); + const modeText={idle:'Prêt à vous aider',waiting:'Prêt à vous aider',listening:'Je vous écoute…',thinking:'Je réfléchis…',speaking:'Je vous réponds…',error:'Un problème est survenu'}; + function setMode(mode){root.dataset.familyMode=mode;const t=$('donaArtSubtitle');if(t)t.textContent=modeText[mode]||'Prêt à vous aider';if(mode==='idle'&&status.textContent==='Hermes prépare sa réponse…')status.textContent='Réponse reçue';} + function setName(value){const name=String(value||$('avName')?.textContent||'Dona').trim();$('donaArtName').textContent=name;} + setName();setMode(root.dataset.mode||'idle'); + const socket=window.alfredSocket; + socket?.on('alfred_state',s=>{setMode(String(s.mode||'idle').toLowerCase());setName(s.assistant_name||s.title||$('avName')?.textContent);}); + socket?.on('alfred_render',()=>{if(status.textContent==='Hermes prépare sa réponse…')status.textContent='Réponse reçue';}); + fetch('/api/state',{cache:'no-store'}).then(r=>r.ok?r.json():null).then(s=>{if(s){setMode(s.mode||'idle');setName(s.assistant_name||s.title);}}).catch(()=>{}); + + // Animated multi-layer liquid orb using canvas, independent of server state. + const canvas=$('donaOrganicCanvas'),ctx=canvas.getContext('2d'); + if(!ctx)return; + let w=320,h=320,phase=0,raf=0,last=0; + const reduce=matchMedia('(prefers-reduced-motion: reduce)').matches; + function resize(){const rect=canvas.getBoundingClientRect();const dpr=Math.min(devicePixelRatio||1,2);w=Math.max(150,rect.width);h=Math.max(150,rect.height);canvas.width=Math.floor(w*dpr);canvas.height=Math.floor(h*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);} + window.addEventListener('resize',resize,{passive:true});resize(); + function path(radius,layer){const n=130;ctx.beginPath();for(let k=0;k<=n;k++){ + const a=k/n*Math.PI*2;const modulation=1+0.067*Math.sin(a*3+phase*.72+layer*.64)+0.044*Math.cos(a*5-phase*.39+layer)+0.021*Math.sin(a*8+phase*.55); + const r=radius*modulation;const x=Math.cos(a)*r,y=Math.sin(a)*r; + if(k===0)ctx.moveTo(x,y);else ctx.lineTo(x,y); + }ctx.closePath();} + function frame(now){raf=requestAnimationFrame(frame);if(document.hidden||now-last<30)return;last=now; + const mode=root.dataset.familyMode||'idle';const speed=mode==='listening'?1.7:mode==='thinking'?1.38:mode==='speaking'?1.5:.75; + if(!reduce)phase+=.024*speed; + ctx.clearRect(0,0,w,h);ctx.save();ctx.translate(w/2,h/2);const r=Math.min(w,h)*.33; + const bloom=ctx.createRadialGradient(-r*.2,-r*.15,r*.1,0,0,r*1.45); + bloom.addColorStop(0,'rgba(238,117,160,.11)');bloom.addColorStop(.7,'rgba(139,112,246,.14)');bloom.addColorStop(1,'rgba(37,180,255,0)');ctx.fillStyle=bloom;ctx.beginPath();ctx.arc(0,0,r*1.48,0,Math.PI*2);ctx.fill(); + for(let layer=3;layer>=0;layer--){const waveR=r*(1+layer*.025);path(waveR,layer); + const g=ctx.createLinearGradient(-r*.9,r*.8,r*.9,-r*.85); + if(layer%2===0){g.addColorStop(0,'rgba(47,196,244,.76)');g.addColorStop(.36,'rgba(68,94,198,.88)');g.addColorStop(.62,'rgba(244,138,173,.74)');g.addColorStop(1,'rgba(255,176,71,.9)');} + else {g.addColorStop(0,'rgba(59,164,255,.65)');g.addColorStop(.4,'rgba(196,119,245,.48)');g.addColorStop(.7,'rgba(241,125,146,.52)');g.addColorStop(1,'rgba(255,185,95,.76)');} + ctx.globalAlpha=layer===0?.76:.25;ctx.fillStyle=g;ctx.fill(); + ctx.globalAlpha=layer===0?.92:.31;ctx.strokeStyle=g;ctx.lineWidth=layer===0?2.5:1.8;ctx.shadowBlur=layer===0?23:13;ctx.shadowColor=layer%2?'#bd80ec':'#ffc685';ctx.stroke();ctx.shadowBlur=0; + } + const ribbon=ctx.createLinearGradient(-r,r*.4,r,-r*.3);ribbon.addColorStop(0,'rgba(67,219,255,.0)');ribbon.addColorStop(.48,'rgba(255,231,197,.56)');ribbon.addColorStop(1,'rgba(255,181,103,.01)'); + ctx.beginPath();for(let i=0;i<=100;i++){const x=-r+i/100*r*2;const y=Math.sin(i/100*Math.PI*2+phase*.35)*r*.25+Math.cos(i/100*Math.PI*3-phase*.45)*r*.11;if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);}ctx.lineWidth=r*.26;ctx.strokeStyle=ribbon;ctx.globalAlpha=.38;ctx.stroke();ctx.restore(); + } + raf=requestAnimationFrame(frame);window.addEventListener('pagehide',()=>cancelAnimationFrame(raf),{once:true}); + } + init(); +})(); diff --git a/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/templates/index.html b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/templates/index.html new file mode 100644 index 0000000..112b220 --- /dev/null +++ b/alfred/backups/dona_waves_voice_v1_20261031_115646_870874/original/templates/index.html @@ -0,0 +1,258 @@ + + + + + + + {{ name }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + En attente +
+ +
+ + ⚙ + ▤ + + + + + + + + + + + +
+
+ + + + + + +
+

Bonjour.

À votre écoute.

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +