246 lines
11 KiB
HTML
246 lines
11 KiB
HTML
<!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>
|