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
This commit is contained in:
Jezza Hehn 2026-04-10 01:43:02 +00:00
parent a33a3b9105
commit a2099e9d81
5 changed files with 69 additions and 622 deletions

View file

@ -22,32 +22,7 @@ discord:
# Agent Configuration # Agent Configuration
# ============================================================================ # ============================================================================
agents: agents:
# Default agent (jarvis or sage) default: "main"
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
# ============================================================================ # ============================================================================
# OpenClaw Gateway # OpenClaw Gateway

View file

@ -416,15 +416,8 @@ class JarvisVoiceBot(discord.Client):
logger.error("TTS synthesizer not available") logger.error("TTS synthesizer not available")
return return
# Map agent ID to TTS voice logger.info(f"Synthesizing TTS...")
# "main" agent uses jarvis voice, "sage" uses sage voice tts_audio = await self.tts_synthesizer.synthesize(agent="default", text=response)
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)
if tts_audio is None or len(tts_audio) == 0: if tts_audio is None or len(tts_audio) == 0:
logger.warning("TTS synthesis failed or returned empty audio") logger.warning("TTS synthesis failed or returned empty audio")

View file

@ -1,4 +1,4 @@
"""Discord slash commands for the Jarvis Voice Bot.""" """Discord slash commands for MoltMic voice bot."""
from typing import Optional from typing import Optional
@ -17,593 +17,87 @@ except ImportError:
logger = get_logger(__name__) logger = get_logger(__name__)
class VoiceBotCommands(app_commands.Group): class MoltMicCommands(app_commands.Group):
"""Slash command group for voice bot controls.""" """Slash commands for MoltMic voice bot."""
def __init__(self, bot): def __init__(self, bot):
"""Initialize command group.""" super().__init__(name="moltmic", description="MoltMic voice commands")
super().__init__(name="jarvis", description="Jarvis Voice Bot commands")
self.bot = bot self.bot = bot
self.agent_name = "jarvis"
@app_commands.command( @app_commands.command(name="join", description="Join your voice channel and start listening")
name="join", @app_commands.describe(channel="Voice channel to join (defaults to your current channel)")
description="Join your voice channel as Jarvis", async def join(self, interaction: discord.Interaction, channel: Optional[discord.VoiceChannel] = None):
)
@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."""
await interaction.response.defer(thinking=True) await interaction.response.defer(thinking=True)
try: try:
# Determine which channel to join target = channel or (interaction.user.voice and interaction.user.voice.channel)
target_channel = channel if not target:
await interaction.followup.send("❌ You're not in a voice channel.", ephemeral=True)
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 return
target_channel = interaction.user.voice.channel # Already in this channel?
if interaction.guild.voice_client and interaction.guild.voice_client.channel.id == target.id:
# Check if already connected await interaction.followup.send(f"Already in {target.mention}", ephemeral=True)
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 return
# Move or connect
if interaction.guild.voice_client:
await interaction.guild.voice_client.move_to(target)
voice_client = interaction.guild.voice_client
else: 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 connect_cls = voice_recv.VoiceRecvClient if HAS_VOICE_RECV else discord.VoiceClient
voice_client = await target_channel.connect( voice_client = await target.connect(cls=connect_cls, self_deaf=False, timeout=60.0)
cls=connect_cls,
self_deaf=False,
timeout=60.0
)
# Create session via bot handler await self.bot.on_voice_join(interaction.guild, target, voice_client)
await self.bot.on_voice_join(interaction.guild, target_channel, voice_client) await interaction.followup.send(f"🎙️ Joined {target.mention} and listening...")
# 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: except discord.errors.ClientException as e:
logger.error(f"Failed to join voice channel: {e}") logger.error(f"Failed to join: {e}")
await interaction.followup.send( await interaction.followup.send(f"❌ Failed to join: {e}", ephemeral=True)
f"❌ Failed to join channel: {e}",
ephemeral=True,
)
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error in join command: {e}") logger.exception(f"Join error: {e}")
await interaction.followup.send( await interaction.followup.send("❌ Unexpected error", ephemeral=True)
"❌ An unexpected error occurred",
ephemeral=True,
)
@app_commands.command( @app_commands.command(name="leave", description="Leave the voice channel")
name="leave",
description="Leave the current voice channel",
)
async def leave(self, interaction: discord.Interaction): async def leave(self, interaction: discord.Interaction):
"""Leave voice channel."""
await interaction.response.defer(thinking=True) await interaction.response.defer(thinking=True)
try: try:
if interaction.guild.voice_client is None: if not interaction.guild.voice_client:
await interaction.followup.send( await interaction.followup.send("❌ Not in a voice channel.", ephemeral=True)
"❌ Not in a voice channel",
ephemeral=True,
)
return return
# Disconnect via bot handler
await self.bot.on_voice_leave(interaction.guild) 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: except Exception as e:
logger.exception(f"Error in leave command: {e}") logger.exception(f"Leave error: {e}")
await interaction.followup.send( await interaction.followup.send("❌ Error leaving.", ephemeral=True)
"❌ An error occurred while leaving",
ephemeral=True,
)
@app_commands.command( @app_commands.command(name="status", description="Show bot status")
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",
)
async def status(self, interaction: discord.Interaction): async def status(self, interaction: discord.Interaction):
"""Show bot status."""
await interaction.response.defer(thinking=True) await interaction.response.defer(thinking=True)
try: try:
session_manager = self.bot.session_manager session = self.bot.session_manager.get_session(interaction.guild.id)
session = session_manager.get_session(interaction.guild.id)
if not session: if not session:
await interaction.followup.send( await interaction.followup.send("❌ Not in a voice channel.", ephemeral=True)
"❌ Not in a voice channel",
ephemeral=True,
)
return return
# Build status embed embed = discord.Embed(title="🎙️ MoltMic Status", color=discord.Color.green())
embed = discord.Embed( embed.add_field(name="Channel", value=f"<#{session.channel_id}>", inline=True)
title="🤖 Jarvis Voice Bot Status", embed.add_field(name="Duration", value=f"{session.duration:.0f}s", inline=True)
color=discord.Color.blue(), embed.add_field(name="Users", value=str(session.get_user_count()), inline=True)
)
# 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,
# )
await interaction.followup.send(embed=embed) await interaction.followup.send(embed=embed)
except Exception as e: except Exception as e:
logger.exception(f"Error in status command: {e}") logger.exception(f"Status error: {e}")
await interaction.followup.send( await interaction.followup.send("❌ Error.", ephemeral=True)
"❌ 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,
)
async def setup_commands(bot): async def setup_commands(bot):
""" """Register slash commands."""
Set up and register slash commands. cmds = MoltMicCommands(bot)
bot.tree.add_command(cmds)
Args: logger.info("Slash commands registered (moltmic: join, leave, status)")
bot: Discord bot instance return cmds
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

