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.
167 lines
5.3 KiB
Python
167 lines
5.3 KiB
Python
"""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())
|