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:
parent
a33a3b9105
commit
a2099e9d81
5 changed files with 69 additions and 622 deletions
27
config.yaml
27
config.yaml
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
29
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.
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue