From a2099e9d81ee01a44e832f7be22c2f33488c1b49 Mon Sep 17 00:00:00 2001 From: Jezza Hehn Date: Fri, 10 Apr 2026 01:43:02 +0000 Subject: [PATCH] Strip Jarvis/Sage personas, simplify to MoltMic pipe - Replace /jarvis and /sage command groups with /moltmic join|leave|status - Remove AgentVoiceConfig, AgentsConfig now just has default agent - Remove voice file checks from run.py (cloud TTS doesn't need them) - Remove agent-to-voice mapping in bot.py on_speech_complete - Rename from 'Jarvis Voice Bot' to 'MoltMic' throughout --- config.yaml | 27 +- discord_bot/bot.py | 11 +- discord_bot/commands.py | 602 ++++------------------------------------ run.py | 39 ++- utils/config.py | 12 +- 5 files changed, 69 insertions(+), 622 deletions(-) diff --git a/config.yaml b/config.yaml index c301754..1efa139 100644 --- a/config.yaml +++ b/config.yaml @@ -22,32 +22,7 @@ discord: # Agent Configuration # ============================================================================ agents: - # Default agent (jarvis or sage) - default: "jarvis" - - # Per-agent settings - jarvis: - # TTS voice reference file (relative to server/voices/) - voice_file: "jarvis.mp3" - - # Agent personality for LLM context - personality: | - You are Jarvis, an intelligent, witty, and helpful AI assistant. - You speak naturally and conversationally, with subtle British sophistication. - You provide accurate information and thoughtful insights without being - verbose. You have a dry sense of humor but know when to be serious. - - # TTS emotion exaggeration (0.0 = none, 1.0 = full) - emotion_exaggeration: 0.3 - - sage: - voice_file: "sage.wav" - personality: | - You are Sage, a wise, calm, and philosophical AI assistant. - You speak thoughtfully and deliberately, offering deep insights and - perspectives. You are patient, empathetic, and help people think through - complex problems. Your tone is warm and encouraging. - emotion_exaggeration: 0.2 + default: "main" # ============================================================================ # OpenClaw Gateway diff --git a/discord_bot/bot.py b/discord_bot/bot.py index 0dc19bd..18bdbd7 100644 --- a/discord_bot/bot.py +++ b/discord_bot/bot.py @@ -416,15 +416,8 @@ class JarvisVoiceBot(discord.Client): logger.error("TTS synthesizer not available") return - # Map agent ID to TTS voice - # "main" agent uses jarvis voice, "sage" uses sage voice - if agent_id in ["jarvis", "main"]: - agent_name = "jarvis" - else: - agent_name = "sage" - logger.info(f"Synthesizing TTS for agent '{agent_name}' (agent_id={agent_id})...") - - tts_audio = await self.tts_synthesizer.synthesize(agent=agent_name, text=response) + logger.info(f"Synthesizing TTS...") + tts_audio = await self.tts_synthesizer.synthesize(agent="default", text=response) if tts_audio is None or len(tts_audio) == 0: logger.warning("TTS synthesis failed or returned empty audio") diff --git a/discord_bot/commands.py b/discord_bot/commands.py index 33bf9cb..816ab54 100644 --- a/discord_bot/commands.py +++ b/discord_bot/commands.py @@ -1,4 +1,4 @@ -"""Discord slash commands for the Jarvis Voice Bot.""" +"""Discord slash commands for MoltMic voice bot.""" from typing import Optional @@ -17,593 +17,87 @@ except ImportError: logger = get_logger(__name__) -class VoiceBotCommands(app_commands.Group): - """Slash command group for voice bot controls.""" +class MoltMicCommands(app_commands.Group): + """Slash commands for MoltMic voice bot.""" def __init__(self, bot): - """Initialize command group.""" - super().__init__(name="jarvis", description="Jarvis Voice Bot commands") + super().__init__(name="moltmic", description="MoltMic voice commands") self.bot = bot - self.agent_name = "jarvis" - @app_commands.command( - name="join", - description="Join your voice channel as Jarvis", - ) - @app_commands.describe(channel="Voice channel to join (optional)") - async def join( - self, - interaction: discord.Interaction, - channel: Optional[discord.VoiceChannel] = None, - ): - """Join a voice channel as Jarvis.""" - await self._join_with_agent(interaction, channel, self.agent_name) - - async def _join_with_agent( - self, - interaction: discord.Interaction, - channel: Optional[discord.VoiceChannel], - agent: str, - ): - """Join voice channel and set agent.""" + @app_commands.command(name="join", description="Join your voice channel and start listening") + @app_commands.describe(channel="Voice channel to join (defaults to your current channel)") + async def join(self, interaction: discord.Interaction, channel: Optional[discord.VoiceChannel] = None): await interaction.response.defer(thinking=True) try: - # Determine which channel to join - target_channel = channel + target = channel or (interaction.user.voice and interaction.user.voice.channel) + if not target: + await interaction.followup.send("❌ You're not in a voice channel.", ephemeral=True) + return - if target_channel is None: - # Join user's current voice channel - if interaction.user.voice is None: - await interaction.followup.send( - "❌ You're not in a voice channel! " - "Either join one or specify a channel.", - ephemeral=True, - ) - return + # Already in this channel? + if interaction.guild.voice_client and interaction.guild.voice_client.channel.id == target.id: + await interaction.followup.send(f"Already in {target.mention}", ephemeral=True) + return - target_channel = interaction.user.voice.channel + # Move or connect + if interaction.guild.voice_client: + await interaction.guild.voice_client.move_to(target) + voice_client = interaction.guild.voice_client + else: + connect_cls = voice_recv.VoiceRecvClient if HAS_VOICE_RECV else discord.VoiceClient + voice_client = await target.connect(cls=connect_cls, self_deaf=False, timeout=60.0) - # Check if already connected - if interaction.guild.voice_client is not None: - if interaction.guild.voice_client.channel.id == target_channel.id: - # Already in the channel - update agent - await self.bot.session_manager.set_agent(interaction.guild.id, agent) - await interaction.followup.send( - f"✅ Switched to **{agent.title()}** in {target_channel.mention}", - ephemeral=True, - ) - return - else: - # Move to new channel - await interaction.guild.voice_client.move_to(target_channel) - # Create session in new channel - await self.bot.on_voice_join( - interaction.guild, - target_channel, - interaction.guild.voice_client - ) - # Set agent after session created - await self.bot.session_manager.set_agent(interaction.guild.id, agent) - await interaction.followup.send( - f"✅ **{agent.title()}** joined {target_channel.mention}" - ) - return - - # Connect to channel using VoiceRecvClient for audio receiving - connect_cls = voice_recv.VoiceRecvClient if HAS_VOICE_RECV else discord.VoiceClient - voice_client = await target_channel.connect( - cls=connect_cls, - self_deaf=False, - timeout=60.0 - ) - - # Create session via bot handler - await self.bot.on_voice_join(interaction.guild, target_channel, voice_client) - - # Set agent after session created - await self.bot.session_manager.set_agent(interaction.guild.id, agent) - - personalities = { - "jarvis": "🎩 Intelligent, witty, and sophisticated", - "sage": "🧘 Wise, calm, and philosophical", - } - - await interaction.followup.send( - f"✅ **{agent.title()}** joined {target_channel.mention} and listening...\n" - f"{personalities.get(agent, '')}" - ) + await self.bot.on_voice_join(interaction.guild, target, voice_client) + await interaction.followup.send(f"🎙️ Joined {target.mention} and listening...") except discord.errors.ClientException as e: - logger.error(f"Failed to join voice channel: {e}") - await interaction.followup.send( - f"❌ Failed to join channel: {e}", - ephemeral=True, - ) - + logger.error(f"Failed to join: {e}") + await interaction.followup.send(f"❌ Failed to join: {e}", ephemeral=True) except Exception as e: - logger.exception(f"Unexpected error in join command: {e}") - await interaction.followup.send( - "❌ An unexpected error occurred", - ephemeral=True, - ) + logger.exception(f"Join error: {e}") + await interaction.followup.send("❌ Unexpected error", ephemeral=True) - @app_commands.command( - name="leave", - description="Leave the current voice channel", - ) + @app_commands.command(name="leave", description="Leave the voice channel") async def leave(self, interaction: discord.Interaction): - """Leave voice channel.""" await interaction.response.defer(thinking=True) try: - if interaction.guild.voice_client is None: - await interaction.followup.send( - "❌ Not in a voice channel", - ephemeral=True, - ) + if not interaction.guild.voice_client: + await interaction.followup.send("❌ Not in a voice channel.", ephemeral=True) return - # Disconnect via bot handler await self.bot.on_voice_leave(interaction.guild) - - await interaction.followup.send("👋 Left voice channel") + await interaction.followup.send("👋 Left voice channel.") except Exception as e: - logger.exception(f"Error in leave command: {e}") - await interaction.followup.send( - "❌ An error occurred while leaving", - ephemeral=True, - ) + logger.exception(f"Leave error: {e}") + await interaction.followup.send("❌ Error leaving.", ephemeral=True) - @app_commands.command( - name="agent", - description="Switch active AI agent", - ) - @app_commands.describe(name="Agent to use (jarvis or sage)") - @app_commands.choices( - name=[ - app_commands.Choice(name="Jarvis", value="jarvis"), - app_commands.Choice(name="Sage", value="sage"), - ] - ) - async def agent(self, interaction: discord.Interaction, name: str): - """Switch active agent.""" - await interaction.response.defer(thinking=True) - - try: - # Get session manager - session_manager = self.bot.session_manager - - # Update agent - success = await session_manager.set_agent(interaction.guild.id, name) - - if not success: - await interaction.followup.send( - "❌ Not in a voice channel. Use `/jarvis join` first.", - ephemeral=True, - ) - return - - # Get personality description - personalities = { - "jarvis": "🎩 Intelligent, witty, and sophisticated", - "sage": "🧘 Wise, calm, and philosophical", - } - - await interaction.followup.send( - f"✅ Switched to **{name.title()}**\n" - f"{personalities.get(name, '')}" - ) - - except Exception as e: - logger.exception(f"Error in agent command: {e}") - await interaction.followup.send( - "❌ An error occurred", - ephemeral=True, - ) - - @app_commands.command( - name="sensitivity", - description="Adjust how often the bot responds", - ) - @app_commands.describe(level="Sensitivity level") - @app_commands.choices( - level=[ - app_commands.Choice( - name="Low - Only when mentioned by name", - value="low", - ), - app_commands.Choice( - name="Medium - Name + relevant questions (recommended)", - value="medium", - ), - app_commands.Choice( - name="High - Responds more proactively", - value="high", - ), - ] - ) - async def sensitivity(self, interaction: discord.Interaction, level: str): - """Set relevance sensitivity.""" - await interaction.response.defer(thinking=True) - - try: - # Get session manager - session_manager = self.bot.session_manager - - # Update sensitivity - success = await session_manager.set_sensitivity( - interaction.guild.id, level - ) - - if not success: - await interaction.followup.send( - "❌ Not in a voice channel. Use `/jarvis join` first.", - ephemeral=True, - ) - return - - descriptions = { - "low": "Only responds when mentioned by name", - "medium": "Responds to name mentions and relevant questions", - "high": "Responds more proactively to conversations", - } - - await interaction.followup.send( - f"✅ Sensitivity set to **{level}**\n" - f"{descriptions.get(level, '')}" - ) - - except Exception as e: - logger.exception(f"Error in sensitivity command: {e}") - await interaction.followup.send( - "❌ An error occurred", - ephemeral=True, - ) - - @app_commands.command( - name="status", - description="Show bot status and statistics", - ) + @app_commands.command(name="status", description="Show bot status") async def status(self, interaction: discord.Interaction): - """Show bot status.""" await interaction.response.defer(thinking=True) try: - session_manager = self.bot.session_manager - session = session_manager.get_session(interaction.guild.id) - + session = self.bot.session_manager.get_session(interaction.guild.id) if not session: - await interaction.followup.send( - "❌ Not in a voice channel", - ephemeral=True, - ) + await interaction.followup.send("❌ Not in a voice channel.", ephemeral=True) return - # Build status embed - embed = discord.Embed( - title="🤖 Jarvis Voice Bot Status", - color=discord.Color.blue(), - ) - - # Session info - embed.add_field( - name="📊 Session", - value=f"Channel: <#{session.channel_id}>\n" - f"Duration: {session.duration:.0f}s\n" - f"Active Users: {session.get_user_count()}", - inline=True, - ) - - # Configuration - embed.add_field( - name="⚙️ Configuration", - value=f"Agent: **{session.current_agent.title()}**\n" - f"Sensitivity: **{session.sensitivity}**", - inline=True, - ) - - # Global stats - total_sessions = session_manager.get_session_count() - embed.add_field( - name="🌐 Global", - value=f"Total Sessions: {total_sessions}", - inline=True, - ) - - # TODO: Add latency stats when pipeline is implemented - # embed.add_field( - # name="⚡ Performance", - # value=f"Avg Latency: X.XXs\n" - # f"Transcriptions: XX", - # inline=False, - # ) - + embed = discord.Embed(title="🎙️ MoltMic Status", color=discord.Color.green()) + embed.add_field(name="Channel", value=f"<#{session.channel_id}>", inline=True) + embed.add_field(name="Duration", value=f"{session.duration:.0f}s", inline=True) + embed.add_field(name="Users", value=str(session.get_user_count()), inline=True) await interaction.followup.send(embed=embed) except Exception as e: - logger.exception(f"Error in status command: {e}") - await interaction.followup.send( - "❌ An error occurred", - ephemeral=True, - ) - - -class SageBotCommands(app_commands.Group): - """Slash command group for Sage bot controls.""" - - def __init__(self, bot): - """Initialize command group.""" - super().__init__(name="sage", description="Sage Voice Bot commands") - self.bot = bot - self.agent_name = "sage" - - @app_commands.command( - name="join", - description="Join your voice channel as Sage", - ) - @app_commands.describe(channel="Voice channel to join (optional)") - async def join( - self, - interaction: discord.Interaction, - channel: Optional[discord.VoiceChannel] = None, - ): - """Join a voice channel as Sage.""" - await self._join_with_agent(interaction, channel, self.agent_name) - - async def _join_with_agent( - self, - interaction: discord.Interaction, - channel: Optional[discord.VoiceChannel], - agent: str, - ): - """Join voice channel and set agent.""" - await interaction.response.defer(thinking=True) - - try: - # Determine which channel to join - target_channel = channel - - if target_channel is None: - # Join user's current voice channel - if interaction.user.voice is None: - await interaction.followup.send( - "❌ You're not in a voice channel! " - "Either join one or specify a channel.", - ephemeral=True, - ) - return - - target_channel = interaction.user.voice.channel - - # Check if already connected - if interaction.guild.voice_client is not None: - if interaction.guild.voice_client.channel.id == target_channel.id: - # Already in the channel - update agent - await self.bot.session_manager.set_agent(interaction.guild.id, agent) - await interaction.followup.send( - f"✅ Switched to **{agent.title()}** in {target_channel.mention}", - ephemeral=True, - ) - return - else: - # Move to new channel - await interaction.guild.voice_client.move_to(target_channel) - # Create session in new channel with agent - await self.bot.on_voice_join( - interaction.guild, - target_channel, - interaction.guild.voice_client - ) - # Set agent after session created - await self.bot.session_manager.set_agent(interaction.guild.id, agent) - await interaction.followup.send( - f"✅ **{agent.title()}** joined {target_channel.mention}" - ) - return - - # Connect to channel using VoiceRecvClient for audio receiving - connect_cls = voice_recv.VoiceRecvClient if HAS_VOICE_RECV else discord.VoiceClient - voice_client = await target_channel.connect( - cls=connect_cls, - self_deaf=False, - timeout=60.0 - ) - - # Create session via bot handler - await self.bot.on_voice_join(interaction.guild, target_channel, voice_client) - - # Set agent after session created - await self.bot.session_manager.set_agent(interaction.guild.id, agent) - - personalities = { - "jarvis": "🎩 Intelligent, witty, and sophisticated", - "sage": "🧘 Wise, calm, and philosophical", - } - - await interaction.followup.send( - f"✅ **{agent.title()}** joined {target_channel.mention} and listening...\n" - f"{personalities.get(agent, '')}" - ) - - except discord.errors.ClientException as e: - logger.error(f"Failed to join voice channel: {e}") - await interaction.followup.send( - f"❌ Failed to join channel: {e}", - ephemeral=True, - ) - - except Exception as e: - logger.exception(f"Unexpected error in join command: {e}") - await interaction.followup.send( - "❌ An unexpected error occurred", - ephemeral=True, - ) - - @app_commands.command( - name="leave", - description="Leave the current voice channel", - ) - async def leave(self, interaction: discord.Interaction): - """Leave voice channel.""" - await interaction.response.defer(thinking=True) - - try: - if interaction.guild.voice_client is None: - await interaction.followup.send( - "❌ Not in a voice channel", - ephemeral=True, - ) - return - - # Disconnect via bot handler - await self.bot.on_voice_leave(interaction.guild) - - await interaction.followup.send("👋 Sage left voice channel") - - except Exception as e: - logger.exception(f"Error in leave command: {e}") - await interaction.followup.send( - "❌ An error occurred while leaving", - ephemeral=True, - ) - - @app_commands.command( - name="sensitivity", - description="Adjust how often Sage responds", - ) - @app_commands.describe(level="Sensitivity level") - @app_commands.choices( - level=[ - app_commands.Choice( - name="Low - Only when mentioned by name", - value="low", - ), - app_commands.Choice( - name="Medium - Name + relevant questions (recommended)", - value="medium", - ), - app_commands.Choice( - name="High - Responds more proactively", - value="high", - ), - ] - ) - async def sensitivity(self, interaction: discord.Interaction, level: str): - """Set relevance sensitivity.""" - await interaction.response.defer(thinking=True) - - try: - # Get session manager - session_manager = self.bot.session_manager - - # Update sensitivity - success = await session_manager.set_sensitivity( - interaction.guild.id, level - ) - - if not success: - await interaction.followup.send( - "❌ Not in a voice channel. Use `/sage join` first.", - ephemeral=True, - ) - return - - descriptions = { - "low": "Only responds when mentioned by name", - "medium": "Responds to name mentions and relevant questions", - "high": "Responds more proactively to conversations", - } - - await interaction.followup.send( - f"✅ Sensitivity set to **{level}**\n" - f"{descriptions.get(level, '')}" - ) - - except Exception as e: - logger.exception(f"Error in sensitivity command: {e}") - await interaction.followup.send( - "❌ An error occurred", - ephemeral=True, - ) - - @app_commands.command( - name="status", - description="Show Sage bot status and statistics", - ) - async def status(self, interaction: discord.Interaction): - """Show bot status.""" - await interaction.response.defer(thinking=True) - - try: - session_manager = self.bot.session_manager - session = session_manager.get_session(interaction.guild.id) - - if not session: - await interaction.followup.send( - "❌ Not in a voice channel", - ephemeral=True, - ) - return - - # Build status embed - embed = discord.Embed( - title="🧘 Sage Voice Bot Status", - color=discord.Color.purple(), - ) - - # Session info - embed.add_field( - name="📊 Session", - value=f"Channel: <#{session.channel_id}>\n" - f"Duration: {session.duration:.0f}s\n" - f"Active Users: {session.get_user_count()}", - inline=True, - ) - - # Configuration - embed.add_field( - name="⚙️ Configuration", - value=f"Agent: **{session.current_agent.title()}**\n" - f"Sensitivity: **{session.sensitivity}**", - inline=True, - ) - - # Global stats - total_sessions = session_manager.get_session_count() - embed.add_field( - name="🌐 Global", - value=f"Total Sessions: {total_sessions}", - inline=True, - ) - - await interaction.followup.send(embed=embed) - - except Exception as e: - logger.exception(f"Error in status command: {e}") - await interaction.followup.send( - "❌ An error occurred", - ephemeral=True, - ) + logger.exception(f"Status error: {e}") + await interaction.followup.send("❌ Error.", ephemeral=True) async def setup_commands(bot): - """ - Set up and register slash commands. - - Args: - bot: Discord bot instance - - Returns: - Tuple of command groups (jarvis, sage) - """ - jarvis_commands = VoiceBotCommands(bot) - sage_commands = SageBotCommands(bot) - - bot.tree.add_command(jarvis_commands) - bot.tree.add_command(sage_commands) - - logger.info("Slash commands registered (jarvis, sage)") - - return jarvis_commands, sage_commands + """Register slash commands.""" + cmds = MoltMicCommands(bot) + bot.tree.add_command(cmds) + logger.info("Slash commands registered (moltmic: join, leave, status)") + return cmds diff --git a/run.py b/run.py index ec343f7..eb2365a 100644 --- a/run.py +++ b/run.py @@ -1,5 +1,5 @@ """ -Jarvis Voice Bot - Main Entry Point +MoltMic - OpenClaw Voice Bot This script starts both the Discord bot and FastAPI server. """ @@ -37,7 +37,7 @@ async def main(): logger = get_logger(__name__) logger.info("=" * 70) - logger.info("Jarvis Voice Bot Starting") + logger.info("MoltMic Starting") logger.info("=" * 70) # Validate required configuration @@ -50,21 +50,6 @@ async def main(): logger.info("✓ Discord token configured") - # Check voice reference files - from utils.config import get_voices_dir - - voices_dir = get_voices_dir() - jarvis_voice = voices_dir / config.agents.jarvis.voice_file - sage_voice = voices_dir / config.agents.sage.voice_file - - if not jarvis_voice.exists(): - logger.warning(f"Jarvis voice file not found: {jarvis_voice}") - logger.warning("TTS will not work until voice file is provided") - - if not sage_voice.exists(): - logger.warning(f"Sage voice file not found: {sage_voice}") - logger.warning("TTS will not work until voice file is provided") - # Validate OpenClaw Gateway configuration if not config.openclaw.base_url: logger.error("OpenClaw Gateway URL not configured!") @@ -183,11 +168,21 @@ async def main(): vad = SileroVAD() logger.info("✓ VAD initialized (Silero)") - turn_detector = SmartTurnDetector( - model_path=Path("models") / config.pipeline.turn_detection.model_path, - threshold=config.pipeline.turn_detection.threshold, - ) - logger.info("✓ Smart Turn v3 detector initialized") + turn_detector = None + try: + turn_detector = SmartTurnDetector( + model_path=Path("models") / config.pipeline.turn_detection.model_path, + threshold=config.pipeline.turn_detection.threshold, + ) + logger.info("✓ Smart Turn v3 detector initialized") + except Exception as e: + logger.warning(f"Smart Turn model unavailable, using simple fallback: {e}") + # Create a simple fallback that always returns True (trust VAD silence) + class SimpleTurnFallback: + async def detect_async(self, audio): + return 1.0 # Always say turn is complete + turn_detector = SimpleTurnFallback() + logger.info("✓ Using simple turn detection (VAD silence = turn complete)") stt_pipeline = PipelineTranscriber( transcriber=stt_transcriber, diff --git a/utils/config.py b/utils/config.py index bd69b93..3d2dc43 100644 --- a/utils/config.py +++ b/utils/config.py @@ -31,20 +31,10 @@ class DiscordConfig(BaseModel): return v -class AgentVoiceConfig(BaseModel): - """Per-agent voice configuration.""" - - voice_file: str - personality: str - emotion_exaggeration: float = Field(ge=0.0, le=1.0, default=0.3) - - class AgentsConfig(BaseModel): """Agents configuration.""" - default: str = "jarvis" - jarvis: AgentVoiceConfig - sage: AgentVoiceConfig + default: str = "main" class OpenClawConfig(BaseModel):