This commit is contained in:
pporcheret
2026-10-26 00:06:24 +01:00
parent c9c3d5f223
commit cd37ac3c0a
207 changed files with 19205 additions and 54 deletions
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Alfred — Configuration audio</title>
<style>
:root { color-scheme: dark; font-family: Inter, system-ui, -apple-system, sans-serif; background:#15171a; color:#f3f4f6; }
* { box-sizing:border-box; }
body { margin:0; min-height:100vh; display:grid; place-items:center; padding:28px; }
main { width:min(100%,640px); }
header { margin-bottom:30px; }
h1 { font-size:1.8rem; font-weight:550; margin:0 0 8px; }
.subtitle { color:#a5a9b0; margin:0; }
.panel { background:#202328; border:1px solid #34383f; border-radius:18px; padding:26px; }
.section { padding:20px 0; }
.section:first-child { padding-top:0; }
.section:last-child { padding-bottom:0; }
.section + .section { border-top:1px solid #353940; }
.line { display:flex; align-items:center; justify-content:space-between; gap:18px; }
.label { display:flex; align-items:center; gap:12px; font-size:1.05rem; }
.icon { width:24px; height:24px; color:#c9cdd3; flex-shrink:0; }
.value { font-variant-numeric:tabular-nums; color:#c8cbd1; }
input[type=range] { width:100%; margin:23px 0 2px; accent-color:#e5e7eb; cursor:pointer; }
input[type=range]:disabled { opacity:.35; cursor:not-allowed; }
.switch { position:relative; width:54px; height:30px; display:inline-block; flex-shrink:0; }
.switch input { opacity:0; width:0; height:0; position:absolute; }
.track { position:absolute; inset:0; background:#525760; border-radius:99px; transition:background .2s; cursor:pointer; }
.track:before { content:""; position:absolute; width:22px; height:22px; top:4px; left:4px; background:white; border-radius:50%; transition:transform .2s; }
.switch input:checked + .track { background:#ad5555; }
.switch input:checked + .track:before { transform:translateX(24px); }
.switch input:focus-visible + .track { outline:2px solid white; outline-offset:3px; }
.hint { margin:9px 0 0 36px; color:#9298a2; font-size:.88rem; }
#status { min-height:1.3em; margin:18px 0 0; color:#a5a9b0; font-size:.86rem; }
a { color:#cdd2da; text-decoration:none; }
</style>
</head>
<body>
<main>
<header>
<h1>Configuration</h1>
<p class="subtitle">Alfred · Paramètres audio</p>
</header>
<div class="panel">
<section class="section">
<div class="line">
<label class="label" for="volume">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M15.5 8.5a5 5 0 0 1 0 7"/><path d="M19 5a10 10 0 0 1 0 14"/></svg>
Volume de sortie
</label>
<output class="value" id="volumeValue" for="volume">50 %</output>
</div>
<input id="volume" type="range" min="0" max="100" value="50" step="1" aria-label="Volume de sortie">
</section>
<section class="section">
<div class="line">
<span class="label" id="speakerLabel">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="m17 9 5 6m0-6-5 6"/></svg>
Couper le haut-parleur
</span>
<label class="switch"><input type="checkbox" id="speakerMute" aria-labelledby="speakerLabel"><span class="track"></span></label>
</div>
<p class="hint">Désactive les réponses vocales d’Alfred.</p>
</section>
<section class="section">
<div class="line">
<span class="label" id="micLabel">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0M12 17v5m-4 0h8"/></svg>
Couper le microphone
</span>
<label class="switch"><input type="checkbox" id="micMute" aria-labelledby="micLabel"><span class="track"></span></label>
</div>
<p class="hint">Suspend l’écoute et la détection du mot « Alfred ».</p>
</section>
</div>
<p id="status" role="status" aria-live="polite"></p>
</main>
<script>
'use strict';
// API attendue côté Flask :
// GET /api/audio/settings -> {"volume": 50, "speaker_muted": false, "microphone_muted": false}
// PATCH /api/audio/settings <- mêmes clés, partielles acceptées.
// Le navigateur n'agit PAS directement sur ALSA ou Vosk.
const volume = document.getElementById('volume');
const volumeValue = document.getElementById('volumeValue');
const speakerMute = document.getElementById('speakerMute');
const micMute = document.getElementById('micMute');
const status = document.getElementById('status');
let volumeTimer;
function setStatus(message) { status.textContent = message; }
function render(data) {
if (typeof data.volume === 'number') volume.value = data.volume;
if (typeof data.speaker_muted === 'boolean') speakerMute.checked = data.speaker_muted;
if (typeof data.microphone_muted === 'boolean') micMute.checked = data.microphone_muted;
volumeValue.textContent = `${volume.value} %`;
}
async function save(change) {
try {
const response = await fetch('/api/audio/settings', {
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(change)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
render(await response.json());
setStatus('Paramètres appliqués.');
} catch (error) {
setStatus('Impossible d’appliquer le réglage : API Flask non disponible ou erreur serveur.');
console.error(error);
}
}
volume.addEventListener('input', () => {
volumeValue.textContent = `${volume.value} %`;
clearTimeout(volumeTimer);
volumeTimer = setTimeout(() => save({ volume: Number(volume.value) }), 180);
});
speakerMute.addEventListener('change', () => save({ speaker_muted: speakerMute.checked }));
micMute.addEventListener('change', () => save({ microphone_muted: micMute.checked }));
(async () => {
try {
const response = await fetch('/api/audio/settings');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
render(await response.json());
setStatus('');
} catch (_) {
setStatus('Interface prête. Connexion à l’API audio Flask nécessaire pour contrôler le matériel.');
}
})();
</script>
</body>
</html>
@@ -0,0 +1,133 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#202326">
<title>{{ name }}</title>
<link rel="stylesheet" href="/static/css/alfred.css">
<link rel="stylesheet" href="/static/css/alfred-controls.css">
<link rel="stylesheet" href="/static/css/alfred-views.css">
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body>
<main class="alfred-shell">
<header class="topbar">
<!-- Le statut remplace l'ancien titre « Alfred ». -->
<div class="status" role="status" aria-live="polite">
<span id="statusDot" class="status-dot" data-mode="idle" aria-hidden="true"></span>
<span id="mode" data-mode="idle">En attente</span>
</div>
<div class="header-actions" aria-label="Navigation et commandes audio">
<button id="viewHome" class="icon-button" type="button" title="Accueil" aria-label="Accueil">⌂</button>
<button id="viewCalendar" class="icon-button" type="button" title="Agenda" aria-label="Agenda">▦</button>
<button id="viewWeather" class="icon-button" type="button" title="Météo Home Assistant" aria-label="Météo Home Assistant">☀</button>
<button id="viewTemperatures" class="icon-button" type="button" title="Températures" aria-label="Températures">°</button>
<button id="viewCameras" class="icon-button" type="button" title="Caméras Home Assistant" aria-label="Caméras Home Assistant">▣</button>
<button id="audioSettingsButton" class="icon-button" type="button"
aria-label="Réglages audio et caméra" aria-haspopup="dialog" aria-expanded="false"
aria-controls="audioSettingsPanel" title="Réglages audio et caméra">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="2.5" fill="#272b30"/><circle cx="16" cy="17" r="2.5" fill="#272b30"/>
</svg>
</button>
<button id="micMute" class="icon-button" type="button"
aria-label="Couper le microphone" aria-pressed="false" title="Couper le microphone">
<svg class="mic-on-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="2.5" width="6" height="12" rx="3"/>
<path d="M5.5 10.5v1.5a6.5 6.5 0 0 0 13 0v-1.5M12 18.5v3M8 21.5h8"/>
</svg>
<svg class="mic-off-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M9.2 5.3V5.5a3 3 0 0 1 5.5-1.4M15 10.6v.9c0 .5-.1 1-.3 1.5M9 9.5v2A3 3 0 0 0 13.6 14"/>
<path d="M5.5 10.5V12a6.5 6.5 0 0 0 11.5 4.1M12 18.5v3M8 21.5h8M3 3l18 18"/>
</svg>
</button>
<button id="speakerMuteQuick" class="icon-button" type="button"
aria-label="Couper le son" aria-pressed="false" title="Couper le son">
<svg class="speaker-on-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m11 5-5 4H3v6h3l5 4V5Z"/><path d="M15 9a4 4 0 0 1 0 6M18 6a8 8 0 0 1 0 12"/>
</svg>
<svg class="speaker-off-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m11 5-5 4H3v6h3l5 4V5Z"/><path d="m16 9 5 6m0-6-5 6"/>
</svg>
</button>
</div>
</header>
<!-- Pop-up : commandes connectées à /api/audio/settings -->
<div id="audioSettingsPanel" class="audio-popover" role="dialog"
aria-label="Réglages audio et caméra" aria-modal="false" hidden>
<div class="popover-heading">
<strong>Réglages</strong>
<button id="closeAudioSettings" class="icon-button mini" type="button" aria-label="Fermer les réglages">×</button>
</div>
<div class="settings-tabs" role="tablist" aria-label="Catégories de réglages">
<button id="audioTab" class="settings-tab" type="button" role="tab" aria-selected="true" aria-controls="audioSettingsSection">Audio</button>
<button id="cameraTab" class="settings-tab" type="button" role="tab" aria-selected="false" aria-controls="cameraSettingsSection">Caméra</button>
</div>
<div id="audioSettingsSection" role="tabpanel" aria-labelledby="audioTab">
<div class="setting-row">
<label for="volumeSlider">Volume du haut-parleur</label>
<output id="volumeValue" for="volumeSlider">70 %</output>
</div>
<input id="volumeSlider" type="range" min="0" max="100" value="70" step="1">
<div class="setting-row">
<label for="micGainSlider">Sensibilité du microphone</label>
<output id="micGainValue" for="micGainSlider">—</output>
</div>
<input id="micGainSlider" type="range" min="0" max="100" value="50" step="1">
<p id="audioSettingsMessage" class="settings-message" role="status" aria-live="polite"></p>
</div>
<div id="cameraSettingsSection" role="tabpanel" aria-labelledby="cameraTab" hidden>
<p class="camera-settings-note">Webcam locale · réglages matériels V4L2.<br>Tu peux voir le résultat dans l’aperçu « CAMÉRA » en bas à gauche.</p>
<div id="cameraSettingsItems" class="camera-settings-items" aria-live="off"></div>
<div class="camera-settings-actions">
<button id="cameraSettingsReload" class="camera-settings-action" type="button">Actualiser</button>
<button id="cameraSettingsReset" class="camera-settings-action reset" type="button">Valeurs d’usine</button>
</div>
<p id="cameraSettingsMessage" class="settings-message" role="status" aria-live="polite"></p>
</div>
</div>
<section id="stage" aria-label="Réponse d'Alfred">
<div id="content"><h1>Bonjour.</h1><p>À votre écoute.</p></div>
</section>
<section id="wave" aria-hidden="true"><canvas id="waveCanvas"></canvas></section>
<!-- Caméra locale : désactivée tant que l'utilisateur ne l'ouvre pas,
pour éviter de monopoliser la webcam utilisée par le MCP visage. -->
<aside class="camera-panel" aria-label="Prévisualisation caméra">
<div class="panel-heading">
<span class="panel-title"><span class="panel-indicator"></span> CAMÉRA</span>
<button id="cameraToggle" class="panel-action" type="button" aria-pressed="false">Activer</button>
</div>
<div class="camera-viewport">
<img id="cameraFeed" alt="Vidéo en direct de la caméra d'Alfred" hidden>
<div id="cameraPlaceholder" class="camera-placeholder">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true">
<rect x="2.5" y="5" width="14" height="14" rx="3"/><path d="m16.5 10 5-3v10l-5-3"/>
</svg>
<span id="cameraMessage">Caméra désactivée</span>
</div>
</div>
</aside>
<!-- Affiche les résultats partiels ET définitifs de la reconnaissance Vosk. -->
<aside class="transcript-panel" aria-label="Transcription vocale">
<div class="panel-heading">
<span class="panel-title"><span class="panel-indicator stt-indicator"></span> RECONNAISSANCE VOCALE · VOSK</span>
<span id="transcriptState" class="transcript-state">En attente</span>
</div>
<p id="transcriptText" class="transcript-text" aria-live="polite">La dernière phrase reconnue s'affichera ici.</p>
</aside>
</main>
<script src="/static/js/alfred.js"></script>
<script src="/static/js/alfred-views.js"></script>
<script src="/static/js/alfred-controls.js"></script>
<script src="/static/js/alfred-camera.js"></script>
<script src="/static/js/alfred-camera-settings.js"></script>
</body>
</html>
@@ -0,0 +1,31 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{name}}</title>
<link rel="stylesheet" href="/static/css/alfred.css">
<link rel="stylesheet" href="/static/css/alfred-controls.css">
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
</head>
<body>
<main>
<header><span id="name">{{name}}</span><span id="mode">repos</span></header>
<section id="stage"><div id="content"><h1>Bonjour.</h1><p>À votre écoute.</p></div></section>
<section id="wave"><canvas id="waveCanvas"></canvas></section>
<footer class="alfred-controls" aria-label="Contrôles audio">
<button id="speakerMute" class="audio-toggle" type="button" aria-label="Couper le haut-parleur" aria-pressed="false" title="Couper le haut-parleur">🔊</button>
<label class="volume-label" for="volumeSlider">Volume</label>
<input id="volumeSlider" type="range" min="0" max="100" step="1" value="70" aria-label="Volume sonore">
<output id="volumeValue" for="volumeSlider">70 %</output>
<span class="controls-separator" aria-hidden="true"></span>
<label class="volume-label" for="micGainSlider">Sensibilité micro</label>
<input id="micGainSlider" type="range" min="0" max="100" step="1" value="50" aria-label="Sensibilité du microphone">
<output id="micGainValue" for="micGainSlider">—</output>
<button id="micMute" class="audio-toggle" type="button" aria-label="Couper le microphone" aria-pressed="false" title="Couper le microphone">🎙️</button>
</footer>
</main>
<script src="/static/js/alfred.js"></script>
<script src="/static/js/alfred-controls.js"></script>
</body>
</html>
@@ -0,0 +1,2 @@
<!doctype html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{name}}</title><link rel="stylesheet" href="/static/css/alfred.css"><script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script></head>
<body><main><header><span id="name">{{name}}</span><span id="mode">repos</span></header><section id="stage"><div id="content"><h1>Bonjour.</h1><p>À votre écoute.</p></div></section><section id="wave"><canvas id="waveCanvas"></canvas></section></main><script src="/static/js/alfred.js"></script></body></html>