Files
stemify/stemroller/worker_client.py
T
ClericalErrorandClaude Opus 5.5 1845f03c41 StemRoller: desktop stem separator, Android stems player, and server daemons
- stemroller/: PySide6/QML desktop app and player for separating music into stems
- android/: Plex music player that plays per-stem mixes, with Android Auto support,
  a graphic + parametric (Equalizer APO-style) EQ, and offline downloads
- server/: stemserve (serves stems to the app) and stemd (separates the library on
  the server's GPU, with "Stem Now" priority requests)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:17:17 -07:00

113 lines
4.1 KiB
Python

"""UI-side owner of the separation worker process and its socket."""
import json
import os
import sys
from PySide6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, Signal
from PySide6.QtNetwork import QLocalServer, QLocalSocket
class WorkerClient(QObject):
stateChanged = Signal(str) # starting | ready | crashed
hello = Signal(dict)
vram = Signal(str, int, int) # device, free_mb, total_mb
progress = Signal(str, float, str) # id, pct, stage
done = Signal(str, dict)
failed = Signal(str, str) # id, message
cancelled = Signal(str)
crashed = Signal()
def __init__(self, parent=None):
super().__init__(parent)
runtime = os.environ.get("XDG_RUNTIME_DIR") or "/tmp"
self._sock_path = os.path.join(runtime, f"stemroller-{os.getpid()}.sock")
QLocalServer.removeServer(self._sock_path)
self._server = QLocalServer(self)
self._server.newConnection.connect(self._on_connection)
if not self._server.listen(self._sock_path):
raise RuntimeError(f"cannot listen on {self._sock_path}: {self._server.errorString()}")
self._sock: QLocalSocket | None = None
self._buf = b""
self._proc: QProcess | None = None
self._quitting = False
self.state = "starting"
self._restart_delay = 500
def start(self):
self._set_state("starting")
proc = QProcess(self)
env = QProcessEnvironment.systemEnvironment()
pkg_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
env.insert("PYTHONPATH", pkg_root + os.pathsep + env.value("PYTHONPATH", ""))
env.insert("PYTHONUNBUFFERED", "1")
proc.setProcessEnvironment(env)
proc.setProcessChannelMode(QProcess.ProcessChannelMode.ForwardedChannels)
proc.finished.connect(self._on_finished)
proc.start(sys.executable, ["-m", "stemroller.worker", "--socket", self._sock_path])
self._proc = proc
def send(self, type_, **payload):
if self._sock and self._sock.state() == QLocalSocket.LocalSocketState.ConnectedState:
self._sock.write((json.dumps({"type": type_, **payload}) + "\n").encode())
self._sock.flush()
return True
return False
def pid(self):
return int(self._proc.processId()) if self._proc else 0
def shutdown(self):
self._quitting = True
self.send("quit")
if self._proc and self._proc.state() != QProcess.ProcessState.NotRunning:
if not self._proc.waitForFinished(1500):
self._proc.kill()
self._proc.waitForFinished(1000)
self._server.close()
def _set_state(self, s):
self.state = s
self.stateChanged.emit(s)
def _on_connection(self):
sock = self._server.nextPendingConnection()
if self._sock:
self._sock.deleteLater()
self._sock = sock
self._buf = b""
sock.readyRead.connect(self._on_ready_read)
def _on_ready_read(self):
self._buf += bytes(self._sock.readAll())
while b"\n" in self._buf:
line, self._buf = self._buf.split(b"\n", 1)
if line.strip():
self._dispatch(json.loads(line))
def _dispatch(self, msg):
t = msg.pop("type")
if t == "hello":
self._restart_delay = 500
self._set_state("ready")
self.hello.emit(msg)
elif t == "vram":
self.vram.emit(msg["device"], msg["free_mb"], msg["total_mb"])
elif t == "progress":
self.progress.emit(msg["id"], msg["pct"], msg["stage"])
elif t == "done":
self.done.emit(msg.pop("id"), msg)
elif t == "error":
self.failed.emit(msg.get("id") or "", msg.get("msg", "unknown error"))
elif t == "cancelled":
self.cancelled.emit(msg["id"])
def _on_finished(self, code, status):
self._sock = None
if self._quitting:
return
self._set_state("crashed")
self.crashed.emit()
QTimer.singleShot(self._restart_delay, self.start)
self._restart_delay = min(self._restart_delay * 2, 10000)