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
# ============================================================================
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

View file

@ -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")

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
@ -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

39
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.
"""
@ -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,

View file

@ -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):