diff --git a/ttstoy/ttstoy.py b/ttstoy/ttstoy.py index 53fa26a..257a677 100644 --- a/ttstoy/ttstoy.py +++ b/ttstoy/ttstoy.py @@ -443,9 +443,8 @@ class TtsToy(Cog): if self._webui_state_processor.is_running(): self._webui_state_processor.stop() self._kill_leftover_webui() - await self._stop_dectalk_server() - await self._stop_morshu_server() - await self._stop_vox_server() + # TTS services (dectalk/morshu/vox) now run as independent systemd + # services and are no longer started or stopped by this cog. if self._health_server: self._health_server.shutdown() @@ -527,174 +526,104 @@ class TtsToy(Cog): return False async def _start_dectalk_server(self) -> bool: - """Start the built-in DECTalk server.""" + """Check whether the independent DECTalk service is reachable. + + The DECTalk server now runs as a standalone systemd service + (dectalk-tts-server) and is no longer spawned by the bot. This method + only confirms the service is up on its fixed port. + """ try: - # Check if already running - if self.dectalk_process and self.dectalk_process.poll() is None: - log.info("DECTalk server already running") - return True - - if not self.dectalk_dir.exists(): - log.error(f"DECTalk server directory not found: {self.dectalk_dir}") - return False - - server_js = self.dectalk_dir / "server.js" - if not server_js.exists(): - log.error("server.js not found, DECTalk server not installed") - return False - - # Check if node_modules exists - node_modules = self.dectalk_dir / "node_modules" - if not node_modules.exists(): - log.info("node_modules not found, installing DECTalk server...") - if not await self._install_dectalk_server(): + def _check(): + try: + r = requests.get( + f"http://127.0.0.1:{self.dectalk_port}/health", timeout=5 + ) + return r.status_code == 200 + except Exception: return False - - # Start the server - env = os.environ.copy() - env['PORT'] = str(self.dectalk_port) - - self.dectalk_process = await asyncio.to_thread( - subprocess.Popen, - ["node", "server.js"], - cwd=str(self.dectalk_dir), - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - - # Wait a moment for server to start - await asyncio.sleep(2) - - # Check if it's still running - if self.dectalk_process.poll() is not None: - log.error("DECTalk server failed to start") - return False - - log.info(f"DECTalk server started on port {self.dectalk_port}") - return True + + reachable = await asyncio.to_thread(_check) + if reachable: + log.info(f"DECTalk service reachable on port {self.dectalk_port}") + else: + log.warning( + f"DECTalk service not reachable on port {self.dectalk_port}. " + "Ensure the dectalk-tts-server systemd service is running." + ) + return reachable except Exception as e: - log.exception(f"Failed to start DECTalk server: {e}") + log.exception(f"Failed to check DECTalk service: {e}") return False async def _stop_dectalk_server(self): - """Stop the built-in DECTalk server.""" - if self.dectalk_process and self.dectalk_process.poll() is None: - try: - self.dectalk_process.terminate() - await asyncio.sleep(1) - if self.dectalk_process.poll() is None: - self.dectalk_process.kill() - log.info("DECTalk server stopped") - except Exception as e: - log.exception(f"Error stopping DECTalk server: {e}") + """No-op. The DECTalk service is managed by systemd, not the bot.""" + return async def _start_morshu_server(self) -> tuple: - """Start the built-in Morshu TTS server. Returns (success, error_msg).""" + """Check whether the independent Morshu service is reachable. + + The Morshu server now runs as a standalone systemd service + (morshu-tts-server) and is no longer spawned by the bot. Returns + (reachable, error_msg). + """ try: - if self.morshu_process and self.morshu_process.poll() is None: - log.info("Morshu server already running") + def _check(): + try: + r = requests.get( + f"http://127.0.0.1:{self.morshu_port}/health", timeout=5 + ) + return r.status_code == 200 + except Exception: + return False + + reachable = await asyncio.to_thread(_check) + if reachable: + log.info(f"Morshu service reachable on port {self.morshu_port}") return True, None - - if not self.morshu_dir.exists(): - return False, f"Directory not found: {self.morshu_dir}" - - server_py = self.morshu_dir / "server.py" - if not server_py.exists(): - return False, f"server.py not found: {server_py}" - - env = os.environ.copy() - env['PORT'] = str(self.morshu_port) - - self.morshu_process = await asyncio.to_thread( - subprocess.Popen, - [sys.executable, str(server_py)], - cwd=str(self.morshu_dir), - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True + return False, ( + f"Morshu service not reachable on port {self.morshu_port}. " + "Ensure the morshu-tts-server systemd service is running." ) - - # Wait for server to start - await asyncio.sleep(3) - - # Check if it's still running - if self.morshu_process.poll() is not None: - stderr = self.morshu_process.stderr.read() - stdout = self.morshu_process.stdout.read() - err = stderr or stdout or "Unknown error" - log.error(f"Morshu server failed (exit {self.morshu_process.returncode}): {err}") - return False, err - - log.info(f"Morshu server started on port {self.morshu_port}") - return True, None except Exception as e: - log.exception(f"Failed to start Morshu server: {e}") + log.exception(f"Failed to check Morshu service: {e}") return False, str(e) async def _stop_morshu_server(self): - """Stop the built-in Morshu server.""" - if self.morshu_process and self.morshu_process.poll() is None: - try: - self.morshu_process.terminate() - await asyncio.sleep(1) - if self.morshu_process.poll() is None: - self.morshu_process.kill() - log.info("Morshu server stopped") - except Exception as e: - log.exception(f"Error stopping Morshu server: {e}") + """No-op. The Morshu service is managed by systemd, not the bot.""" + return async def _start_vox_server(self) -> tuple: - """Start the built-in VOX TTS server. Returns (success, error_msg).""" + """Check whether the independent VOX service is reachable. + + The VOX server now runs as a standalone systemd service + (vox-tts-server) and is no longer spawned by the bot. Returns + (reachable, error_msg). + """ try: - if self.vox_process and self.vox_process.poll() is None: + def _check(): + try: + r = requests.get( + f"http://127.0.0.1:{self.vox_port}/health", timeout=5 + ) + return r.status_code == 200 + except Exception: + return False + + reachable = await asyncio.to_thread(_check) + if reachable: + log.info(f"VOX service reachable on port {self.vox_port}") return True, None - - server_py = self.vox_dir / "server.py" - if not server_py.exists(): - return False, f"server.py not found: {server_py}" - - env = os.environ.copy() - env['PORT'] = str(self.vox_port) - - self.vox_process = await asyncio.to_thread( - subprocess.Popen, - [sys.executable, str(server_py)], - cwd=str(self.vox_dir), - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True + return False, ( + f"VOX service not reachable on port {self.vox_port}. " + "Ensure the vox-tts-server systemd service is running." ) - - await asyncio.sleep(2) - - if self.vox_process.poll() is not None: - stderr = self.vox_process.stderr.read() - stdout = self.vox_process.stdout.read() - err = stderr or stdout or "Unknown error" - log.error(f"VOX server failed (exit {self.vox_process.returncode}): {err}") - return False, err - - log.info(f"VOX server started on port {self.vox_port}") - return True, None except Exception as e: - log.exception(f"Failed to start VOX server: {e}") + log.exception(f"Failed to check VOX service: {e}") return False, str(e) async def _stop_vox_server(self): - """Stop the built-in VOX server.""" - if self.vox_process and self.vox_process.poll() is None: - try: - self.vox_process.terminate() - await asyncio.sleep(1) - if self.vox_process.poll() is None: - self.vox_process.kill() - log.info("VOX server stopped") - except Exception as e: - log.exception(f"Error stopping VOX server: {e}") + """No-op. The VOX service is managed by systemd, not the bot.""" + return async def _get_effective_minimax_voice(self, user: discord.abc.User) -> str: mode = await self._get_tts_mode() @@ -2185,12 +2114,12 @@ class TtsToy(Cog): @ttstoy_group.command(name="dectalkstop") @commands.is_owner() async def dectalk_stop(self, ctx: commands.Context): - """Stop the built-in DECTalk server.""" - if not self.dectalk_process or self.dectalk_process.poll() is not None: - return await ctx.send("❌ DECTalk server is not running") - - await self._stop_dectalk_server() - await ctx.send("✅ DECTalk server stopped") + """The DECTalk server is managed by systemd, not the bot.""" + await ctx.send( + "The DECTalk server now runs as an independent systemd service " + "(dectalk-tts-server). Manage it with: " + "`sudo systemctl stop dectalk-tts-server`" + ) @ttstoy_group.command(name="dectalkstatus") @commands.is_owner() @@ -2204,9 +2133,7 @@ class TtsToy(Cog): node_modules = self.dectalk_dir / "node_modules" installed = server_js.exists() and node_modules.exists() - # Check if process is running - process_running = self.dectalk_process and self.dectalk_process.poll() is None - + # The service is managed by systemd; report API reachability. # Check if API is responding api_responding = False try: @@ -2219,17 +2146,17 @@ class TtsToy(Cog): lines = [ "**DECTalk Server Status**", f"Installed: {'✅ Yes' if installed else '❌ No'}", - f"Process Running: {'✅ Yes' if process_running else '❌ No'}", f"API Responding: {'✅ Yes' if api_responding else '❌ No'}", f"Auto-start: {'✅ Enabled' if auto_start else '❌ Disabled'}", f"URL: `{dectalk_url}`", "", ] - if not installed: - lines.append(f"Install with: `{ctx.clean_prefix}ttstoy dectalkinstall`") - elif not process_running: - lines.append(f"Start with: `{ctx.clean_prefix}ttstoy dectalkstart`") + if not api_responding: + lines.append( + "Service managed by systemd. Start with: " + "`sudo systemctl start dectalk-tts-server`" + ) await ctx.send("\n".join(lines)) @@ -2269,19 +2196,17 @@ class TtsToy(Cog): @ttstoy_group.command(name="morshustop") @commands.is_owner() async def morshu_stop(self, ctx: commands.Context): - """Stop the built-in Morshu server.""" - if not self.morshu_process or self.morshu_process.poll() is not None: - return await ctx.send("❌ Morshu server is not running") - - await self._stop_morshu_server() - await ctx.send("✅ Morshu server stopped") + """The Morshu server is managed by systemd, not the bot.""" + await ctx.send( + "The Morshu server now runs as an independent systemd service " + "(morshu-tts-server). Manage it with: " + "`sudo systemctl stop morshu-tts-server`" + ) @ttstoy_group.command(name="morshustatus") @commands.is_owner() async def morshu_status(self, ctx: commands.Context): """Check Morshu server status.""" - process_running = self.morshu_process and self.morshu_process.poll() is None - api_responding = False try: r = requests.get(f"http://127.0.0.1:{self.morshu_port}/health", timeout=2) @@ -2291,13 +2216,15 @@ class TtsToy(Cog): lines = [ "**Morshu Server Status**", - f"Process Running: {'✅ Yes' if process_running else '❌ No'}", f"API Responding: {'✅ Yes' if api_responding else '❌ No'}", f"URL: `http://127.0.0.1:{self.morshu_port}`", ] - if not process_running: - lines.append(f"\nStart with: `{ctx.clean_prefix}ttstoy morshustart`") + if not api_responding: + lines.append( + "\nService managed by systemd. Start with: " + "`sudo systemctl start morshu-tts-server`" + ) await ctx.send("\n".join(lines)) @@ -2321,19 +2248,17 @@ class TtsToy(Cog): @ttstoy_group.command(name="voxstop") @commands.is_owner() async def vox_stop(self, ctx: commands.Context): - """Stop the built-in VOX server.""" - if not self.vox_process or self.vox_process.poll() is not None: - return await ctx.send("❌ VOX server is not running") - - await self._stop_vox_server() - await ctx.send("✅ VOX server stopped") + """The VOX server is managed by systemd, not the bot.""" + await ctx.send( + "The VOX server now runs as an independent systemd service " + "(vox-tts-server). Manage it with: " + "`sudo systemctl stop vox-tts-server`" + ) @ttstoy_group.command(name="voxstatus") @commands.is_owner() async def vox_status(self, ctx: commands.Context): """Check VOX server status.""" - process_running = self.vox_process and self.vox_process.poll() is None - api_responding = False try: r = requests.get(f"http://127.0.0.1:{self.vox_port}/health", timeout=2) @@ -2343,13 +2268,15 @@ class TtsToy(Cog): lines = [ "**VOX Server Status**", - f"Process Running: {'✅ Yes' if process_running else '❌ No'}", f"API Responding: {'✅ Yes' if api_responding else '❌ No'}", f"URL: `http://127.0.0.1:{self.vox_port}`", ] - if not process_running: - lines.append(f"\nStart with: `{ctx.clean_prefix}ttstoy voxstart`") + if not api_responding: + lines.append( + "\nService managed by systemd. Start with: " + "`sudo systemctl start vox-tts-server`" + ) await ctx.send("\n".join(lines)) @@ -3426,17 +3353,9 @@ class TtsToy(Cog): async def cog_load(self): """Called when cog is loaded.""" - mode = await self._get_tts_mode() - auto_start = await self.config.dectalk_auto_start() - - if auto_start: - server_js = self.dectalk_dir / "server.js" - node_modules = self.dectalk_dir / "node_modules" - if server_js.exists() and node_modules.exists(): - log.info("DECTalk auto-start: server is installed, starting...") - await self._start_dectalk_server() - elif mode == "dectalk": - log.warning("DECTalk mode active but server not installed. Run [p]ttstoy dectalkinstall") + # The dectalk/morshu/vox TTS services now run as independent systemd + # services (managed outside the bot), so the cog no longer starts them. + # It simply connects to them over HTTP on their fixed ports. # Start health HTTP server try: