UNPKG

@p0llen/speaker-detector-client

Version:

React client for interfacing with the [speaker-detector](https://pypi.org/project/speaker-detector) Python backend. Supports real-time speaker identification, meeting transcription, and speaker enrollment.

231 lines (225 loc) 8.3 kB
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator"; import React, { useState, useEffect } from "react"; import { useSpeakerDetection } from "../hooks/useSpeakerDetection"; import useListeningMode from "../hooks/useListeningMode"; import { API_BASE, CLIENT_VERSION } from "../lib/constants"; import MicStatus from "./MicStatus"; // ⬅️ embed the unified status component var SpeakerStatus = function SpeakerStatus() { var { minConfidence, interval } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Live detection stream var { speaker, confidence, isSpeaking, error, status, backendOnline } = useSpeakerDetection({ minConfidence, interval, log: true }); // Backend-controlled settings (via hook) var { mode: listeningMode, setMode: setListeningMode, intervalMs, setIntervalMs, threshold, setThreshold, syncing } = useListeningMode("off"); // Local UI state var [speakers, setSpeakers] = useState(null); var [speakersError, setSpeakersError] = useState(null); var [backendVersion, setBackendVersion] = useState(null); var [defaults, setDefaults] = useState(null); // --- Fetch helpers --- var fetchSpeakers = () => { setSpeakersError(null); fetch("".concat(API_BASE, "/api/speakers")).then(res => { if (!res.ok) throw new Error("Speakers API failed"); return res.json(); }).then(data => setSpeakers(Array.isArray(data) ? data : [])).catch(err => { console.error("❌ Failed to fetch speakers:", err); setSpeakers(null); setSpeakersError("Failed to load speakers"); }); }; var fetchBackendVersion = () => { fetch("".concat(API_BASE, "/api/version")).then(res => res.json()).then(data => setBackendVersion(data.version)).catch(() => setBackendVersion(null)); }; // Hydrate current settings & defaults from backend constants var fetchListeningMeta = /*#__PURE__*/function () { var _ref = _asyncToGenerator(function* () { try { var res = yield fetch("".concat(API_BASE, "/api/listening-mode"), { cache: "no-store" }); if (!res.ok) throw new Error("listening-mode GET failed"); var json = yield res.json(); if (json.defaults && typeof json.defaults === "object") { setDefaults({ threshold: typeof json.defaults.threshold === "number" ? json.defaults.threshold : threshold, interval_ms: typeof json.defaults.interval_ms === "number" ? json.defaults.interval_ms : intervalMs }); } else { setDefaults({ threshold, interval_ms: intervalMs }); } } catch (_unused) { setDefaults({ threshold, interval_ms: intervalMs }); } }); return function fetchListeningMeta() { return _ref.apply(this, arguments); }; }(); useEffect(() => { fetchSpeakers(); fetchBackendVersion(); fetchListeningMeta(); }, []); var resetToDefaults = () => { if (!defaults) return; setThreshold(defaults.threshold); setIntervalMs(defaults.interval_ms); }; var backendLabel = backendOnline === null ? "checking…" : backendOnline ? "online" : "unreachable"; // If backend is truly unreachable and the hook flagged error, show a compact error if (error && backendOnline === false) { return /*#__PURE__*/React.createElement("p", { className: "speaker-error" }, "\u274C ", error); } return /*#__PURE__*/React.createElement("div", { className: "speaker-status-panel" }, /*#__PURE__*/React.createElement("h4", { className: "speaker-heading" }, "\uD83D\uDD0A Speaker Status"), /*#__PURE__*/React.createElement("p", { style: { marginBottom: 8 } }, /*#__PURE__*/React.createElement("strong", null, "Backend:"), " ", backendLabel, backendVersion && /*#__PURE__*/React.createElement(React.Fragment, null, " ", "\xA0|\xA0 ", /*#__PURE__*/React.createElement("strong", null, "Backend v"), backendVersion), "\xA0|\xA0 ", /*#__PURE__*/React.createElement("strong", null, "Client v"), CLIENT_VERSION), /*#__PURE__*/React.createElement(MicStatus, { backendOnline: backendOnline, status: status, listeningMode: listeningMode, isSpeaking: isSpeaking }), /*#__PURE__*/React.createElement("div", { style: { marginTop: 12, marginBottom: 12 } }, /*#__PURE__*/React.createElement("label", null, "\uD83C\uDF9A Listening Mode:\xA0", /*#__PURE__*/React.createElement("select", { value: listeningMode, onChange: e => setListeningMode(e.target.value), style: { background: "#222", color: "#fff", borderRadius: 4, padding: "4px 6px", border: "1px solid #555" } }, /*#__PURE__*/React.createElement("option", { value: "off" }, "\uD83D\uDD07 Off"), /*#__PURE__*/React.createElement("option", { value: "single" }, "\uD83E\uDDCD Single"), /*#__PURE__*/React.createElement("option", { value: "multi" }, "\uD83D\uDC65 Multi")))), /*#__PURE__*/React.createElement("div", { style: { marginBottom: 10 } }, /*#__PURE__*/React.createElement("label", null, "\u23F1 Interval (ms):", /*#__PURE__*/React.createElement("input", { type: "range", min: "500", max: "5000", step: "100", value: intervalMs, onChange: e => setIntervalMs(Number(e.target.value)), style: { width: 260, marginLeft: 8 } }), /*#__PURE__*/React.createElement("span", { style: { marginLeft: 8 } }, intervalMs, " ms")), defaults && /*#__PURE__*/React.createElement("div", { style: { fontSize: 12, color: "#999", marginTop: 4 } }, "Default: ", defaults.interval_ms, " ms")), /*#__PURE__*/React.createElement("div", { style: { marginBottom: 10 } }, /*#__PURE__*/React.createElement("label", null, "\uD83E\uDDE0 Threshold:", /*#__PURE__*/React.createElement("input", { type: "range", min: "0", max: "1", step: "0.01", value: threshold, onChange: e => setThreshold(parseFloat(e.target.value)), style: { width: 260, marginLeft: 8 } }), /*#__PURE__*/React.createElement("span", { style: { marginLeft: 8 } }, threshold.toFixed(2))), defaults && /*#__PURE__*/React.createElement("div", { style: { fontSize: 12, color: "#999", marginTop: 4 } }, "Default: ", defaults.threshold)), /*#__PURE__*/React.createElement("div", { style: { margin: "6px 0 14px" } }, /*#__PURE__*/React.createElement("button", { onClick: resetToDefaults, style: { background: "#2a2a2a", color: "#fff", border: "1px solid #555", borderRadius: 6, padding: "6px 10px", cursor: "pointer" }, title: defaults ? "Reset to threshold ".concat(defaults.threshold, ", interval ").concat(defaults.interval_ms, "ms") : "Reset to defaults" }, "\u267B\uFE0F Reset to defaults"), syncing && /*#__PURE__*/React.createElement("span", { style: { marginLeft: 10, fontStyle: "italic", color: "#999" } }, "\uD83D\uDD04 Syncing settings\u2026")), /*#__PURE__*/React.createElement("p", { style: { marginTop: 6 } }, /*#__PURE__*/React.createElement("strong", null, "Detected Speaker:"), " ", speaker || "None", " ", /*#__PURE__*/React.createElement("br", null), /*#__PURE__*/React.createElement("strong", null, "Confidence:"), " ", confidence !== null ? "".concat(Math.round(confidence * 100), "%") : "N/A"), /*#__PURE__*/React.createElement("hr", null), /*#__PURE__*/React.createElement("h5", null, "\uD83E\uDDE0 Enrolled Speakers"), speakersError ? /*#__PURE__*/React.createElement("p", { style: { color: "#f55" } }, "\u26A0\uFE0F ", speakersError) : speakers === null ? /*#__PURE__*/React.createElement("p", { style: { fontStyle: "italic", color: "#aaa" } }, "Loading...") : speakers.length === 0 ? /*#__PURE__*/React.createElement("p", null, "No speakers enrolled yet.") : /*#__PURE__*/React.createElement("ul", null, speakers.map(s => /*#__PURE__*/React.createElement("li", { key: s.name }, s.name, " (", s.recordings, ")")))); }; export default SpeakerStatus;