29
run.py
View file

@ -1,5 +1,5 @@
""" """
Jarvis Voice Bot - Main Entry Point MoltMic - OpenClaw Voice Bot
This script starts both the Discord bot and FastAPI server. This script starts both the Discord bot and FastAPI server.
""" """
@ -37,7 +37,7 @@ async def main():
logger = get_logger(__name__) logger = get_logger(__name__)
logger.info("=" * 70) logger.info("=" * 70)
logger.info("Jarvis Voice Bot Starting") logger.info("MoltMic Starting")
logger.info("=" * 70) logger.info("=" * 70)
# Validate required configuration # Validate required configuration
@ -50,21 +50,6 @@ async def main():
logger.info("✓ Discord token configured") 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 # Validate OpenClaw Gateway configuration
if not config.openclaw.base_url: if not config.openclaw.base_url:
logger.error("OpenClaw Gateway URL not configured!") logger.error("OpenClaw Gateway URL not configured!")
@ -183,11 +168,21 @@ async def main():
vad = SileroVAD() vad = SileroVAD()
logger.info("✓ VAD initialized (Silero)") logger.info("✓ VAD initialized (Silero)")
turn_detector = None
try:
turn_detector = SmartTurnDetector( turn_detector = SmartTurnDetector(
model_path=Path("models") / config.pipeline.turn_detection.model_path, model_path=Path("models") / config.pipeline.turn_detection.model_path,
threshold=config.pipeline.turn_detection.threshold, threshold=config.pipeline.turn_detection.threshold,
) )
logger.info("✓ Smart Turn v3 detector initialized") 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( stt_pipeline = PipelineTranscriber(
transcriber=stt_transcriber, transcriber=stt_transcriber,

View file

@ -31,20 +31,10 @@ class DiscordConfig(BaseModel):
return v 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): class AgentsConfig(BaseModel):
"""Agents configuration.""" """Agents configuration."""
default: str = "jarvis" default: str = "main"
jarvis: AgentVoiceConfig
sage: AgentVoiceConfig
class OpenClawConfig(BaseModel): class OpenClawConfig(BaseModel):