Alfred/templates/index.html
2025-11-11 13:38:25 +01:00

246 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>Caméra USB Détection, Volume, TTS & STT (Vosk)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
html,body { margin:0; padding:0; background:#111; color:#eee; font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif; }
header { padding:12px 16px; background:#1b1b1b; position:sticky; top:0 }
.wrap { max-width:1000px; margin:0 auto; padding:16px }
.video { display:block; width:100%; height:auto; border-radius:16px; box-shadow:0 6px 24px rgba(0,0,0,.35); background:#000 }
.row { display:flex; gap:12px; flex-wrap:wrap; margin-top:12px; align-items:center }
.btn { background:#2c7be5; border:none; color:#fff; padding:10px 14px; border-radius:10px; cursor:pointer; text-decoration:none }
.btn.secondary { background:#444 }
.btn:active { transform:translateY(1px) }
.card { background:#151515; border-radius:16px; padding:14px; box-shadow:0 10px 30px rgba(0,0,0,.35); }
.slider { width:240px }
.badge { font-size:.85rem; opacity:.85; }
.muted { color:#f39; }
.pill { padding:4px 8px; border-radius:999px; background:#222; }
input[type="text"] { background:#1e1e1e; color:#fff; border:1px solid #333; border-radius:10px; padding:10px 12px; min-width:280px }
.grow { flex:1 1 auto; min-width:220px }
textarea { width:100%; min-height:90px; background:#1a1a1a; color:#ddd; border:1px solid #333; border-radius:12px; padding:10px; }
</style>
</head>
<body>
<header><strong>Diffusion Caméra USB</strong></header>
<div class="wrap">
<!-- Ligne actions au-dessus de la vidéo -->
<div class="row" style="margin-top:2px">
<a class="btn" href="{{ url_for('snapshot') }}" target="_blank">📸 Snapshot</a>
<button id="btnToggleDetect" class="btn">{{ '🟢 Détection ON' if detect_enabled else '⚪ Détection OFF' }}</button>
<button id="btnToggleFps" class="btn secondary">{{ '🙈 Cacher FPS' if show_fps else '👁️ Afficher FPS' }}</button>
<span class="pill">Détecteur: <strong id="detBackend">{{ detector_backend }}</strong></span>
<span class="pill">Status: <strong id="detStatus">{{ 'ON' if detect_enabled else 'OFF' }}</strong></span>
<span class="pill">FPS: <strong id="fpsText">-- / --</strong></span>
</div>
<!-- Vidéo -->
<img class="video" src="{{ url_for('video_feed') }}" alt="Flux caméra (MJPEG)" />
<!-- Volume -->
<div class="row" style="margin-top:18px">
<div class="card" style="flex:1 1 100%">
<div class="row">
<button id="volDown" class="btn" title="-5%"></button>
<input id="volSlider" class="slider" type="range" min="0" max="150" step="1" />
<button id="volUp" class="btn" title="+5%"></button>
<button id="volMute" class="btn">🔇 Mute</button>
<span class="badge">backend: <code id="backend">{{ audio_backend }}</code></span>
<span id="mutedTag" class="badge muted" style="display:none">[muet]</span>
</div>
<!-- TTS + STT SOUS le slider -->
<div class="row" style="margin-top:14px">
<!-- TTS -->
<input id="ttsText" class="grow" type="text" placeholder="Texte à lire…" />
<button id="btnSay" class="btn">🗣️ Lire</button>
<span class="pill">TTS: <strong id="ttsBackend">{{ tts_backend }}</strong></span>
{% if tts_backend == 'piper' %}
<span class="pill">Voix: <strong>{{ piper_model }}</strong></span>
{% endif %}
</div>
<!-- STT (Vosk) -->
<div class="row">
<button id="btnSttStart" class="btn">🎙️ Démarrer STT</button>
<button id="btnSttStop" class="btn secondary">⏹️ Stop</button>
<span class="pill">Vosk: <strong id="sttModel">{{ stt_model }}</strong></span>
<span class="pill">Écoute: <strong id="sttState">OFF</strong></span>
</div>
<div style="margin-top:8px">
<div class="badge">Partiel :</div>
<textarea id="sttPartial" readonly></textarea>
<div class="badge" style="margin-top:8px">Dernier final :</div>
<textarea id="sttFinal" readonly></textarea>
</div>
</div>
</div>
</div>
<script>
// -------- Volume UI --------
const slider = document.getElementById('volSlider');
const labelBackend = document.getElementById('backend');
const mutedTag = document.getElementById('mutedTag');
const btnUp = document.getElementById('volUp');
const btnDown = document.getElementById('volDown');
const btnMute = document.getElementById('volMute');
let vol = {{ initial_volume|int }};
let muted = {{ initial_muted|tojson }};
function renderVolume() {
slider.value = vol;
mutedTag.style.display = (muted === true) ? 'inline' : 'none';
}
async function refreshVolume() {
try {
const r = await fetch('{{ url_for("api_get_volume") }}');
const j = await r.json();
if (typeof j.volume === 'number') vol = j.volume;
if (typeof j.muted === 'boolean') muted = j.muted;
if (j.backend) labelBackend.textContent = j.backend;
renderVolume();
} catch(e) {}
}
slider.addEventListener('change', async () => {
const val = parseInt(slider.value);
const r = await fetch('{{ url_for("api_set_volume") }}?level=' + val, { method:'POST' });
const j = await r.json();
vol = (typeof j.volume === 'number') ? j.volume : val;
muted = (typeof j.muted === 'boolean') ? j.muted : muted;
renderVolume();
});
btnUp.addEventListener('click', async () => {
const r = await fetch('{{ url_for("api_volume_up") }}', { method:'POST' });
const j = await r.json();
if (typeof j.volume === 'number') vol = j.volume;
if (typeof j.muted === 'boolean') muted = j.muted;
renderVolume();
});
btnDown.addEventListener('click', async () => {
const r = await fetch('{{ url_for("api_volume_down") }}', { method:'POST' });
const j = await r.json();
if (typeof j.volume === 'number') vol = j.volume;
if (typeof j.muted === 'boolean') muted = j.muted;
renderVolume();
});
btnMute.addEventListener('click', async () => {
const r = await fetch('{{ url_for("api_volume_mute") }}', { method:'POST' });
const j = await r.json();
if (typeof j.volume === 'number') vol = j.volume;
if (typeof j.muted === 'boolean') muted = j.muted;
renderVolume();
});
// -------- Détection / FPS UI --------
const detBackend = document.getElementById('detBackend');
const detStatus = document.getElementById('detStatus');
const btnToggleDetect = document.getElementById('btnToggleDetect');
const btnToggleFps = document.getElementById('btnToggleFps');
const fpsText = document.getElementById('fpsText');
async function refreshStats() {
try {
const r = await fetch('/stats');
const j = await r.json();
detBackend.textContent = j.backend || 'none';
detStatus.textContent = j.detect_enabled ? 'ON' : 'OFF';
fpsText.textContent = (j.cam_fps?.toFixed ? j.cam_fps.toFixed(1) : j.cam_fps) + ' / ' +
(j.infer_fps?.toFixed ? j.infer_fps.toFixed(1) : j.infer_fps);
btnToggleDetect.textContent = j.detect_enabled ? '🟢 Détection ON' : '⚪ Détection OFF';
btnToggleFps.textContent = j.show_fps ? '🙈 Cacher FPS' : '👁️ Afficher FPS';
} catch(e) {}
}
btnToggleDetect.addEventListener('click', async () => {
const r = await fetch('/detect/toggle', { method:'POST' });
await r.json();
refreshStats();
});
btnToggleFps.addEventListener('click', async () => {
const r0 = await fetch('/stats');
const s = await r0.json();
const desired = !(s.show_fps === true);
const r = await fetch('/fps/show?state=' + (desired ? 'true' : 'false'), { method:'POST' });
await r.json();
refreshStats();
});
// -------- TTS UI --------
const ttsInput = document.getElementById('ttsText');
const btnSay = document.getElementById('btnSay');
const ttsBackend = document.getElementById('ttsBackend');
btnSay.addEventListener('click', async () => {
const text = (ttsInput.value || '').trim();
if (!text) return;
btnSay.disabled = true;
try {
const r = await fetch('/tts/say', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
const j = await r.json();
if (!j.ok) {
alert('TTS erreur (' + (j.backend || 'none') + '): ' + (j.error || 'inconnue'));
} else {
if (j.backend && j.backend !== ttsBackend.textContent) {
ttsBackend.textContent = j.backend;
}
}
} catch (e) {
alert('Erreur TTS: ' + e);
} finally {
btnSay.disabled = false;
}
});
// -------- STT (Vosk) UI --------
const btnSttStart = document.getElementById('btnSttStart');
const btnSttStop = document.getElementById('btnSttStop');
const sttModel = document.getElementById('sttModel');
const sttState = document.getElementById('sttState');
const sttPartial = document.getElementById('sttPartial');
const sttFinal = document.getElementById('sttFinal');
async function refreshStt() {
try {
const r = await fetch('/stt/status');
const j = await r.json();
sttState.textContent = j.listening ? 'ON' : 'OFF';
sttPartial.value = j.partial || '';
if (j.last_final) sttFinal.value = j.last_final;
if (j.model && j.model !== sttModel.textContent) sttModel.textContent = j.model.split('/').pop();
if (j.error) sttPartial.value = '[Erreur] ' + j.error + '\n' + (sttPartial.value || '');
} catch(e) {}
}
btnSttStart.addEventListener('click', async () => {
btnSttStart.disabled = true;
try {
await fetch('/stt/start', { method:'POST' });
setTimeout(refreshStt, 300);
} finally {
btnSttStart.disabled = false;
}
});
btnSttStop.addEventListener('click', async () => {
await fetch('/stt/stop', { method:'POST' });
refreshStt();
});
// init
renderVolume();
refreshStats();
refreshStt();
setInterval(refreshVolume, 3000);
setInterval(refreshStats, 1000);
setInterval(refreshStt, 800);
</script>
</body>
</html>