feat: add /voice preview command
- Added 8 random preview sample lines for voice testing - New /voice preview <name> command to hear voices before selecting - Previews play in queue like regular messages (no queue jumping) - Preview does NOT change user's active voice preference - Updated queue system to support voice override for previews - Added documentation for new command in README
This commit is contained in:
@@ -109,6 +109,7 @@ A Discord bot that reads messages aloud using [Pocket TTS](https://github.com/ky
|
||||
- `/voice set <name>` - Change your personal TTS voice
|
||||
- `/voice current` - Shows your current voice
|
||||
- `/voice refresh` - Re-scan for new voice files (no restart needed)
|
||||
- `/voice preview <name>` - Preview a voice before selecting it
|
||||
|
||||
### Test Mode
|
||||
|
||||
|
||||
103
bot.py
103
bot.py
@@ -1,5 +1,6 @@
|
||||
__version__ = "1.1.0"
|
||||
|
||||
import random
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -30,6 +31,18 @@ from voice_manager import VoiceManager
|
||||
# Inactivity timeout in seconds (10 minutes)
|
||||
INACTIVITY_TIMEOUT = 10 * 60
|
||||
|
||||
# Sample lines for voice preview
|
||||
PREVIEW_LINES = [
|
||||
"Hello! This is how I sound. Choose me as your voice with /voice set.",
|
||||
"Testing, one, two, three! Can you hear me clearly?",
|
||||
"Here's a preview of my voice. Pretty cool, right?",
|
||||
"Greetings! I am ready to speak for you.",
|
||||
"Voice check! This is what I sound like.",
|
||||
"Audio test complete. This voice is ready to go!",
|
||||
"Sample message incoming. How do I sound to you?",
|
||||
"Preview mode activated. Testing speech synthesis.",
|
||||
]
|
||||
|
||||
|
||||
class TTSBot(commands.Bot):
|
||||
"""Discord bot that reads messages aloud using Pocket TTS."""
|
||||
@@ -41,7 +54,7 @@ class TTSBot(commands.Bot):
|
||||
super().__init__(command_prefix="!", intents=intents)
|
||||
|
||||
self.voice_manager = VoiceManager(Config.VOICES_DIR, Config.DEFAULT_VOICE)
|
||||
self.message_queue: asyncio.Queue[tuple[discord.Message, str]] = asyncio.Queue()
|
||||
self.message_queue: asyncio.Queue[tuple[discord.Message, str] | tuple[discord.Message, str, str]] = asyncio.Queue()
|
||||
self.last_activity: float = 0.0
|
||||
|
||||
self._setup_slash_commands()
|
||||
@@ -59,6 +72,7 @@ class TTSBot(commands.Bot):
|
||||
app_commands.Choice(name="set", value="set"),
|
||||
app_commands.Choice(name="current", value="current"),
|
||||
app_commands.Choice(name="refresh", value="refresh"),
|
||||
app_commands.Choice(name="preview", value="preview"),
|
||||
])
|
||||
async def voice_command(
|
||||
interaction: discord.Interaction,
|
||||
@@ -73,6 +87,8 @@ class TTSBot(commands.Bot):
|
||||
await self._handle_voice_current(interaction)
|
||||
elif action.value == "refresh":
|
||||
await self._handle_voice_refresh(interaction)
|
||||
elif action.value == "preview":
|
||||
await self._handle_voice_preview(interaction, voice_name)
|
||||
|
||||
@voice_command.autocomplete("voice_name")
|
||||
async def voice_name_autocomplete(
|
||||
@@ -206,6 +222,66 @@ class TTSBot(commands.Bot):
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
async def _handle_voice_preview(self, interaction: discord.Interaction, voice_name: str | None) -> None:
|
||||
"""Handle /voice preview command."""
|
||||
if not voice_name:
|
||||
await interaction.response.send_message(
|
||||
"❌ Please provide a voice name. Use `/voice list` to see available voices.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Check if user is in a voice channel
|
||||
if interaction.user.voice is None:
|
||||
await interaction.response.send_message(
|
||||
"❌ You need to be in a voice channel to hear a preview!",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
voice_name = voice_name.lower()
|
||||
|
||||
# Validate voice exists
|
||||
if not self.voice_manager.is_voice_available(voice_name):
|
||||
voices = self.voice_manager.get_available_voices()
|
||||
await interaction.response.send_message(
|
||||
f"❌ Voice `{voice_name}` not found.\n"
|
||||
f"Available voices: {', '.join(f'`{v}`' for v in voices)}",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Select a random preview line
|
||||
preview_text = random.choice(PREVIEW_LINES)
|
||||
|
||||
# Create a preview message object with all necessary attributes
|
||||
class PreviewMessage:
|
||||
def __init__(self, user, channel, voice_channel):
|
||||
self.author = user
|
||||
self.channel = channel
|
||||
self._voice_channel = voice_channel
|
||||
|
||||
@property
|
||||
def voice(self):
|
||||
class VoiceState:
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
return VoiceState(self._voice_channel)
|
||||
|
||||
preview_message = PreviewMessage(
|
||||
interaction.user,
|
||||
interaction.channel,
|
||||
interaction.user.voice.channel
|
||||
)
|
||||
|
||||
# Queue the preview with voice override
|
||||
await self.message_queue.put((preview_message, preview_text, voice_name))
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"⏳ Queued preview for `{voice_name}`. Sample: \"{preview_text[:50]}{'...' if len(preview_text) > 50 else ''}\"",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
async def setup_hook(self) -> None:
|
||||
"""Called when the bot is starting up."""
|
||||
print("Initializing TTS...")
|
||||
@@ -258,16 +334,23 @@ class TTSBot(commands.Bot):
|
||||
async def process_queue(self) -> None:
|
||||
"""Process messages from the queue one at a time."""
|
||||
while True:
|
||||
message, text = await self.message_queue.get()
|
||||
queue_item = await self.message_queue.get()
|
||||
|
||||
# Handle both regular messages (message, text) and previews (message, text, voice_name)
|
||||
if len(queue_item) == 3:
|
||||
message, text, voice_override = queue_item
|
||||
else:
|
||||
message, text = queue_item
|
||||
voice_override = None
|
||||
|
||||
try:
|
||||
await self.speak_message(message, text)
|
||||
await self.speak_message(message, text, voice_override)
|
||||
except Exception as e:
|
||||
print(f"Error processing message: {e}")
|
||||
finally:
|
||||
self.message_queue.task_done()
|
||||
|
||||
async def speak_message(self, message: discord.Message, text: str) -> None:
|
||||
async def speak_message(self, message: discord.Message, text: str, voice_override: str | None = None) -> None:
|
||||
"""Generate TTS and play it in the user's voice channel."""
|
||||
if message.author.voice is None:
|
||||
return
|
||||
@@ -280,14 +363,20 @@ class TTSBot(commands.Bot):
|
||||
|
||||
print(f"Generating TTS for: {text[:50]}...")
|
||||
|
||||
# Get user's voice (loads on-demand if needed)
|
||||
user_id = message.author.id
|
||||
# Get voice state (use override for previews, otherwise user's voice)
|
||||
try:
|
||||
if voice_override:
|
||||
voice_state = await asyncio.to_thread(
|
||||
self.voice_manager.get_voice_state, voice_override
|
||||
)
|
||||
else:
|
||||
user_id = message.author.id
|
||||
voice_state = await asyncio.to_thread(
|
||||
self.voice_manager.get_user_voice_state, user_id
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error loading voice for user {user_id}: {e}")
|
||||
print(f"Error loading voice: {e}")
|
||||
if not voice_override:
|
||||
await message.channel.send(
|
||||
f"{message.author.mention}, failed to load your voice. Use `/voice set` to choose a voice.",
|
||||
delete_after=5
|
||||
|
||||
Reference in New Issue
Block a user