@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
JavaScript
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;