feat: point the fleet at wss LiveKit room uwh-telhai
Publish and subscribe over wss://livekit.uni-wh.de:7800 and refuse cleartext ws://. Conference room is uwh-telhai. Includes the uncommitted encoded H.264 publish path, Rally hairpin, and KMS wall overlay.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""Shared MetricGAN daemon: one model, many camera clients."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from enhance_ipc import read_frame, write_frame
|
||||
|
||||
log = logging.getLogger("audio.enhance_daemon")
|
||||
|
||||
Handler = Callable[[str, np.ndarray], np.ndarray]
|
||||
|
||||
|
||||
class SharedEnhanceEngine:
|
||||
"""One AudioCleaner model; per-identity context and TDOA."""
|
||||
|
||||
def __init__(self, make_cleaner: Callable):
|
||||
self._template = make_cleaner()
|
||||
self._sessions: dict[str, object] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def identities(self) -> list[str]:
|
||||
return list(self._sessions)
|
||||
|
||||
def _session(self, ident: str):
|
||||
sess = self._sessions.get(ident)
|
||||
if sess is not None:
|
||||
return sess
|
||||
sess = self._clone()
|
||||
self._sessions[ident] = sess
|
||||
return sess
|
||||
|
||||
def _clone(self):
|
||||
import copy
|
||||
sess = copy.copy(self._template)
|
||||
sess._ctx = np.zeros(0, dtype=np.float32)
|
||||
sess._last_tdoas = None
|
||||
sess._tdoa_age = 0
|
||||
sess._client = None
|
||||
sess.last_dt_ms = -1.0
|
||||
compiled = getattr(sess, "_ov_compiled", None)
|
||||
if compiled is not None:
|
||||
sess._ov_request = compiled.create_infer_request()
|
||||
return sess
|
||||
|
||||
def process(self, ident: str, pcm: np.ndarray) -> np.ndarray:
|
||||
with self._lock:
|
||||
sess = self._session(ident)
|
||||
return sess.process(pcm)
|
||||
|
||||
|
||||
def serve_unix(path: str, handler: Handler, stop: Optional[threading.Event] = None) -> None:
|
||||
"""Accept unix-socket clients; handler(ident, pcm) -> int16 mono."""
|
||||
if os.path.exists(path):
|
||||
os.unlink(path)
|
||||
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
||||
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
srv.bind(path)
|
||||
srv.listen(64)
|
||||
srv.settimeout(0.2)
|
||||
|
||||
def _client(conn: socket.socket) -> None:
|
||||
try:
|
||||
while stop is None or not stop.is_set():
|
||||
ident, pcm = read_frame(conn)
|
||||
out = handler(ident, pcm)
|
||||
out = np.asarray(out, dtype=np.int16)
|
||||
write_frame(conn, ident, out)
|
||||
except (EOFError, ConnectionError, BrokenPipeError, TimeoutError, OSError):
|
||||
pass
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
threads: list[threading.Thread] = []
|
||||
try:
|
||||
while stop is None or not stop.is_set():
|
||||
try:
|
||||
conn, _ = srv.accept()
|
||||
except TimeoutError:
|
||||
continue
|
||||
conn.settimeout(15.0)
|
||||
t = threading.Thread(target=_client, args=(conn,), daemon=True)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
finally:
|
||||
srv.close()
|
||||
if os.path.exists(path):
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def wait_for_socket(path: str, timeout: float = 90.0) -> bool:
|
||||
"""Return True once a unix socket at path accepts a connection."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
s.settimeout(0.3)
|
||||
s.connect(path)
|
||||
s.close()
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
time.sleep(0.05)
|
||||
return False
|
||||
|
||||
|
||||
def make_production_cleaner(**kw):
|
||||
from audio_cleanup import AudioCleaner, apply_torch_thread_limits
|
||||
|
||||
threads = int(kw.pop("torch_num_threads", 2))
|
||||
apply_torch_thread_limits(threads)
|
||||
return AudioCleaner(torch_num_threads=threads, **kw)
|
||||
|
||||
|
||||
def main(argv: Optional[list[str]] = None) -> int:
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Shared MetricGAN enhance daemon")
|
||||
ap.add_argument("--socket", default="/tmp/livekit-enhance.sock")
|
||||
ap.add_argument("--enhance-mode", default="force")
|
||||
ap.add_argument("--enhance-model",
|
||||
default="speechbrain/metricgan-plus-voicebank")
|
||||
ap.add_argument("--enhance-chunk-s", type=float, default=1.0)
|
||||
ap.add_argument("--enhance-hop-s", type=float, default=0.1)
|
||||
ap.add_argument("--enhance-infer", default="auto")
|
||||
ap.add_argument("--beamform-mode", default="auto")
|
||||
ap.add_argument("--torch-num-threads", type=int, default=2)
|
||||
ap.add_argument("--tdoa-every", type=int, default=5)
|
||||
ap.add_argument("--audio-rate", type=int, default=16000)
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [enhance-daemon] %(levelname)s %(message)s")
|
||||
log.info("loading shared MetricGAN (one copy for all cameras)")
|
||||
|
||||
def make():
|
||||
return make_production_cleaner(
|
||||
mode=args.enhance_mode,
|
||||
model_source=args.enhance_model,
|
||||
sample_rate=args.audio_rate,
|
||||
chunk_s=args.enhance_chunk_s,
|
||||
hop_s=args.enhance_hop_s,
|
||||
beamform=args.beamform_mode,
|
||||
torch_num_threads=args.torch_num_threads,
|
||||
tdoa_every=args.tdoa_every,
|
||||
enhance_infer=args.enhance_infer,
|
||||
)
|
||||
|
||||
engine = SharedEnhanceEngine(make)
|
||||
log.info("model ready backend=%s socket=%s",
|
||||
getattr(engine._template, "backend", "?"), args.socket)
|
||||
serve_unix(args.socket, engine.process)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user