This commit is contained in:
pporcheret
2026-11-01 15:01:22 +01:00
parent 2c5537aff1
commit e2340807aa
36 changed files with 4952 additions and 8 deletions
+55 -8
View File
@@ -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
+116
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.
Binary file not shown.
+66
View File
@@ -0,0 +1,66 @@
"""Dona: heure/date du système, sans appel LLM. Fuseau Europe/Paris par défaut."""
from __future__ import annotations
from datetime import datetime
import os
import re
import unicodedata
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
DAYS = ('lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche')
MONTHS = ('janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet',
'août', 'septembre', 'octobre', 'novembre', 'décembre')
def _normalized(text: str) -> 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
@@ -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']
+55
View File
@@ -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:
@@ -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;
}
}
+34
View File
@@ -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;
}
}
@@ -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;
@@ -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();
})();
+258
View File
@@ -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<steps.length;i++)ctx.lineTo(steps[i].x,steps[i].y);
for(let i=steps.length-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<steps.length;i++)ctx.lineTo(steps[i].x,steps[i].y);
ctx.lineWidth=1.1+(layer%3)*.35+strength;
ctx.strokeStyle=g;ctx.shadowBlur=behind?11:21;
ctx.shadowColor=layer%2?'#f2a1bc':'#5faaff';ctx.stroke();ctx.shadowBlur=0;
// Fines nervures / maillage, effet étoffe lumineuse.
if(layer%2===0){
ctx.globalAlpha=.13+strength*.08;
for(let i=6;i<n;i+=7){
const p=steps[i],x=p.x,y=p.y;
ctx.beginPath();ctx.moveTo(x,y);
ctx.lineTo(x+Math.sin(layer+phi)*5,y+thickness*p.env);
ctx.lineWidth=.7;ctx.strokeStyle=layer%4?'#b9adff':'#ffcaad';ctx.stroke();
}
}
ctx.globalAlpha=1;
};
for(let layer=7;layer>=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<palette.length;layer++){
const [left,center,right]=palette[layer];
const m=layer*1.07;
const points=[];
for(let x=0;x<=w+5;x+=5){
const nx=x/w;
const envelope=Math.pow(Math.max(0,Math.sin(Math.PI*nx)),.75);
const s1=Math.sin(nx*Math.PI*(2.55+m*.24)+phase*(.70+m*.04)+m*.79);
const s2=Math.sin(nx*Math.PI*(5.0-m*.17)-phase*(.42+m*.02)+m*1.18);
const y=mid+(s1*.77+s2*.23)*amplitude*envelope
+Math.cos(nx*Math.PI*2.6+phase*.28+m)*h*.014;
points.push([x,y]);
}
const gradient=ctx.createLinearGradient(0,0,w,0);
gradient.addColorStop(0,left);gradient.addColorStop(.5,center);gradient.addColorStop(1,right);
ctx.beginPath();ctx.moveTo(0,mid);
for(const [x,y] of points)ctx.lineTo(x,y);
const thickness=h*(.11+.018*(layer%3)+.09*smoothed);
for(let i=points.length-1;i>=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();
})();
+8
View File
@@ -51,6 +51,10 @@
<link rel="stylesheet" href="/static/css/dona-learning-v9-3.css?v=93">
<!-- DONA_VISION_V95 -->
<link rel="stylesheet" href="/static/css/dona-vision-v9-5.css?v=95">
<!-- DONA_WAVES_V1 -->
<link rel="stylesheet" href="/static/css/dona-waves-v1.css?v=1">
<!-- DONA_REPLY_FLOATING_V1 -->
<link rel="stylesheet" href="/static/css/dona-reply-floating-v1.css?v=1">
</head>
<body>
<main class="alfred-shell">
@@ -254,5 +258,9 @@
<script defer src="/static/js/dona-ui-v9-2-1-close.js?v=921"></script>
<script defer src="/static/js/dona-learning-v9-3.js?v=93"></script>
<script defer src="/static/js/dona-vision-v9-5.js?v=95"></script>
<!-- DONA_WAVES_V1 -->
<script defer src="/static/js/dona-waves-v1.js?v=1"></script>
<!-- DONA_REPLY_FLOATING_V1 -->
<script defer src="/static/js/dona-reply-floating-v1.js?v=1"></script>
</body>
</html>