diff --git a/ttstoy/README.md b/ttstoy/README.md new file mode 100644 index 0000000..68cd960 --- /dev/null +++ b/ttstoy/README.md @@ -0,0 +1,93 @@ +# TTS Toy + +A RedBot cog for text-to-speech in voice chat with multiple TTS engines, emoji sound effects, and voice cloning. + +## Modes + +| Mode | Description | Requires | +|------|-------------|----------| +| `minimax` | MiniMax cloud API, natural voices | API key | +| `chatterbox` | Local Chatterbox server, AI voice cloning | Chatterbox server | +| `dectalk` | Classic 1980s robotic voice | DECTalk server | +| `morshu` | Morshu from CD-i Zelda | g2p_en, numpy, pydub | +| `vox` | Black Mesa VOX announcer | Built-in | + +Switch modes: `[p]ttstoy mode ` + +## Quick Start + +``` +[p]ttstoy mode chatterbox +[p]chatterbox addvoice MyVoice (attach .wav/.mp3 or paste a URL) +[p]tts Hello world [laugh] this is cool +``` + +## Commands + +Use `[p]ttstoy` to see all available commands for the current mode. + +### Chatterbox Voice Management + +| Command | Description | +|---------|-------------| +| `[p]chatterbox addvoice ` | Upload a voice clip (attach file or paste URL) | +| `[p]chatterbox removevoice ` | Remove a voice and delete from server | +| `[p]chatterbox sharevoice @User` | Share a voice with another user | +| `[p]chatterbox myvoices` | List your voices | +| `[p]chatterbox temp [0.0-1.5]` | Per-voice randomness (per voice per user) | +| `[p]chatterbox exag [0.25-2.0]` | Per-voice expressiveness (per voice per user) | +| `[p]chatterbox volume [-20-20]` | Per-voice dB loudness offset (per voice per user) | +| `[p]chatterbox guide` | Full feature guide | +| `[p]chatterbox model [turbo\|original]` | Switch Chatterbox model (owner) | +| `[p]chatterbox url [url]` | Set Chatterbox server URL (owner) | + +Append `reset` to temp, exag, or volume to clear the setting. + +### General + +| Command | Description | +|---------|-------------| +| `[p]tts ` | Speak in voice chat | +| `[p]ttstoy myvoice [name]` | Set or view your personal voice | +| `[p]ttstoy voice [name]` | Set global default voice (owner) | +| `[p]ttstoy mode [mode]` | Show or set TTS mode (owner) | +| `[p]ttstoy sfxvolume [0-100]` | Set SFX volume (owner) | +| `[p]sfx` | List available sound effects | +| `[p]ttstoy info` | DM setup instructions | + +### DECTalk + +| Command | Description | +|---------|-------------| +| `[p]ttstoy dectalkinstall` | Install built-in DECTalk server | +| `[p]ttstoy dectalkstart` | Start server | +| `[p]ttstoy dectalkstop` | Stop server | +| `[p]ttstoy dectalkstatus` | Check server status | +| `[p]ttstoy dectalkautotoggle` | Toggle auto-start | +| `[p]ttstoy dectalkurl [url]` | Set DECTalk API URL | + +DECTalk voices are controlled in-text: `[:np]` Paul, `[:nb]` Betty, `[:nh]` Harry, `[:nf]` Frank, `[:nd]` Dennis, `[:nk]` Kit, `[:nu]` Ursula, `[:nr]` Rita, `[:nw]` Wendy. + +### VOX + +| Command | Description | +|---------|-------------| +| `[p]ttstoy voxpack [vox\|vox2]` | Switch VOX voice pack | +| `[p]ttstoy voxwords` | List available words | + +## Emoji SFX + +Include emoji in your TTS text to trigger sound effects: + +``` +[p]tts Hello ๐ŸŽ‰ world ๐Ÿ”ฅ +``` + +Use `[p]sfx` to see all available triggers. Add `.mp3` files to `sfx//` to customize. + +## Requirements + +- RedBot 3.5+ +- Audio cog loaded +- ffmpeg +- `requests`, `g2p_en`, `numpy`, `pydub` (pip) diff --git a/ttstoy/__init__.py b/ttstoy/__init__.py new file mode 100755 index 0000000..73592f1 --- /dev/null +++ b/ttstoy/__init__.py @@ -0,0 +1,5 @@ +from .ttstoy import TtsToy + + +async def setup(bot): + await bot.add_cog(TtsToy(bot)) diff --git a/ttstoy/__pycache__/__init__.cpython-311.opt-1.pyc b/ttstoy/__pycache__/__init__.cpython-311.opt-1.pyc new file mode 100644 index 0000000..0dae7a9 Binary files /dev/null and b/ttstoy/__pycache__/__init__.cpython-311.opt-1.pyc differ diff --git a/ttstoy/__pycache__/__init__.cpython-311.pyc b/ttstoy/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..0dae7a9 Binary files /dev/null and b/ttstoy/__pycache__/__init__.cpython-311.pyc differ diff --git a/ttstoy/__pycache__/morshutalk_engine.cpython-311.opt-1.pyc b/ttstoy/__pycache__/morshutalk_engine.cpython-311.opt-1.pyc new file mode 100644 index 0000000..0b5066a Binary files /dev/null and b/ttstoy/__pycache__/morshutalk_engine.cpython-311.opt-1.pyc differ diff --git a/ttstoy/__pycache__/morshutalk_engine.cpython-311.pyc b/ttstoy/__pycache__/morshutalk_engine.cpython-311.pyc new file mode 100644 index 0000000..0b5066a Binary files /dev/null and b/ttstoy/__pycache__/morshutalk_engine.cpython-311.pyc differ diff --git a/ttstoy/__pycache__/mycog.cpython-311.opt-1.pyc b/ttstoy/__pycache__/mycog.cpython-311.opt-1.pyc new file mode 100644 index 0000000..e1a4f37 Binary files /dev/null and b/ttstoy/__pycache__/mycog.cpython-311.opt-1.pyc differ diff --git a/ttstoy/__pycache__/mycog.cpython-311.pyc b/ttstoy/__pycache__/mycog.cpython-311.pyc new file mode 100644 index 0000000..b75224f Binary files /dev/null and b/ttstoy/__pycache__/mycog.cpython-311.pyc differ diff --git a/ttstoy/__pycache__/openaitts.cpython-311.opt-1.pyc b/ttstoy/__pycache__/openaitts.cpython-311.opt-1.pyc new file mode 100644 index 0000000..6739302 Binary files /dev/null and b/ttstoy/__pycache__/openaitts.cpython-311.opt-1.pyc differ diff --git a/ttstoy/__pycache__/openaitts.cpython-311.pyc b/ttstoy/__pycache__/openaitts.cpython-311.pyc new file mode 100644 index 0000000..6739302 Binary files /dev/null and b/ttstoy/__pycache__/openaitts.cpython-311.pyc differ diff --git a/ttstoy/__pycache__/ttstoy.cpython-310.pyc b/ttstoy/__pycache__/ttstoy.cpython-310.pyc new file mode 100644 index 0000000..bf6cdf7 Binary files /dev/null and b/ttstoy/__pycache__/ttstoy.cpython-310.pyc differ diff --git a/ttstoy/__pycache__/ttstoy.cpython-311.opt-1.pyc b/ttstoy/__pycache__/ttstoy.cpython-311.opt-1.pyc new file mode 100644 index 0000000..f977c89 Binary files /dev/null and b/ttstoy/__pycache__/ttstoy.cpython-311.opt-1.pyc differ diff --git a/ttstoy/__pycache__/ttstoy.cpython-311.pyc b/ttstoy/__pycache__/ttstoy.cpython-311.pyc new file mode 100644 index 0000000..0f06b3f Binary files /dev/null and b/ttstoy/__pycache__/ttstoy.cpython-311.pyc differ diff --git a/ttstoy/__pycache__/vox_engine.cpython-311.opt-1.pyc b/ttstoy/__pycache__/vox_engine.cpython-311.opt-1.pyc new file mode 100644 index 0000000..eba6f08 Binary files /dev/null and b/ttstoy/__pycache__/vox_engine.cpython-311.opt-1.pyc differ diff --git a/ttstoy/__pycache__/vox_engine.cpython-311.pyc b/ttstoy/__pycache__/vox_engine.cpython-311.pyc new file mode 100644 index 0000000..eba6f08 Binary files /dev/null and b/ttstoy/__pycache__/vox_engine.cpython-311.pyc differ diff --git a/ttstoy/dectalk-server/.env.example b/ttstoy/dectalk-server/.env.example new file mode 100644 index 0000000..aea03e2 --- /dev/null +++ b/ttstoy/dectalk-server/.env.example @@ -0,0 +1,3 @@ +# DECTalk Server Configuration +PORT=3000 +OUTPUT_DIR=./output diff --git a/ttstoy/dectalk-server/.gitignore b/ttstoy/dectalk-server/.gitignore new file mode 100644 index 0000000..8a1fe52 --- /dev/null +++ b/ttstoy/dectalk-server/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +output/ +*.log +.env diff --git a/ttstoy/dectalk-server/INSTALL.md b/ttstoy/dectalk-server/INSTALL.md new file mode 100644 index 0000000..ee88b34 --- /dev/null +++ b/ttstoy/dectalk-server/INSTALL.md @@ -0,0 +1,83 @@ +# Installing Authentic DECTalk Server + +This guide shows how to install and run the authentic DECTalk server with the Moonbase Alpha voice. + +## Quick Install + +```bash +cd ~/devcogs/ttstoy/dectalk-server +npm install +``` + +This installs the `dectalk` npm package which includes the authentic DECTalk binaries. + +## What You Get + +The `dectalk` package provides: +- Perfect Paul voice (the Moonbase Alpha voice) +- Authentic DECTalk synthesis engine +- Support for DECTalk phoneme commands +- Same voice used by Stephen Hawking + +## Start the Server + +```bash +PORT=3001 npm start +``` + +Or use the start script: +```bash +./start.sh +``` + +## Test It + +```bash +# Test health +curl http://127.0.0.1:3001/health + +# Generate audio +curl "http://127.0.0.1:3001/say?text=aeiou" -o aeiou.mp3 +ffplay aeiou.mp3 +``` + +## Configure TTSTOY + +In Discord: +``` +[p]ttstoy mode dectalk +[p]ttstoy dectalkurl http://127.0.0.1:3001 +[p]tts aeiou john madden +``` + +## Troubleshooting + +### "Cannot find module 'dectalk'" + +Run `npm install` in the dectalk-server directory. + +### Port already in use + +Use a different port: +```bash +PORT=3002 npm start +``` + +### Linux dependency issues + +The dectalk package may need additional libraries: +```bash +sudo apt install libasound2 +``` + +## DECTalk Commands + +You can use DECTalk phoneme commands in your text: + +``` +[p]tts [:phoneme on] hehlow werld +[p]tts [:rate 200] speaking very fast +[p]tts [:dv ap 100] aeiou +``` + +These commands control pitch, rate, and other voice parameters just like in Moonbase Alpha. diff --git a/ttstoy/dectalk-server/INTEGRATION.md b/ttstoy/dectalk-server/INTEGRATION.md new file mode 100644 index 0000000..bf34de4 --- /dev/null +++ b/ttstoy/dectalk-server/INTEGRATION.md @@ -0,0 +1,186 @@ +# Integration with RedBot TTSTOY + +This guide shows how to integrate the DECTalk server with your RedBot TTSTOY cog. + +## Quick Start + +### 1. Install and Start DECTalk Server + +```bash +cd dectalk-server +npm install +npm start +``` + +The server will start on `http://127.0.0.1:3000` + +### 2. Configure TTSTOY + +In your Discord server, run these commands: + +``` +[p]ttstoy mode dectalk +[p]ttstoy dectalkurl http://127.0.0.1:3000 +``` + +### 3. Test It + +``` +[p]tts Hello, I am DECTalk, a text to speech system. +``` + +## API Compatibility + +The DECTalk server implements the same API format as your local-tts server: + +### Endpoints + +| Endpoint | Method | Purpose | Compatible With | +|----------|--------|---------|-----------------| +| `/health` | GET | Health check | local-tts | +| `/engine` | GET | Engine info | local-tts | +| `/say?text=` | GET | Simple TTS | DECTalk-specific | +| `/v1/t2a_v2` | POST | MiniMax-compatible TTS | TTSTOY cog | + +### Response Format + +The `/v1/t2a_v2` endpoint returns the exact same format as MiniMax API: + +```json +{ + "base_resp": { + "status_code": 0, + "status_msg": "Success" + }, + "data": { + "audio": "", + "audio_id": "" + } +} +``` + +This matches the format expected by `_save_minimax_tts()` in your TTSTOY cog. + +## How It Works + +1. **Text Input**: TTSTOY sends text via POST to `/v1/t2a_v2` +2. **DECTalk Generation**: Server generates WAV audio using DECTalk +3. **MP3 Conversion**: ffmpeg converts WAV to MP3 (mono, 32kHz, 128kbps) +4. **Hex Encoding**: MP3 is converted to hex string (MiniMax format) +5. **Response**: Server returns hex-encoded audio in MiniMax-compatible JSON + +## Switching Between TTS Modes + +You can easily switch between different TTS engines: + +```bash +# Use MiniMax cloud API +[p]ttstoy mode minimax + +# Use local AI TTS +[p]ttstoy mode local +[p]ttstoy localurl http://127.0.0.1:8000 + +# Use DECTalk +[p]ttstoy mode dectalk +[p]ttstoy dectalkurl http://127.0.0.1:3000 +``` + +## Features Supported + +โœ… Text-to-speech synthesis +โœ… Emoji SFX (handled by TTSTOY cog) +โœ… Audio concatenation (handled by TTSTOY cog) +โœ… Volume control (handled by TTSTOY cog) +โŒ Voice selection (DECTalk has one voice) +โŒ Speed control (not implemented yet) + +## Troubleshooting + +### Server won't start + +Check if port 3000 is already in use: +```bash +lsof -i :3000 +``` + +Use a different port: +```bash +PORT=8080 npm start +``` + +### "Cannot reach DECTalk API" + +1. Check if server is running: +```bash +curl http://127.0.0.1:3000/health +``` + +2. Check server logs for errors + +3. Verify ffmpeg is installed: +```bash +ffmpeg -version +``` + +### Audio quality issues + +The server outputs MP3 at: +- Sample rate: 32kHz +- Bitrate: 128kbps +- Channels: Mono + +This matches TTSTOY's expectations. If you need different settings, modify the `convertToMP3()` function in `server.js`. + +## Production Deployment + +### Using systemd + +1. Edit `dectalk-server.service` with your paths +2. Copy to systemd: +```bash +sudo cp dectalk-server.service /etc/systemd/system/ +``` + +3. Enable and start: +```bash +sudo systemctl daemon-reload +sudo systemctl enable dectalk-server +sudo systemctl start dectalk-server +sudo systemctl status dectalk-server +``` + +### Using PM2 + +```bash +npm install -g pm2 +pm2 start server.js --name dectalk-server +pm2 save +pm2 startup +``` + +## Performance Notes + +- Each TTS request generates a new MP3 file +- Files are saved to `output/` directory +- Consider adding cleanup for old files +- DECTalk is fast and lightweight compared to AI TTS + +## Comparison with local-tts + +| Feature | local-tts | dectalk-server | +|---------|-----------|----------------| +| Voice Quality | Natural (AI) | Robotic (classic) | +| Speed | Slower | Faster | +| Resources | High (GPU recommended) | Low | +| Voice Cloning | Yes | No | +| API Key | No | No | +| Setup Complexity | High | Low | + +## Next Steps + +- Replace `say` command with actual DECTalk binary for authentic voice +- Add voice parameter support (if using multi-voice DECTalk) +- Implement speed control +- Add audio file cleanup +- Add rate limiting diff --git a/ttstoy/dectalk-server/README.md b/ttstoy/dectalk-server/README.md new file mode 100644 index 0000000..3b9830f --- /dev/null +++ b/ttstoy/dectalk-server/README.md @@ -0,0 +1,237 @@ +# DECTalk TTS Server + +A simple Node.js server that provides DECTalk text-to-speech with MiniMax-compatible API endpoints for RedBot TTSTOY cog. + +## Features + +- Authentic DECTalk voice (Perfect Paul - same as Moonbase Alpha) +- MiniMax-compatible API endpoint (`/v1/t2a_v2`) for seamless integration with TTSTOY +- Simple GET endpoint (`/say?text=`) for testing +- Automatic WAV to MP3 conversion using ffmpeg +- Health check endpoint (`/health`) + +## Requirements + +- Node.js (v14 or higher) +- npm +- ffmpeg (for audio conversion) +- Linux system (DECTalk package doesn't support macOS) + +### Installing Dependencies + +**Ubuntu/Debian:** +```bash +sudo apt install nodejs npm ffmpeg +``` + +**Fedora:** +```bash +sudo dnf install nodejs npm ffmpeg +``` + +**Arch Linux:** +```bash +sudo pacman -S nodejs npm ffmpeg +``` + +**macOS:** +```bash +brew install node ffmpeg +# macOS has built-in 'say' command, no need for espeak +``` + +## Installation + +```bash +cd dectalk-server +npm install +``` + +This will install: +- `express` - Web server framework +- `dectalk` - Authentic DECTalk voice synthesis (Moonbase Alpha) + +The `dectalk` package includes the actual DECTalk binaries and will work on Linux systems. + +## Usage + +### Start the server + +```bash +npm start +``` + +The server will run on port 3000 by default. + +### Custom port + +```bash +PORT=8080 npm start +``` + +### Custom output directory + +```bash +OUTPUT_DIR=/path/to/output npm start +``` + +## API Endpoints + +### Health Check +```bash +GET /health +``` + +Returns: +```json +{"status": "ok"} +``` + +### Engine Info +```bash +GET /engine +``` + +Returns: +```json +{ + "type": "dectalk", + "version": "1.0.0", + "description": "DECTalk Text-to-Speech Engine" +} +``` + +### Simple TTS (GET) +```bash +GET /say?text=Hello%20world +``` + +Returns: MP3 audio file + +### MiniMax-Compatible TTS (POST) +```bash +POST /v1/t2a_v2 +Content-Type: application/json + +{ + "text": "Hello world", + "voice_setting": { + "voice_id": "default", + "speed": 1.0 + } +} +``` + +Returns: +```json +{ + "base_resp": { + "status_code": 0, + "status_msg": "Success" + }, + "data": { + "audio": "", + "audio_id": "" + } +} +``` + +## Integration with RedBot TTSTOY + +1. Start the DECTalk server: +```bash +npm start +``` + +2. Configure TTSTOY to use DECTalk mode: +``` +[p]ttstoy mode dectalk +[p]ttstoy dectalkurl http://127.0.0.1:3000 +``` + +3. Test it: +``` +[p]tts Hello, I am DECTalk +``` + +## Notes + +- Uses authentic DECTalk (Perfect Paul voice from Moonbase Alpha) +- The `dectalk` npm package includes the actual DECTalk binaries +- Audio files are saved to the `output/` directory +- MP3 format: mono, 32kHz, 128kbps (matches TTSTOY expectations) +- Supports all 9 original DECTalk voices +- Supports DECTalk phoneme commands (e.g., `[:phoneme on]`, `[:rate 200]`) + +## DECTalk Commands + +DECTalk supports special commands for controlling speech: + +``` +[:phoneme on] - Enable phoneme mode +[:rate 200] - Set speech rate (default 180) +[:dv ap 100] - Set voice parameters +``` + +Example: +```bash +curl "http://127.0.0.1:3001/say?text=%5B:rate%20150%5DHello%20world&voice=paul" -o test.mp3 +``` + +You can also use these in Discord: +``` +[p]tts [:rate 200] speaking very fast +[p]tts [:phoneme on] hehlow werld +``` + +## DECTalk Voices + +DECTalk includes 9 classic voices: + +| Voice | Name | Description | +|-------|------|-------------| +| Paul | Perfect Paul | Default male voice (Moonbase Alpha, Stephen Hawking) | +| Betty | Beautiful Betty | Female voice | +| Harry | Huge Harry | Deep male voice | +| Frank | Frail Frank | Elderly male voice | +| Dennis | Doctor Dennis | Male voice | +| Kit | Kit the Kid | Child voice | +| Ursula | Uppity Ursula | Female voice | +| Rita | Rough Rita | Female voice | +| Wendy | Whispering Wendy | Soft female voice | + +### Using Different Voices + +**In Discord:** +``` +[p]ttstoy voice Paul +[p]tts Hello, I am Perfect Paul +[p]ttstoy voice Betty +[p]tts Hello, I am Beautiful Betty +``` + +**Direct API:** +```bash +curl "http://127.0.0.1:3001/say?text=Hello&voice=paul" -o paul.mp3 +curl "http://127.0.0.1:3001/say?text=Hello&voice=betty" -o betty.mp3 +``` + +**List available voices:** +```bash +curl http://127.0.0.1:3001/voices +``` + +## Customization + +The server uses the default DECTalk voice (Perfect Paul). The `dectalk` package handles all the voice synthesis internally, so no additional configuration is needed for the authentic Moonbase Alpha sound. + +## Development + +Run with auto-reload: +```bash +npm run dev +``` + +## License + +MIT diff --git a/ttstoy/dectalk-server/TESTING.md b/ttstoy/dectalk-server/TESTING.md new file mode 100644 index 0000000..7395bac --- /dev/null +++ b/ttstoy/dectalk-server/TESTING.md @@ -0,0 +1,158 @@ +# Testing DECTalk Server with TTSTOY + +Quick testing guide to verify everything works. + +## Step 1: Start the DECTalk Server + +```bash +cd ~/Desktop/ttstoy/dectalk-server +PORT=3001 npm start +``` + +You should see: +``` +DECTalk TTS Server running on http://localhost:3001 +Health check: http://localhost:3001/health +Simple API: http://localhost:3001/say?text=Hello +MiniMax-compatible API: POST http://localhost:3001/v1/t2a_v2 +``` + +## Step 2: Test the API Directly + +Open a new terminal and test: + +```bash +# Test health check +curl http://127.0.0.1:3001/health + +# Test simple endpoint (saves to test.mp3) +curl "http://127.0.0.1:3001/say?text=Hello%20world" -o test.mp3 + +# Play the audio +ffplay test.mp3 +# or +mpv test.mp3 + +# Test MiniMax-compatible endpoint +curl -X POST http://127.0.0.1:3001/v1/t2a_v2 \ + -H "Content-Type: application/json" \ + -d '{"text": "Testing DECTalk compatibility", "voice_setting": {"voice_id": "default"}}' \ + | jq '.base_resp' +``` + +Expected output: +```json +{ + "status_code": 0, + "status_msg": "Success" +} +``` + +## Step 3: Configure TTSTOY + +In Discord: + +``` +[p]ttstoy dectalkurl http://127.0.0.1:3001 +[p]ttstoy mode dectalk +[p]ttstoy mode +``` + +You should see: +``` +Current TTS Mode: ๐Ÿค– DECTALK +DECTalk API URL: http://127.0.0.1:3001 +``` + +## Step 4: Test TTS in Discord + +``` +[p]tts Hello, I am DECTalk, a text to speech system. +``` + +The bot should: +1. Generate audio using DECTalk +2. Upload the MP3 file to Discord +3. Play it in voice chat + +## Step 5: Test with Emoji SFX + +``` +[p]tts Hello ๐ŸŽ‰ this is a party ๐Ÿ”ฅ +``` + +The bot should mix DECTalk voice with sound effects. + +## Troubleshooting + +### Server won't start (port in use) + +```bash +# Find what's using port 3001 +lsof -i :3001 + +# Use a different port +PORT=3002 npm start + +# Update TTSTOY +[p]ttstoy dectalkurl http://127.0.0.1:3002 +``` + +### "Cannot reach DECTalk API" + +1. Check server is running: +```bash +curl http://127.0.0.1:3001/health +``` + +2. Check server logs in the terminal where you started it + +3. Verify URL in TTSTOY: +``` +[p]ttstoy dectalkurl +``` + +### Audio not playing + +1. Make sure ffmpeg is installed: +```bash +ffmpeg -version +``` + +2. Check bot logs for errors + +3. Test the `/say` endpoint directly and play the audio + +### "say: command not found" (Linux) + +The current server uses macOS `say` command. On Linux, you need to: + +1. Install actual DECTalk or use espeak: +```bash +# Option 1: espeak (simple) +sudo apt install espeak + +# Option 2: Install dectalk-tts +npm install -g dectalk-tts +``` + +2. Modify `server.js` to use the appropriate command + +## Success Checklist + +- [ ] DECTalk server starts without errors +- [ ] `/health` endpoint returns `{"status": "ok"}` +- [ ] `/say` endpoint generates audio file +- [ ] TTSTOY mode switches to dectalk +- [ ] `[p]tts` command works in Discord +- [ ] Audio plays in voice chat +- [ ] Emoji SFX work correctly + +## Next Steps + +Once everything works: + +1. Set up auto-start with systemd or PM2 +2. Configure firewall if needed +3. Consider using actual DECTalk binary for authentic voice +4. Add cleanup for old audio files in output/ diff --git a/ttstoy/dectalk-server/VOICES.md b/ttstoy/dectalk-server/VOICES.md new file mode 100644 index 0000000..6e7cb3c --- /dev/null +++ b/ttstoy/dectalk-server/VOICES.md @@ -0,0 +1,69 @@ +# DECTalk Voices Reference + +The 9 original DECTalk voices, each with unique characteristics. + +## Voice List + +| Voice | Description | Type | +|-------|-------------|------| +| paul | Perfect Paul (default, Moonbase Alpha) | Male | +| betty | Beautiful Betty | Female | +| harry | Huge Harry (deep voice) | Male | +| frank | Frail Frank (elderly) | Male | +| dennis | Doctor Dennis | Male | +| kit | Kit the Kid | Child | +| ursula | Uppity Ursula | Female | +| rita | Rough Rita (gravelly) | Female | +| wendy | Whispering Wendy (soft) | Female | + +## Using Voices in Discord + +### Set Your Voice +``` +[p]ttstoy myvoice paul +[p]tts aeiou john madden +``` + +The voice command `[:np]` is automatically prepended to your text. + +### Override Voice Mid-Text +You can use DECTalk commands to change voices within your message: +``` +[p]tts [:nh]I'm gonna eat a pizza. [:dial67589340] Hi, can i order a pizza? [:nv]no! [:nh]why? [:nv] cuz you are john madden![:np] +``` + +Voice commands: +- `[:np]` - Paul +- `[:nb]` - Betty +- `[:nh]` - Harry +- `[:nf]` - Frank +- `[:nd]` - Dennis +- `[:nk]` - Kit +- `[:nu]` - Ursula +- `[:nr]` - Rita +- `[:nw]` - Wendy + +## Testing Voices + +```bash +# Test all voices +for voice in paul betty harry frank dennis kit ursula rita wendy; do + echo "Testing $voice..." + curl "http://127.0.0.1:3001/say?text=Hello%20I%20am%20$voice&voice=$voice" -o "${voice}.mp3" +done +``` + +## In Discord + +``` +# Set voice and speak +[p]ttstoy voice paul +[p]tts aeiou john madden + +# Change voice +[p]ttstoy voice betty +[p]tts Hello, I am Beautiful Betty + +# Override voice in text +[p]tts [:nh]Deep voice [:nb]now female voice [:np]back to paul +``` diff --git a/ttstoy/dectalk-server/dectalk-server.service b/ttstoy/dectalk-server/dectalk-server.service new file mode 100644 index 0000000..2100ed6 --- /dev/null +++ b/ttstoy/dectalk-server/dectalk-server.service @@ -0,0 +1,16 @@ +[Unit] +Description=DECTalk TTS Server +After=network.target + +[Service] +Type=simple +User=YOUR_USER +WorkingDirectory=/path/to/dectalk-server +Environment=PORT=3000 +Environment=OUTPUT_DIR=/path/to/dectalk-server/output +ExecStart=/usr/bin/node /path/to/dectalk-server/server.js +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/ttstoy/dectalk-server/package-lock.json b/ttstoy/dectalk-server/package-lock.json new file mode 100644 index 0000000..852c4e2 --- /dev/null +++ b/ttstoy/dectalk-server/package-lock.json @@ -0,0 +1,1125 @@ +{ + "name": "dectalk-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dectalk-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "dectalk": "^1.0.0", + "express": "^4.18.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dectalk": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dectalk/-/dectalk-1.3.1.tgz", + "integrity": "sha512-5rriTMO7HfGGyMEQfMaQGIJpl6zNyjHWmVJjdkylxEUneQsWIJchVktas7/MRa2BXaYy4h4wT/EUh4hchLir2w==", + "dependencies": { + "tmp": "^0.2.1" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/ttstoy/dectalk-server/package.json b/ttstoy/dectalk-server/package.json new file mode 100644 index 0000000..898853b --- /dev/null +++ b/ttstoy/dectalk-server/package.json @@ -0,0 +1,20 @@ +{ + "name": "dectalk-server", + "version": "1.0.0", + "description": "DECTalk TTS server compatible with RedBot TTSTOY cog", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "keywords": ["dectalk", "tts", "text-to-speech", "redbot", "moonbase-alpha"], + "author": "", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "dectalk": "^1.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/ttstoy/dectalk-server/server.js b/ttstoy/dectalk-server/server.js new file mode 100644 index 0000000..5296d64 --- /dev/null +++ b/ttstoy/dectalk-server/server.js @@ -0,0 +1,209 @@ +#!/usr/bin/env node + +/** + * DECTalk TTS Server + * Compatible with RedBot TTSTOY cog + * + * Uses authentic DECTalk (Moonbase Alpha voice) + * Provides MiniMax-compatible API endpoint at /v1/t2a_v2 + * Also provides simple GET endpoint at /say?text= + */ + +const express = require('express'); +const { say } = require('dectalk'); +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +const app = express(); +const PORT = process.env.PORT || 3000; +const OUTPUT_DIR = process.env.OUTPUT_DIR || path.join(__dirname, 'output'); + +// Ensure output directory exists +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +app.use(express.json()); + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +// Engine info endpoint +app.get('/engine', (req, res) => { + res.json({ + type: 'dectalk', + version: '1.0.0', + description: 'DECTalk Text-to-Speech Engine' + }); +}); + +// Voices endpoint - list available DECTalk voice commands +app.get('/voices', (req, res) => { + const voices = [ + { voice_id: 'paul', command: '[:np]', description: 'Perfect Paul (default, male)' }, + { voice_id: 'betty', command: '[:nb]', description: 'Beautiful Betty (female)' }, + { voice_id: 'harry', command: '[:nh]', description: 'Huge Harry (deep male)' }, + { voice_id: 'frank', command: '[:nf]', description: 'Frail Frank (elderly male)' }, + { voice_id: 'dennis', command: '[:nd]', description: 'Doctor Dennis (male)' }, + { voice_id: 'kit', command: '[:nk]', description: 'Kit the Kid (child)' }, + { voice_id: 'ursula', command: '[:nu]', description: 'Uppity Ursula (female)' }, + { voice_id: 'rita', command: '[:nr]', description: 'Rough Rita (gravelly female)' }, + { voice_id: 'wendy', command: '[:nw]', description: 'Whispering Wendy (soft female)' } + ]; + res.json(voices); +}); + +// Simple GET endpoint for DECTalk +app.get('/say', async (req, res) => { + const text = req.query.text; + + if (!text) { + return res.status(400).send('Missing text parameter'); + } + + try { + const wavBuffer = await generateDECTalk(text); + + // Convert WAV to MP3 using ffmpeg + const mp3Buffer = await convertToMP3(wavBuffer); + + res.set('Content-Type', 'audio/mpeg'); + res.send(mp3Buffer); + } catch (error) { + console.error('DECTalk generation error:', error); + res.status(500).send(`TTS generation failed: ${error.message}`); + } +}); + +// MiniMax-compatible endpoint for RedBot TTSTOY cog +app.post('/v1/t2a_v2', async (req, res) => { + const { text } = req.body; + + if (!text) { + return res.json({ + base_resp: { + status_code: 1002, + status_msg: 'Text is required' + } + }); + } + + console.log(`Generating DECTalk TTS`); + console.log(`Text: ${text.substring(0, 100)}...`); + + try { + // Generate DECTalk audio - voice is controlled by [:n*] commands in text + const wavBuffer = await generateDECTalk(text); + + // Convert WAV to MP3 + const mp3Buffer = await convertToMP3(wavBuffer); + + // Save to file + const audioId = crypto.randomBytes(16).toString('hex'); + const mp3Path = path.join(OUTPUT_DIR, `${audioId}.mp3`); + fs.writeFileSync(mp3Path, mp3Buffer); + + // Convert to hex (MiniMax format) + const audioHex = mp3Buffer.toString('hex'); + + console.log(`โœ… Generated audio: ${audioId}.mp3 (${mp3Buffer.length} bytes)`); + + // Return MiniMax-compatible response + res.json({ + base_resp: { + status_code: 0, + status_msg: 'Success' + }, + data: { + audio: audioHex, + audio_id: audioId + } + }); + } catch (error) { + console.error('DECTalk generation error:', error); + res.json({ + base_resp: { + status_code: 1005, + status_msg: `TTS generation failed: ${error.message}` + } + }); + } +}); + +/** + * Generate authentic DECTalk audio (Moonbase Alpha voice) + * @param {string} text - Text to synthesize (can include [:n*] voice commands) + * @returns {Promise} WAV audio buffer + */ +async function generateDECTalk(text) { + try { + // Use the authentic DECTalk package + // Voice is controlled by [:n*] commands in the text itself + const wavBuffer = await say(text); + return wavBuffer; + } catch (error) { + throw new Error(`DECTalk generation failed: ${error.message}`); + } +} + +/** + * Convert WAV buffer to MP3 using ffmpeg + * @param {Buffer} wavBuffer - WAV audio buffer + * @returns {Promise} MP3 audio buffer + */ +function convertToMP3(wavBuffer) { + return new Promise((resolve, reject) => { + const ffmpeg = spawn('ffmpeg', [ + '-f', 'wav', + '-i', 'pipe:0', + '-f', 'mp3', + '-ac', '1', + '-ar', '32000', + '-b:a', '128k', + 'pipe:1' + ]); + + const chunks = []; + + ffmpeg.stdout.on('data', (chunk) => { + chunks.push(chunk); + }); + + ffmpeg.stderr.on('data', (data) => { + // ffmpeg outputs progress to stderr, ignore it + }); + + ffmpeg.on('close', (code) => { + if (code !== 0) { + reject(new Error(`ffmpeg process exited with code ${code}`)); + } else { + resolve(Buffer.concat(chunks)); + } + }); + + ffmpeg.on('error', (err) => { + reject(new Error(`Failed to start ffmpeg: ${err.message}`)); + }); + + // Write WAV data to ffmpeg stdin + ffmpeg.stdin.write(wavBuffer); + ffmpeg.stdin.end(); + }); +} + +// Start server +app.listen(PORT, () => { + console.log(`๐Ÿค– DECTalk TTS Server running on http://localhost:${PORT}`); + console.log(`Health check: http://localhost:${PORT}/health`); + console.log(`Voices: http://localhost:${PORT}/voices`); + console.log(`Simple API: http://localhost:${PORT}/say?text=Hello`); + console.log(`MiniMax-compatible API: POST http://localhost:${PORT}/v1/t2a_v2`); + console.log(''); + console.log('Voice commands (use in text):'); + console.log(' [:np] Paul [:nb] Betty [:nh] Harry [:nf] Frank [:nd] Dennis'); + console.log(' [:nk] Kit [:nu] Ursula [:nr] Rita [:nw] Wendy'); +}); diff --git a/ttstoy/dectalk-server/start.sh b/ttstoy/dectalk-server/start.sh new file mode 100755 index 0000000..64ecdea --- /dev/null +++ b/ttstoy/dectalk-server/start.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Quick start script for DECTalk server + +echo "๐Ÿค– Starting DECTalk TTS Server..." + +# Check if node_modules exists +if [ ! -d "node_modules" ]; then + echo "๐Ÿ“ฆ Installing dependencies (including authentic DECTalk)..." + npm install +fi + +# Check if ffmpeg is available +if ! command -v ffmpeg &> /dev/null; then + echo "โŒ Error: ffmpeg is not installed" + echo "Install it with:" + echo " - Ubuntu/Debian: sudo apt install ffmpeg" + echo " - Fedora: sudo dnf install ffmpeg" + echo " - Arch: sudo pacman -S ffmpeg" + exit 1 +fi + +# Check if dectalk package is installed +if [ ! -d "node_modules/dectalk" ]; then + echo "โŒ Error: dectalk package not installed" + echo "Run: npm install" + exit 1 +fi + +# Start the server +echo "โœ… Starting server on port ${PORT:-3000}..." +echo "๐ŸŽ™๏ธ Using authentic DECTalk (Moonbase Alpha voice)" +npm start diff --git a/ttstoy/dectalk-server/test_api.sh b/ttstoy/dectalk-server/test_api.sh new file mode 100755 index 0000000..966d2ee --- /dev/null +++ b/ttstoy/dectalk-server/test_api.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Test script for DECTalk server API + +BASE_URL="${1:-http://127.0.0.1:3000}" + +echo "๐Ÿงช Testing DECTalk Server at $BASE_URL" +echo "" + +# Test 1: Health check +echo "1๏ธโƒฃ Testing /health endpoint..." +curl -s "$BASE_URL/health" | jq '.' || echo "โŒ Health check failed" +echo "" + +# Test 2: Engine info +echo "2๏ธโƒฃ Testing /engine endpoint..." +curl -s "$BASE_URL/engine" | jq '.' || echo "โŒ Engine info failed" +echo "" + +# Test 3: Simple GET endpoint +echo "3๏ธโƒฃ Testing /say endpoint (saving to test_say.mp3)..." +curl -s "$BASE_URL/say?text=Hello%20world" -o test_say.mp3 +if [ -f test_say.mp3 ] && [ -s test_say.mp3 ]; then + echo "โœ… Audio saved to test_say.mp3 ($(stat -f%z test_say.mp3 2>/dev/null || stat -c%s test_say.mp3) bytes)" +else + echo "โŒ Failed to generate audio" +fi +echo "" + +# Test 4: MiniMax-compatible endpoint +echo "4๏ธโƒฃ Testing /v1/t2a_v2 endpoint (MiniMax-compatible)..." +curl -s -X POST "$BASE_URL/v1/t2a_v2" \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Testing MiniMax compatibility", + "voice_setting": { + "voice_id": "default", + "speed": 1.0 + } + }' | jq '.base_resp, .data.audio_id' || echo "โŒ MiniMax endpoint failed" +echo "" + +echo "โœ… All tests complete!" +echo "" +echo "To test audio playback:" +echo " ffplay test_say.mp3" +echo " # or" +echo " mpv test_say.mp3" diff --git a/ttstoy/info.json b/ttstoy/info.json new file mode 100755 index 0000000..563937d --- /dev/null +++ b/ttstoy/info.json @@ -0,0 +1,10 @@ +{ + "author": ["Kingston"], + "install_msg": "TtsToy (MiniMax) cog loaded.", + "name": "TtsToy", + "short": "MiniMax text-to-speech with emoji-triggered SFX and MorshuTalk.", + "description": "Uses MiniMax TTS, DECTalk, and MorshuTalk with optional emoji/custom-emoji SFX segments for voice channel playback.", + "requirements": ["requests", "g2p_en", "numpy", "pydub"], + "tags": ["tts", "minimax", "voice", "audio", "morshu"], + "min_bot_version": "3.5.0" +} diff --git a/ttstoy/morshutalk_engine.py b/ttstoy/morshutalk_engine.py new file mode 100644 index 0000000..2f0c333 --- /dev/null +++ b/ttstoy/morshutalk_engine.py @@ -0,0 +1,306 @@ +""" +Bundled MorshuTalk engine for ttstoy. +Based on MorshuTalk by jalenluorion (https://github.com/jalenluorion/MorshuTalk) +Generates speech audio from Morshu's voice lines using phoneme matching. +""" +import re +import unicodedata +from os import path +from typing import List, Tuple, Callable, Literal + +import numpy as np +import random +import warnings +from pydub import AudioSegment + +# --------------------------------------------------------------------------- +# G2P (Grapheme-to-Phoneme) wrapper with progress + cancel support +# --------------------------------------------------------------------------- +import nltk +for _res in ('averaged_perceptron_tagger_eng', 'cmudict', 'punkt', 'punkt_tab'): + try: + nltk.data.find(f'taggers/{_res}' if 'tagger' in _res else _res) + except LookupError: + nltk.download(_res, quiet=True) + +from g2p_en.g2p import G2p, unicode, normalize_numbers, word_tokenize, pos_tag + + +class G2pProgress(G2p): + def __init__(self): + super().__init__() + self.cancelled = False + + def cancel(self): + self.cancelled = True + + def run_with_progress(self, text, callback: Callable[[int, int], None] = None): + self.cancelled = False + + text = unicode(text) + text = normalize_numbers(text) + text = ''.join(char for char in unicodedata.normalize('NFD', text) + if unicodedata.category(char) != 'Mn') + text = text.lower() + text = re.sub("[^ a-z'.,?!\\-]", "", text) + text = text.replace("i.e.", "that is") + text = text.replace("e.g.", "for example") + + words = word_tokenize(text) + tokens = pos_tag(words) + + step = 0 + total = len(tokens) + prons = [] + + for word, pos in tokens: + if self.cancelled: + return + + if callback: + callback(step, total) + step += 1 + + if re.search("[a-z]", word) is None: + pron = [word] + elif word in self.homograph2features: + pron1, pron2, pos1 = self.homograph2features[word] + pron = pron1 if pos.startswith(pos1) else pron2 + elif word in self.cmu: + pron = self.cmu[word][0] + else: + pron = self.predict(word) + + prons.extend(pron) + prons.extend([" "]) + + if callback: + callback(total, total) + + return prons[:-1] + + +# --------------------------------------------------------------------------- +# Singleton G2P instance and audio data +# --------------------------------------------------------------------------- +g2p = G2pProgress() + +_WAV_PATH = path.join(path.dirname(__file__), 'morshutalk_morshu.wav') +morshu_wav = AudioSegment.from_wav(_WAV_PATH) + +# Phoneme timing record from the morshu audio +morshu_rec = np.rec.array([ + ('', 160, 0), ('L', 250, 2), ('AE', 348, 2), ('M', 420, 2), ('P', 510, 1), + ('OY', 700, 2), ('L', 835, 1), ('', 1090, 0), + ('R', 1180, 2), ('OW', 1300, 2), ('', 1390, 0), ('P', 1490, 2), ('', 1850, 0), + ('B', 1895, 2), ('AA', 2090, 2), ('M', 2235, 2), ('Z', 2390, 2), + ('', 2780, 0), ('Y', 2840, 2), ('UW', 2960, 2), + ('W', 3030, 2), ('AA', 3110, 2), ('N', 3150, 1), ('IH', 3240, 2), ('T', 3370, 2), ('', 3810, 0), + ('IH', 3960, 2), ('T', 4070, 2), ('Y', 4260, 2), ('UH', 4400, 2), ('R', 4510, 2), ('Z', 4600, 2), + ('M', 4675, 2), ('AY', 4810, 2), ('', 4885, 0), + ('F', 4930, 2), ('R', 4980, 2), ('EH', 5100, 2), ('N', 5240, 2), ('D', 5300, 2), ('', 5520, 0), + ('AE', 5630, 2), ('Z', 5740, 2), ('L', 5870, 2), ('AO', 6000, 2), ('NG', 6140, 2), + ('AE', 6170, 1), ('Z', 6265, 2), ('Y', 6300, 2), ('UW', 6380, 2), + ('HH', 6450, 2), ('AE', 6510, 1), ('V', 6580, 2), + ('IH', 6640, 2), ('N', 6670, 2), ('AH', 6747, 2), ('F', 6855, 2), + ('R', 6960, 2), ('UW', 7060, 2), ('B', 7170, 1), ('IY', 7340, 2), ('Z', 7520, 2), ('', 8236, 0), + ('S', 8407, 2), ('AA', 8495, 2), ('R', 8570, 2), ('IY', 8630, 1), + ('L', 8740, 2), ('IH', 8811, 2), ('NG', 8942, 2), ('K', 9014, 2), ('', 9251, 0), + ('AY', 9384, 2), ('', 9467, 0), ('K', 9512, 2), ('AE', 9640, 2), ('N', 9716, 2), ('', 9844, 0), + ('G', 9894, 2), ('IH', 9985, 2), ('V', 10060, 2), ('', 10149, 0), + ('K', 10256, 2), ('R', 10297, 2), ('EH', 10383, 2), ('IH', 10482, 1), ('', 10564, 0), ('T', 10617, 2), + ('', 10962, 0), ('K', 11019, 2), ('AH', 11100, 2), ('M', 11229, 2), ('B', 11246, 2), ('AE', 11369, 2), + ('', 11511, 0), ('W', 11590, 2), ('EH', 11622, 1), ('N', 11705, 2), + ('Y', 11755, 2), ('UH', 11808, 2), ('R', 11864, 2), ('AH', 11959, 2), + ('L', 12095, 2), ('IH', 12202, 2), ('L', 12386, 2), + ('', 12596, 0), ('M', 12748, 2), ('M', 12888, 2), ('M', 13037, 2), ('M', 13196, 2), ('', 13426, 0), + ('R', 13494, 2), ('IH', 13589, 2), ('', 13632, 0), ('CH', 13773, 2), ('ER', 13991, 2), ('', 13992, 0) +], names=('phoneme', 'timing', 'priority')) + +similar_phonemes = { + 'AW': ['AE', 'UW'], + 'DH': ['D'], + 'EY': ['EH', 'IY'], + 'JH': ['CH'], + 'SH': ['CH'], + 'TH': ['D'], + 'ZH': ['CH'], +} + + +# --------------------------------------------------------------------------- +# Morshu TTS Engine +# --------------------------------------------------------------------------- +class Morshu: + def __init__(self): + self.input_str = "" + self.input_phonemes = [] + self.stop_chars = '.,?!:;()\n' + self.space_length = 20 + self.stop_length = 100 + self.use_phoneme_priority = True + self.out_audio = AudioSegment.empty() + self.audio_segment_timings = np.rec.array((0, 0), names=('output', 'morshu')) + self.canceled = False + + def cancel(self): + g2p.cancel() + self.canceled = True + + def load_text(self, text: str = None, progress_callback: Callable[[int, int, int], None] = None) \ + -> AudioSegment | Literal[False]: + """Generate audio from text. Returns AudioSegment or False if cancelled.""" + self.canceled = False + + if progress_callback is None: + progress_callback = lambda major_step, minor_step, minor_total: None + + if text is None: + text = self.input_str + self.input_str = text + text = text.replace('\n', ',,,') + + phonemes = g2p.run_with_progress(text, lambda step, total: progress_callback(0, step, total)) + if g2p.cancelled: + return False + + progress_step = 0 + progress_total = len(phonemes) + + output = AudioSegment.empty().set_frame_rate(morshu_wav.frame_rate) + audio_out_millis = [] + audio_morshu_millis = [] + phoneme_segment = [] + + while len(phonemes) > 0: + if self.canceled: + return False + + progress_callback(1, progress_step, progress_total) + progress_step += 1 + + p = phonemes.pop(0) + if p in g2p.phonemes: + phoneme_segment.append(p) + if p not in g2p.phonemes or len(phonemes) == 0: + output = self.append_best_morshu_phoneme_segment( + output, phoneme_segment, audio_out_millis, audio_morshu_millis) + phoneme_segment = [] + if p == ' ': + output = self.append_audio_segment( + output, AudioSegment.silent(self.space_length), -1, + audio_out_millis, audio_morshu_millis) + elif p in self.stop_chars: + output = self.append_audio_segment( + output, AudioSegment.silent(self.stop_length), -1, + audio_out_millis, audio_morshu_millis) + + if len(output) == 0: + warnings.warn('returned audio segment is empty', UserWarning) + self.audio_segment_timings = np.rec.array((0, 0), names=('output', 'morshu')) + else: + self.audio_segment_timings = np.rec.array( + tuple(zip(audio_out_millis, audio_morshu_millis)), + names=('output', 'morshu')) + + progress_callback(1, progress_total, progress_total) + self.out_audio = output + return output + + @staticmethod + def substitute_similar_phonemes(phonemes: List[str]): + i = 0 + while i < len(phonemes): + if phonemes[i].endswith('0') or phonemes[i].endswith('1') or phonemes[i].endswith('2'): + phonemes[i] = phonemes[i][:len(phonemes[i]) - 1] + if phonemes[i] in similar_phonemes: + phonemes = phonemes[0:i] + similar_phonemes[phonemes[i]] + phonemes[i + 1:] + i += 1 + return phonemes + + @staticmethod + def append_audio_segment(audio_out: AudioSegment, audio_segment: AudioSegment, + morshu_millis_start: int, + audio_out_millis: List[int], + audio_morshu_millis: List[int]) -> AudioSegment: + audio_out_millis.append(len(audio_out)) + audio_morshu_millis.append(morshu_millis_start) + audio_out += audio_segment + return audio_out + + @staticmethod + def get_phoneme_sequence_occurrences(phonemes: List[str]) -> List[Tuple[int, int]]: + occurrences = [] + for i in range(len(morshu_rec) - len(phonemes)): + if (morshu_rec['phoneme'][i:i + len(phonemes)] == phonemes).all(): + start = morshu_rec['timing'][i - 1] + end = morshu_rec['timing'][i + len(phonemes) - 1] + occurrences.append((start, end)) + return occurrences + + def get_best_morshu_single_phoneme(self, phoneme: str, preceding: str = "", + succeeding: str = "") -> Tuple[AudioSegment, int]: + best_indices = [] + phoneme_indices = np.where(morshu_rec['phoneme'] == phoneme)[0] + if len(phoneme_indices) == 0: + return AudioSegment.empty(), 0 + + highest_priority = 0 + for i in phoneme_indices: + morshu_preceding = morshu_rec['phoneme'][i - 1] + priority = morshu_rec['priority'][i] if self.use_phoneme_priority else 0 + + if morshu_preceding == preceding: + priority += 10 + elif any(c in morshu_preceding for c in "AEIOU") and any(c in preceding for c in "AEIOU"): + priority += 5 + + morshu_succeeding = morshu_rec['phoneme'][i + 1] + if morshu_succeeding == succeeding: + priority += 10 + elif any(c in morshu_succeeding for c in "AEIOU") and any(c in succeeding for c in "AEIOU"): + priority += 1 + + if priority < highest_priority: + continue + if priority > highest_priority: + highest_priority = priority + best_indices = [] + best_indices.append(i) + + index = random.choice(best_indices) + segment = morshu_wav[morshu_rec['timing'][index - 1]: morshu_rec['timing'][index]] + return segment, morshu_rec['timing'][index - 1] + + def append_best_morshu_phoneme_segment(self, output: AudioSegment, phonemes: List[str], + audio_out_millis: List[int] = None, + audio_morshu_millis: List[int] = None) -> AudioSegment: + phonemes = Morshu.substitute_similar_phonemes(phonemes) + if len(phonemes) == 1: + segment, start = self.get_best_morshu_single_phoneme(phonemes[0]) + return Morshu.append_audio_segment(output, segment, start, audio_out_millis, audio_morshu_millis) + + preceding = "" + while len(phonemes) > 0: + sequence_length = 1 + segment = AudioSegment.empty() + start = 0 + + while sequence_length <= len(phonemes): + occurrences = Morshu.get_phoneme_sequence_occurrences(phonemes[:sequence_length]) + if len(occurrences) == 0: + break + start, end = random.choice(occurrences) + segment = morshu_wav[start:end] + sequence_length += 1 + sequence_length -= 1 + + if sequence_length == 1: + succeeding = phonemes[sequence_length + 1] if sequence_length + 1 < len(phonemes) else "" + segment, start = self.get_best_morshu_single_phoneme(phonemes[0], preceding, succeeding) + + output = Morshu.append_audio_segment(output, segment, start, audio_out_millis, audio_morshu_millis) + preceding = phonemes[sequence_length - 1] + del phonemes[:sequence_length] + + return output diff --git a/ttstoy/morshutalk_morshu.wav b/ttstoy/morshutalk_morshu.wav new file mode 100644 index 0000000..e0db031 Binary files /dev/null and b/ttstoy/morshutalk_morshu.wav differ diff --git a/ttstoy/sfx/airhorn/airhorn-lethal-company.mp3 b/ttstoy/sfx/airhorn/airhorn-lethal-company.mp3 new file mode 100644 index 0000000..d1950ba Binary files /dev/null and b/ttstoy/sfx/airhorn/airhorn-lethal-company.mp3 differ diff --git a/ttstoy/sfx/angel/heaven.mp3 b/ttstoy/sfx/angel/heaven.mp3 new file mode 100644 index 0000000..f41bef1 Binary files /dev/null and b/ttstoy/sfx/angel/heaven.mp3 differ diff --git a/ttstoy/sfx/boathorn/boathorn.mp3 b/ttstoy/sfx/boathorn/boathorn.mp3 new file mode 100644 index 0000000..94addb9 Binary files /dev/null and b/ttstoy/sfx/boathorn/boathorn.mp3 differ diff --git a/ttstoy/sfx/cashew/cashew.mp3 b/ttstoy/sfx/cashew/cashew.mp3 new file mode 100644 index 0000000..bb49a9e Binary files /dev/null and b/ttstoy/sfx/cashew/cashew.mp3 differ diff --git a/ttstoy/sfx/check/ding-sound-effect_2.mp3 b/ttstoy/sfx/check/ding-sound-effect_2.mp3 new file mode 100644 index 0000000..b802072 Binary files /dev/null and b/ttstoy/sfx/check/ding-sound-effect_2.mp3 differ diff --git a/ttstoy/sfx/clap/applause-sound-effect.mp3 b/ttstoy/sfx/clap/applause-sound-effect.mp3 new file mode 100644 index 0000000..ba0c0b3 Binary files /dev/null and b/ttstoy/sfx/clap/applause-sound-effect.mp3 differ diff --git a/ttstoy/sfx/drum/byong.mp3 b/ttstoy/sfx/drum/byong.mp3 new file mode 100644 index 0000000..6ce73be Binary files /dev/null and b/ttstoy/sfx/drum/byong.mp3 differ diff --git a/ttstoy/sfx/drum/byong.wav b/ttstoy/sfx/drum/byong.wav new file mode 100644 index 0000000..e19e722 Binary files /dev/null and b/ttstoy/sfx/drum/byong.wav differ diff --git a/ttstoy/sfx/emoji_map.json b/ttstoy/sfx/emoji_map.json new file mode 100644 index 0000000..44aebc0 --- /dev/null +++ b/ttstoy/sfx/emoji_map.json @@ -0,0 +1,19 @@ +{ + "๐ŸŽ‰": "party", + "๐Ÿ˜‚": "laugh", + "๐Ÿฅ–": "spy", + "๐Ÿ‘": "clap", + "๐Ÿ”ฅ": "fire", + "๐Ÿ’€": "skull", + "โœ…": "check", + "โŒ": "error", + "๐Ÿ“ข": "airhorn", + "๐Ÿšข": "boathorn", + "๐Ÿ˜ถ": "drum", + "๐Ÿ‘ผ": "angel", + "๐Ÿฅœ": "cashew", + "๐Ÿ’ช": "physical", + "๐Ÿง ": "intelligence", + "๐Ÿ‘": "psychic", + "โœ๏ธ": "motor" +} diff --git a/ttstoy/sfx/error/erro-win-7_3OFHeWK.mp3 b/ttstoy/sfx/error/erro-win-7_3OFHeWK.mp3 new file mode 100644 index 0000000..0e1b492 Binary files /dev/null and b/ttstoy/sfx/error/erro-win-7_3OFHeWK.mp3 differ diff --git a/ttstoy/sfx/fire/fire-whoosh.mp3 b/ttstoy/sfx/fire/fire-whoosh.mp3 new file mode 100644 index 0000000..37c8833 Binary files /dev/null and b/ttstoy/sfx/fire/fire-whoosh.mp3 differ diff --git a/ttstoy/sfx/intelligence/interface-skill-passiveINT-04-01 (2).wav b/ttstoy/sfx/intelligence/interface-skill-passiveINT-04-01 (2).wav new file mode 100644 index 0000000..eef0c4d Binary files /dev/null and b/ttstoy/sfx/intelligence/interface-skill-passiveINT-04-01 (2).wav differ diff --git a/ttstoy/sfx/motor/interface-skill-passiveMOT-04-01 (1).wav b/ttstoy/sfx/motor/interface-skill-passiveMOT-04-01 (1).wav new file mode 100644 index 0000000..a397ccb Binary files /dev/null and b/ttstoy/sfx/motor/interface-skill-passiveMOT-04-01 (1).wav differ diff --git a/ttstoy/sfx/party/confetti-pop-sound.mp3 b/ttstoy/sfx/party/confetti-pop-sound.mp3 new file mode 100644 index 0000000..fd4c525 Binary files /dev/null and b/ttstoy/sfx/party/confetti-pop-sound.mp3 differ diff --git a/ttstoy/sfx/physical/interface-skill-passiveFYS-03-01 (1).wav b/ttstoy/sfx/physical/interface-skill-passiveFYS-03-01 (1).wav new file mode 100644 index 0000000..d6cc9c4 Binary files /dev/null and b/ttstoy/sfx/physical/interface-skill-passiveFYS-03-01 (1).wav differ diff --git a/ttstoy/sfx/psychic/interface-skill-passivePSY-04-02.wav b/ttstoy/sfx/psychic/interface-skill-passivePSY-04-02.wav new file mode 100644 index 0000000..ffc6cce Binary files /dev/null and b/ttstoy/sfx/psychic/interface-skill-passivePSY-04-02.wav differ diff --git a/ttstoy/sfx/skull/skullsound2.mp3 b/ttstoy/sfx/skull/skullsound2.mp3 new file mode 100644 index 0000000..fe9666f Binary files /dev/null and b/ttstoy/sfx/skull/skullsound2.mp3 differ diff --git a/ttstoy/sfx/spy/spy-laugh.mp3 b/ttstoy/sfx/spy/spy-laugh.mp3 new file mode 100644 index 0000000..00f014d Binary files /dev/null and b/ttstoy/sfx/spy/spy-laugh.mp3 differ diff --git a/ttstoy/stack.json b/ttstoy/stack.json new file mode 100644 index 0000000..ff667a0 --- /dev/null +++ b/ttstoy/stack.json @@ -0,0 +1,10 @@ +{ + "homepage": "/home/owen/Desktop/Homepage", + "webtable": "/home/owen/Desktop/WebTable", + "porgupicker": "/home/owen/Desktop/PorguPicker", + "galaxy": "/home/owen/Desktop/Something", + "ttstoy": "/home/owen/Desktop/ttstoy", + "ttstoy_webui": "/home/owen/Desktop/ttstoy/webui", + "cookie_domain": ".kingstons-scrapyard.net", + "session_secret": "scrapyard-shared-session-k8x7m2p4q9w1" +} diff --git a/ttstoy/ttstoy.py b/ttstoy/ttstoy.py new file mode 100644 index 0000000..6deffd2 --- /dev/null +++ b/ttstoy/ttstoy.py @@ -0,0 +1,3301 @@ +import os +import shutil +import logging +import asyncio +import subprocess +import re +import threading +import json +from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path +import lavalink +from typing import Optional + +import discord +from discord.ext import tasks +import requests + +from redbot.core import commands, Config +from redbot.core.bot import Red, cog_data_path +from redbot.core.commands import Cog +from redbot.cogs.audio import Audio + +try: + from .morshutalk_engine import Morshu + MORSHU_AVAILABLE = True +except Exception: + try: + from morshutalk_engine import Morshu + MORSHU_AVAILABLE = True + except Exception: + MORSHU_AVAILABLE = False + +try: + from .vox_engine import generate_vox, get_available_words, get_available_packs + VOX_AVAILABLE = True +except Exception: + try: + from vox_engine import generate_vox, get_available_words, get_available_packs + VOX_AVAILABLE = True + except Exception: + VOX_AVAILABLE = False + +log = logging.getLogger("red.ttstoy") + +# ---------------------------- +# MiniMax defaults +# ---------------------------- +DEFAULT_MINIMAX_MODEL = "speech-01-turbo" +DEFAULT_MINIMAX_VOICE = "moss_audio_b304011b-11b2-11f1-90fe-36953c023630" + +MINIMAX_VOICES = { + "BigMan": "moss_audio_54f969bf-12c1-11f1-93de-a6f4120d2cc7", + "BlueGnome": "moss_audio_166dfe7c-1390-11f1-b6f2-729162d0a8d2", + "Dracafow": "moss_audio_13cb66d2-12c7-11f1-b6f2-729162d0a8d2", + "Gaben": "moss_audio_5239ca93-115b-11f1-b9c4-4ea5324904c7", + "Grigori": "moss_audio_63a397aa-24c9-11f1-918f-5a2de67f838a", + "Gnome": "moss_audio_b304011b-11b2-11f1-90fe-36953c023630", + "King": "moss_audio_39c16b2c-1158-11f1-995d-b25e2f30d3fa", + "Peppa": "moss_audio_d656489f-114b-11f1-995d-b25e2f30d3fa", + "Robotnik": "moss_audio_123443a8-11b4-11f1-bfa6-763108879732", +} + +DECTALK_VOICES = { + "paul": "paul", # Perfect Paul (default, male) + "betty": "betty", # Beautiful Betty (female) + "harry": "harry", # Huge Harry (male) + "frank": "frank", # Frail Frank (male) + "dennis": "dennis", # Doctor Dennis (male) + "kit": "kit", # Kit the Kid (child) + "ursula": "ursula", # Uppity Ursula (female) + "rita": "rita", # Rough Rita (female) + "wendy": "wendy", # Whispering Wendy (female) +} + +CHATTERBOX_DEFAULT_URL = "http://127.0.0.1:8099" + +_DEFAULT_EMOJI_SFX = { + "๐ŸŽ‰": "party", + "๐Ÿ˜‚": "laugh", + "๐Ÿฅ–": "spy", + "๐Ÿ‘": "clap", + "๐Ÿ”ฅ": "fire", + "๐Ÿ’€": "skull", + "โœ…": "check", + "โŒ": "error", + "๐Ÿ“ข": "airhorn", + "๐Ÿšข": "boathorn", + "๐Ÿ˜ถ": "drum", + "๐Ÿ‘ผ": "angel", + "๐Ÿฅœ": "cashew", + "๐Ÿ’ช": "physical", + "๐Ÿง ": "intelligence", + "๐Ÿ‘": "psychic", + "โœ๏ธ": "motor", +} + +_SFX_MAP_PATH = Path(__file__).resolve().parent / "sfx" / "emoji_map.json" + + +def _load_emoji_sfx_map() -> dict: + """Load emojiโ†’folder mapping from JSON file, falling back to defaults.""" + try: + if _SFX_MAP_PATH.exists(): + with open(_SFX_MAP_PATH) as f: + return json.load(f) + except Exception: + pass + return dict(_DEFAULT_EMOJI_SFX) + + +def _save_emoji_sfx_map(mapping: dict): + """Persist emojiโ†’folder mapping to JSON file.""" + _SFX_MAP_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(_SFX_MAP_PATH, "w") as f: + json.dump(mapping, f, ensure_ascii=False, indent=2) + + +# Live reference โ€” all code reads from this dict; _reload_emoji_sfx_map() refreshes it. +EMOJI_SFX_FOLDERS: dict = _load_emoji_sfx_map() + + +def _reload_emoji_sfx_map(): + """Reload the mapping from disk into the live dict.""" + EMOJI_SFX_FOLDERS.clear() + EMOJI_SFX_FOLDERS.update(_load_emoji_sfx_map()) + +EMOJI_CANDIDATE_PATTERN = re.compile( + r"(|:[A-Za-z0-9_]+:|[\U0001F300-\U0001FAFF\U00002600-\U000026FF\U00002700-\U000027BF][\uFE0E\uFE0F]?(?:\u200D[\U0001F300-\U0001FAFF\U00002600-\U000026FF\U00002700-\U000027BF][\uFE0E\uFE0F]?)*)" +) + +# Pattern for inline voice switches: [mode|voice] or [mode] (voice optional) +VOICE_SWITCH_PATTERN = re.compile( + r"\[([a-zA-Z]+)(?:\|([^\]]+))?\]" +) + +VALID_VOICE_MODES = {"minimax", "chatterbox", "dectalk", "morshu", "vox"} + + +def resolve_voice_for_mode(mode: str, voice_name: str, chatterbox_voices: list = None) -> str: + """Resolve a voice name/label to the engine-specific voice ID, case-insensitive. + + For chatterbox, pass the list of known voice filenames (e.g. ['123_Emily.wav']). + The function matches by the display portion of the filename (after the first underscore, + without extension), case-insensitively. + """ + name_lower = voice_name.lower().strip() + if mode == "minimax": + for label, vid in MINIMAX_VOICES.items(): + if label.lower() == name_lower: + return vid + return voice_name # pass through raw IDs + if mode == "dectalk": + return DECTALK_VOICES.get(name_lower, name_lower) + if mode == "chatterbox" and chatterbox_voices: + # Try exact match first + for fn in chatterbox_voices: + if fn.lower() == name_lower or fn == voice_name: + return fn + # Match by display name (the part after userid_) + for fn in chatterbox_voices: + parts = fn.split("_", 1) + if len(parts) > 1: + display = parts[1].rsplit(".", 1)[0] # strip extension + else: + display = fn.rsplit(".", 1)[0] + if display.lower() == name_lower: + return fn + return voice_name + + +def _split_voice_segments(text: str): + """ + Split text on [mode|voice] tags into segments. + + Returns a list of (mode_override, voice_override, text) tuples. + mode_override/voice_override are None for the initial segment (use caller defaults). + """ + segments = [] + last_end = 0 + for m in VOICE_SWITCH_PATTERN.finditer(text): + # Text before this tag belongs to the current (last) voice segment + before = text[last_end:m.start()] + if before: + if not segments: + segments.append((None, None, before)) + else: + prev = segments[-1] + segments[-1] = (prev[0], prev[1], prev[2] + before) + + mode_tag = m.group(1).lower() + voice_tag = (m.group(2) or "").strip() + if mode_tag in VALID_VOICE_MODES: + segments.append((mode_tag, voice_tag, "")) + else: + # Invalid mode, treat the whole tag as literal text + if not segments: + segments.append((None, None, m.group(0))) + else: + prev = segments[-1] + segments[-1] = (prev[0], prev[1], prev[2] + m.group(0)) + last_end = m.end() + + # Remaining text after the last tag + tail = text[last_end:] + if tail: + if not segments: + segments.append((None, None, tail)) + else: + prev = segments[-1] + segments[-1] = (prev[0], prev[1], prev[2] + tail) + + if not segments: + segments.append((None, None, text)) + + return segments + + +def resolve_minimax_voice_id(label_or_id: str) -> str: + return MINIMAX_VOICES.get(label_or_id, label_or_id) + + +def resolve_dectalk_voice_id(label_or_id: str) -> str: + # Case-insensitive lookup + label_lower = label_or_id.lower() + return DECTALK_VOICES.get(label_lower, label_lower) + + +def _normalize_sfx_trigger(token: str) -> str: + if token.startswith("<") and token.endswith(">"): + # Discord custom emoji format: <:name:id> or + parts = token.strip("<>").split(":") + if len(parts) == 3: + return f":{parts[1]}:" + return token + + +# --------------------------------------------------------------------- +# HEALTH SERVER +# --------------------------------------------------------------------- +TTSTOY_HEALTH_PORT = 8097 + +class _HealthHandler(BaseHTTPRequestHandler): + cog_ref = None # set by TtsToy + + def do_GET(self): + if self.path == "/health": + cog = _HealthHandler.cog_ref + payload = { + "status": "ok", + "service": "ttstoy", + } + body = json.dumps(payload).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(body) + else: + self.send_response(404) + self.end_headers() + + def log_message(self, format, *args): + pass # suppress access logs + + +# --------------------------------------------------------------------- +# MODAL FOR API KEY ENTRY +# --------------------------------------------------------------------- +class MinimaxKeyModal(discord.ui.Modal, title="Set MiniMax API Key"): + api_key = discord.ui.TextInput( + label="Enter MiniMax API Key", + style=discord.TextStyle.short, + placeholder="paste your API key", + required=True, + ) + + def __init__(self, config: Config): + super().__init__() + self.config = config + + async def on_submit(self, interaction: discord.Interaction): + await self.config.minimax_api_key.set(self.api_key.value.strip()) + await interaction.response.send_message("โœ… MiniMax API key saved.", ephemeral=True) + + +class MinimaxKeyButton(discord.ui.View): + def __init__(self, config: Config): + super().__init__(timeout=None) + self.config = config + + @discord.ui.button(label="Enter MiniMax API Key", style=discord.ButtonStyle.primary) + async def enter_key(self, interaction: discord.Interaction, button: discord.ui.Button): # type: ignore[override] + modal = MinimaxKeyModal(self.config) + await interaction.response.send_modal(modal) + + +# --------------------------------------------------------------------- +# MAIN COG +# --------------------------------------------------------------------- +class TtsToy(Cog): + """Multi-engine TTS with voice cloning, emoji SFX, and a web UI. + + Engines: MiniMax ยท Chatterbox ยท DECTalk ยท Morshu ยท VOX + """ + + def __init__(self, bot: Red): + super().__init__() + self.bot = bot + + self.tts_storage = cog_data_path(cog_instance=self).joinpath("ttstoy") + self.sfx_root = Path(__file__).resolve().parent / "sfx" + self.dectalk_dir = Path(__file__).resolve().parent / "dectalk-server" + self.dectalk_process = None + self.dectalk_port = 3001 + + # Morshu TTS engine + self.morshu = Morshu() if MORSHU_AVAILABLE else None + + # TTS queue system - one queue per guild + self.tts_queues = {} # guild_id -> asyncio.Queue + self.tts_locks = {} # guild_id -> asyncio.Lock + self.tts_processors = {} # guild_id -> asyncio.Task + + self._ensure_sfx_folders() + self.clear_old_tts.start() + + self._health_server: HTTPServer | None = None + self._health_thread: threading.Thread | None = None + self._webui_process = None # subprocess for the Flask web UI + + self.config: Config = Config.get_conf( + self, identifier=0x0A0A0A0A, force_registration=True + ) + + self.config.register_global( + minimax_api_key=None, + minimax_model=DEFAULT_MINIMAX_MODEL, + minimax_voice=DEFAULT_MINIMAX_VOICE, + sfx_volume=100, + tts_mode="minimax", + chatterbox_api_url=CHATTERBOX_DEFAULT_URL, + dectalk_api_url=f"http://127.0.0.1:3001", + dectalk_auto_start=True, + accessibility_mode=False, + vox_pack="vox", + webui_tts_channel_id=None, # channel to post web UI TTS transcripts/audio + webui_public_url="https://ttstoy.kingstons-scrapyard.net", # public URL shown in login DMs + ) + + self.config.register_guild( + webui_tts_channel_id=None, # per-guild channel for web UI TTS posts + ) + + self.config.register_user( + minimax_voice=None, + chatterbox_voices=[], # list of voice filenames this user has uploaded + chatterbox_temperature=None, # DEPRECATED: kept for migration + chatterbox_exaggeration=None, # DEPRECATED: kept for migration + chatterbox_temperature_offsets={}, # voice_name -> temperature (e.g. {"Emily.wav": 0.8}) + chatterbox_exaggeration_offsets={}, # voice_name -> exaggeration (e.g. {"Emily.wav": 1.5}) + chatterbox_volume_offsets={}, # voice_name -> dB offset (e.g. {"Emily.wav": -3.0}) + chatterbox_speed_offsets={}, # voice_name -> speed factor (e.g. {"Emily.wav": 1.2}) + ) + + async def red_delete_data_for_user(self, **kwargs): + # only per-user voice; nothing special to wipe beyond config + pass + + # ----------------------------------------- + @tasks.loop(hours=1) + async def clear_old_tts(self): + try: + if os.path.exists(self.tts_storage): + shutil.rmtree(self.tts_storage) + except OSError: + log.exception("Trying to clear old TTS audio files") + + async def cog_unload(self): + self.clear_old_tts.stop() + if self._webui_state_processor.is_running(): + self._webui_state_processor.stop() + await self._stop_webui() + await self._stop_dectalk_server() + + if self._health_server: + self._health_server.shutdown() + self._health_server = None + + # Cancel all TTS processor tasks + for task in self.tts_processors.values(): + if not task.done(): + task.cancel() + + # Wait for all tasks to finish + if self.tts_processors: + await asyncio.gather(*self.tts_processors.values(), return_exceptions=True) + + # ----------------------------------------- + async def _get_minimax_settings(self): + return ( + await self.config.minimax_api_key(), + await self.config.minimax_model(), + await self.config.minimax_voice(), + ) + + async def _get_tts_mode(self): + """Get current TTS mode.""" + mode = await self.config.tts_mode() + # Migrate old "local" mode to "chatterbox" + if mode == "local": + await self.config.tts_mode.set("chatterbox") + return "chatterbox" + return mode + + async def _get_chatterbox_api_url(self): + """Get Chatterbox TTS Server API URL.""" + return await self.config.chatterbox_api_url() + + async def _get_dectalk_api_url(self): + """Get DECTalk API URL.""" + return await self.config.dectalk_api_url() + + async def _install_dectalk_server(self) -> bool: + """Install DECTalk server dependencies.""" + try: + if not self.dectalk_dir.exists(): + log.error(f"DECTalk server directory not found: {self.dectalk_dir}") + return False + + # Run npm install + result = await asyncio.to_thread( + subprocess.run, + ["npm", "install"], + cwd=str(self.dectalk_dir), + capture_output=True, + text=True, + timeout=300 + ) + + if result.returncode != 0: + log.error(f"npm install failed: {result.stderr}") + return False + + log.info("DECTalk server installed successfully") + return True + except Exception as e: + log.exception(f"Failed to install DECTalk server: {e}") + return False + + async def _start_dectalk_server(self) -> bool: + """Start the built-in DECTalk server.""" + try: + # Check if already running + if self.dectalk_process and self.dectalk_process.poll() is None: + log.info("DECTalk server already running") + return True + + if not self.dectalk_dir.exists(): + log.error(f"DECTalk server directory not found: {self.dectalk_dir}") + return False + + server_js = self.dectalk_dir / "server.js" + if not server_js.exists(): + log.error("server.js not found, DECTalk server not installed") + return False + + # Check if node_modules exists + node_modules = self.dectalk_dir / "node_modules" + if not node_modules.exists(): + log.info("node_modules not found, installing DECTalk server...") + if not await self._install_dectalk_server(): + return False + + # Start the server + env = os.environ.copy() + env['PORT'] = str(self.dectalk_port) + + self.dectalk_process = await asyncio.to_thread( + subprocess.Popen, + ["node", "server.js"], + cwd=str(self.dectalk_dir), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Wait a moment for server to start + await asyncio.sleep(2) + + # Check if it's still running + if self.dectalk_process.poll() is not None: + log.error("DECTalk server failed to start") + return False + + log.info(f"DECTalk server started on port {self.dectalk_port}") + return True + except Exception as e: + log.exception(f"Failed to start DECTalk server: {e}") + return False + + async def _stop_dectalk_server(self): + """Stop the built-in DECTalk server.""" + if self.dectalk_process and self.dectalk_process.poll() is None: + try: + self.dectalk_process.terminate() + await asyncio.sleep(1) + if self.dectalk_process.poll() is None: + self.dectalk_process.kill() + log.info("DECTalk server stopped") + except Exception as e: + log.exception(f"Error stopping DECTalk server: {e}") + + async def _get_effective_minimax_voice(self, user: discord.abc.User) -> str: + mode = await self._get_tts_mode() + + # DECTalk doesn't use voice selection - users control voices with [:n*] commands + if mode == "dectalk": + return "paul" # Default, but users can override with commands + + # Morshu doesn't use voice selection - it's always Morshu + if mode == "morshu": + return "morshu" + + # VOX doesn't use voice selection - pack is chosen separately + if mode == "vox": + return "vox" + + # Chatterbox supports per-user voice selection + if mode == "chatterbox": + u = await self.config.user(user).minimax_voice() + # Only use saved voice if it looks like a Chatterbox filename (has a file extension) + if u and ("." in u): + return u + g = await self.config.minimax_voice() + if g and ("." in g): + return g + return "Emily.wav" + + u = await self.config.user(user).minimax_voice() + if u: + return resolve_minimax_voice_id(u) + + g = await self.config.minimax_voice() + return resolve_minimax_voice_id(g) + + async def _get_all_chatterbox_voices(self) -> list: + """Collect all chatterbox voice filenames across all users.""" + all_voices = [] + all_users = await self.config.all_users() + for uid, udata in all_users.items(): + voices = udata.get("chatterbox_voices", []) + all_voices.extend(voices) + return all_voices + + def _save_minimax_tts(self, api_key, model, voice_id, text, audio_path, mode="minimax", dectalk_url=None): + """ + Save TTS audio using MiniMax API or DECTalk API. + + Args: + api_key: API key (required for MiniMax) + model: Model name + voice_id: Voice identifier + text: Text to synthesize + audio_path: Output file path + mode: "minimax" or "dectalk" + dectalk_url: DECTalk API URL (required if mode="dectalk") + + Returns: + dict: Response data including detected language (if available) + """ + if mode == "dectalk": + if not dectalk_url: + raise RuntimeError("DECTalk API URL not configured") + url = f"{dectalk_url.rstrip('/')}/v1/t2a_v2" + headers = {"Content-Type": "application/json"} + else: + url = "https://api.minimax.io/v1/t2a_v2" + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + + payload = { + "model": model, + "text": text, + "stream": False, + "language_boost": "auto", + "output_format": "hex", + "voice_setting": { + "voice_id": voice_id, + "speed": 1, + "vol": 1, + "pitch": 0, + }, + "audio_setting": { + "sample_rate": 32000, + "bitrate": 128000, + "format": "mp3", + "channel": 1, + }, + } + + r = requests.post(url, headers=headers, json=payload, timeout=60) + r.raise_for_status() + data = r.json() + + base = data.get("base_resp") or {} + if base.get("status_code") != 0: + raise RuntimeError( + f"MiniMax error {base.get('status_code')}: {base.get('status_msg')}" + ) + + hex_audio = data.get("data", {}).get("audio") + if not hex_audio: + raise RuntimeError("MiniMax returned no audio data.") + + with open(audio_path, "wb") as f: + f.write(bytes.fromhex(hex_audio)) + + # Return response data for language detection + return data + + def _save_morshu_tts(self, text: str, audio_path: str): + """ + Generate TTS audio using MorshuTalk. + + Args: + text: Text to synthesize + audio_path: Output file path + """ + if not self.morshu: + raise RuntimeError("MorshuTalk engine is not available. Check that g2p_en, numpy, and pydub are installed.") + + audio = self.morshu.load_text(text) + if audio is False or audio is None: + raise RuntimeError("MorshuTalk failed to generate audio.") + + audio.export(audio_path, format="mp3", bitrate="128k") + + def _save_vox_tts(self, text: str, audio_path: str, pack: str = "vox"): + """ + Generate TTS audio using Black Mesa VOX engine. + + Args: + text: Text to synthesize (words matched against VOX dictionary) + audio_path: Output file path + pack: VOX pack to use ("vox" or "vox2") + """ + if not VOX_AVAILABLE: + raise RuntimeError("VOX engine is not available.") + generate_vox(text, audio_path, pack=pack) + + def _save_chatterbox_tts(self, text: str, audio_path: str, chatterbox_url: str, voice: str = "Emily.wav", temperature: float = None, exaggeration: float = None, volume_db: float = 0.0, speed_factor: float = None): + """ + Generate TTS audio using Chatterbox TTS Server's /tts endpoint. + """ + url = f"{chatterbox_url.rstrip('/')}/tts" + payload = { + "text": text, + "voice_mode": "predefined", + "predefined_voice_id": voice if voice else "Emily.wav", + "output_format": "wav", + "split_text": False, + } + if temperature is not None: + payload["temperature"] = temperature + if exaggeration is not None: + payload["exaggeration"] = exaggeration + if speed_factor is not None: + payload["speed_factor"] = speed_factor + log.info(f"[Chatterbox] POST {url} | voice={voice} | temp={temperature} | exag={exaggeration} | vol_db={volume_db} | speed={speed_factor} | text={text[:80]!r}") + try: + r = requests.post(url, json=payload, timeout=120) + log.info(f"[Chatterbox] Response: {r.status_code} | {len(r.content)} bytes") + r.raise_for_status() + except requests.exceptions.HTTPError as e: + # Log response body for debugging + try: + body = r.text[:500] + except Exception: + body = "(could not read body)" + log.error(f"[Chatterbox] HTTP {r.status_code} error. Body: {body}") + if r.status_code == 500 and "failed to synthesize" in body.lower(): + raise RuntimeError( + f"Chatterbox couldn't generate audio with voice `{voice}`. " + "The voice clip may be too short, too long, or in a bad format. " + "Try uploading a clear 5-15 second .wav clip." + ) + raise + + if len(r.content) < 100: + raise RuntimeError(f"Chatterbox returned too little audio data ({len(r.content)} bytes)") + + # Apply volume offset if non-zero + if volume_db and volume_db != 0.0: + tmp_raw = audio_path + ".raw.wav" + with open(tmp_raw, "wb") as f: + f.write(r.content) + try: + subprocess.run( + [ + "ffmpeg", "-y", "-i", tmp_raw, + "-filter:a", f"volume={volume_db}dB", + "-vn", audio_path, + ], + check=True, capture_output=True, text=True, + ) + finally: + if os.path.exists(tmp_raw): + os.remove(tmp_raw) + else: + with open(audio_path, "wb") as f: + f.write(r.content) + + def _ensure_sfx_folders(self): + self.sfx_root.mkdir(parents=True, exist_ok=True) + # Seed the JSON map file if it doesn't exist yet + if not _SFX_MAP_PATH.exists(): + _save_emoji_sfx_map(dict(_DEFAULT_EMOJI_SFX)) + for folder in sorted(set(EMOJI_SFX_FOLDERS.values())): + (self.sfx_root / folder).mkdir(parents=True, exist_ok=True) + + def _get_sfx_source_for_trigger(self, trigger: str) -> Optional[Path]: + normalized = _normalize_sfx_trigger(trigger) + folder_name = EMOJI_SFX_FOLDERS.get(normalized) + if not folder_name: + return None + + folder_path = self.sfx_root / folder_name + folder_path.mkdir(parents=True, exist_ok=True) + + # Search for any supported audio format + for ext in ("*.mp3", "*.wav", "*.ogg"): + candidates = sorted(folder_path.glob(ext)) + if candidates: + return candidates[0] + + return None + + def _split_prompt_segments(self, text: str): + """ + Split prompt into ordered TTS/SFX segments. + + SFX triggers are read from EMOJI_SFX_FOLDERS and support: + - Unicode emoji keys (example: "๐ŸŽ‰") + - Colon-name keys (example: ":airhorn:") + - Discord custom emoji tokens in prompts (`<:airhorn:123>` / ``) + when their normalized `:airhorn:` key exists in EMOJI_SFX_FOLDERS. + """ + segments = [] + text_buffer = [] + + def flush_text_buffer(): + if text_buffer: + combined = "".join(text_buffer) + if combined: + segments.append(("tts", combined)) + text_buffer.clear() + + cursor = 0 + for match in EMOJI_CANDIDATE_PATTERN.finditer(text): + if match.start() > cursor: + text_buffer.append(text[cursor : match.start()]) + + token = match.group(0) + normalized = _normalize_sfx_trigger(token) + if normalized in EMOJI_SFX_FOLDERS: + flush_text_buffer() + segments.append(("sfx", token)) + else: + text_buffer.append(token) + + cursor = match.end() + + if cursor < len(text): + text_buffer.append(text[cursor:]) + + flush_text_buffer() + return segments + + def _render_sfx_part(self, sfx_source: Path, output_path: str, volume_percent: int): + volume_multiplier = max(0, min(100, volume_percent)) / 100 + cmd = [ + "ffmpeg", + "-y", + "-i", + str(sfx_source), + "-filter:a", + f"volume={volume_multiplier}", + "-vn", + "-ac", + "1", + "-ar", + "32000", + "-b:a", + "128k", + output_path, + ] + subprocess.run(cmd, check=True, capture_output=True, text=True) + + def _concat_audio_parts(self, part_paths, output_path: str): + if len(part_paths) == 1: + shutil.copyfile(part_paths[0], output_path) + return + + cmd = ["ffmpeg", "-y"] + for path in part_paths: + cmd.extend(["-i", path]) + + filter_inputs = "".join(f"[{i}:a]" for i in range(len(part_paths))) + cmd.extend( + [ + "-filter_complex", + f"{filter_inputs}concat=n={len(part_paths)}:v=0:a=1[outa]", + "-map", + "[outa]", + "-ac", + "1", + "-ar", + "32000", + "-b:a", + "128k", + output_path, + ] + ) + subprocess.run(cmd, check=True, capture_output=True, text=True) + + def _save_prompt_audio(self, api_key, model, voice_id, text, output_path: str, sfx_volume: int, mode="minimax", dectalk_url=None, vox_pack="vox", chatterbox_url=None, chatterbox_temperature=None, chatterbox_exaggeration=None, chatterbox_volume_db=0.0, chatterbox_speed=None, all_chatterbox_voices=None): + """ + Save prompt audio with SFX and multi-voice support. + + Supports inline voice switches via [mode|voice] or [mode] tags, e.g.: + [minimax|robotnik] Hello [dectalk] [:nh]Deep voice here + + Returns: + dict: Response data including detected language (if available) + """ + voice_segments = _split_voice_segments(text) + has_voice_switches = len(voice_segments) > 1 or voice_segments[0][0] is not None + + # Fast path: no voice switches โ€” use the legacy single-engine path + if not has_voice_switches: + return self._render_single_mode( + api_key, model, voice_id, text, output_path, sfx_volume, + mode, dectalk_url, vox_pack, chatterbox_url, + chatterbox_temperature, chatterbox_exaggeration, + chatterbox_volume_db, chatterbox_speed, + ) + + # Multi-voice path: render each voice segment separately, then concat + self.tts_storage.mkdir(parents=True, exist_ok=True) + temp_dir = os.path.join(self.tts_storage, f"parts_{os.path.basename(output_path)}") + os.makedirs(temp_dir, exist_ok=True) + all_parts = [] + + try: + for seg_idx, (seg_mode, seg_voice, seg_text) in enumerate(voice_segments): + seg_text = seg_text.strip() + if not seg_text: + continue + + # Resolve effective mode and voice for this segment + eff_mode = seg_mode or mode + if seg_voice: + eff_voice = resolve_voice_for_mode(eff_mode, seg_voice, all_chatterbox_voices) + else: + eff_voice = voice_id + + # Split this segment's text on SFX emojis + sfx_segments = self._split_prompt_segments(seg_text) + if not sfx_segments: + continue + + for part_idx, (kind, value) in enumerate(sfx_segments): + part_path = os.path.join(temp_dir, f"seg{seg_idx:02}_{part_idx:03}.mp3") + + if kind == "sfx": + sfx_source = self._get_sfx_source_for_trigger(value) + if sfx_source: + self._render_sfx_part(sfx_source, part_path, sfx_volume) + all_parts.append(part_path) + continue + # Fallback: treat as text + value = _normalize_sfx_trigger(value) + + if not value.strip(): + continue + + self._render_tts_part( + value, part_path, eff_mode, eff_voice, + api_key, model, dectalk_url, vox_pack, chatterbox_url, + chatterbox_temperature, chatterbox_exaggeration, + chatterbox_volume_db, chatterbox_speed, + ) + all_parts.append(part_path) + + if not all_parts: + raise RuntimeError("Prompt resulted in no playable audio segments.") + self._concat_audio_parts(all_parts, output_path) + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + return {"language": "MULTI"} + + def _render_tts_part(self, text, part_path, mode, voice_id, + api_key, model, dectalk_url, vox_pack, chatterbox_url, + cb_temp, cb_exag, cb_vol_db, cb_speed): + """Render a single TTS text segment with the given engine and voice.""" + if mode == "morshu": + self._save_morshu_tts(text, part_path) + elif mode == "vox": + self._save_vox_tts(text, part_path, pack=vox_pack or "vox") + elif mode == "chatterbox": + if not chatterbox_url: + raise RuntimeError("Chatterbox API URL not configured") + self._save_chatterbox_tts( + text, part_path, chatterbox_url, voice_id, + temperature=cb_temp, exaggeration=cb_exag, + volume_db=cb_vol_db, speed_factor=cb_speed, + ) + else: + # minimax or dectalk + self._save_minimax_tts(api_key, model, voice_id, text, part_path, mode, dectalk_url) + + def _render_single_mode(self, api_key, model, voice_id, text, output_path, sfx_volume, + mode, dectalk_url, vox_pack, chatterbox_url, + cb_temp, cb_exag, cb_vol_db, cb_speed): + """Original single-mode render path (no inline voice switches).""" + # Morshu mode bypasses SFX splitting + if mode == "morshu": + self._save_morshu_tts(text, output_path) + return {"language": "MORSHU"} + + # VOX mode bypasses SFX splitting + if mode == "vox": + self._save_vox_tts(text, output_path, pack=vox_pack or "vox") + return {"language": "VOX"} + + # Chatterbox mode + if mode == "chatterbox": + if not chatterbox_url: + raise RuntimeError("Chatterbox API URL not configured") + segments = self._split_prompt_segments(text) + if not segments: + raise RuntimeError("Prompt had no content to synthesize.") + cb_kwargs = {"temperature": cb_temp, "exaggeration": cb_exag, "volume_db": cb_vol_db, "speed_factor": cb_speed} + if not any(kind == "sfx" for kind, _ in segments): + self._save_chatterbox_tts(text, output_path, chatterbox_url, voice_id, **cb_kwargs) + return {"language": "CHATTERBOX"} + part_paths = [] + temp_dir = os.path.join(self.tts_storage, f"parts_{os.path.basename(output_path)}") + os.makedirs(temp_dir, exist_ok=True) + try: + for index, (kind, value) in enumerate(segments): + part_path = os.path.join(temp_dir, f"part_{index:03}.mp3") + if kind == "tts": + if not value.strip(): + continue + self._save_chatterbox_tts(value, part_path, chatterbox_url, voice_id, **cb_kwargs) + else: + sfx_source = self._get_sfx_source_for_trigger(value) + if sfx_source is None: + normalized_text = _normalize_sfx_trigger(value) + self._save_chatterbox_tts(normalized_text, part_path, chatterbox_url, voice_id, **cb_kwargs) + else: + self._render_sfx_part(sfx_source, part_path, sfx_volume) + part_paths.append(part_path) + if not part_paths: + raise RuntimeError("Prompt resulted in no playable audio segments.") + self._concat_audio_parts(part_paths, output_path) + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + return {"language": "CHATTERBOX"} + + # MiniMax / DECTalk path + segments = self._split_prompt_segments(text) + if not segments: + raise RuntimeError("Prompt had no content to synthesize.") + + detected_language = None + + if not any(kind == "sfx" for kind, _ in segments): + response = self._save_minimax_tts(api_key, model, voice_id, text, output_path, mode, dectalk_url) + if response and isinstance(response, dict): + detected_language = response.get("data", {}).get("extra_info", {}).get("language") + return {"language": detected_language} + + part_paths = [] + temp_dir = os.path.join(self.tts_storage, f"parts_{os.path.basename(output_path)}") + os.makedirs(temp_dir, exist_ok=True) + + try: + for index, (kind, value) in enumerate(segments): + part_path = os.path.join(temp_dir, f"part_{index:03}.mp3") + if kind == "tts": + if not value.strip(): + continue + response = self._save_minimax_tts(api_key, model, voice_id, value, part_path, mode, dectalk_url) + if detected_language is None and response and isinstance(response, dict): + detected_language = response.get("data", {}).get("extra_info", {}).get("language") + else: + sfx_source = self._get_sfx_source_for_trigger(value) + if sfx_source is None: + normalized_text = _normalize_sfx_trigger(value) + response = self._save_minimax_tts(api_key, model, voice_id, normalized_text, part_path, mode, dectalk_url) + if detected_language is None and response and isinstance(response, dict): + detected_language = response.get("data", {}).get("extra_info", {}).get("language") + else: + self._render_sfx_part(sfx_source, part_path, sfx_volume) + part_paths.append(part_path) + + if not part_paths: + raise RuntimeError("Prompt resulted in no playable audio segments.") + self._concat_audio_parts(part_paths, output_path) + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + return {"language": detected_language} + + # --------------------------------------------------------------------- + # TOP-LEVEL LOGIN (shortcut for [p]ttstoy login) + # --------------------------------------------------------------------- + @commands.command(name="login") + @commands.guild_only() + async def login_shortcut(self, ctx: commands.Context): + """Get one-time login keys for Kingston's Scrapyard sites. Keys are DM'd to you.""" + await self.webui_login(ctx) + + # --------------------------------------------------------------------- + # CONFIG GROUP + # --------------------------------------------------------------------- + @commands.group(name="ttstoy", invoke_without_command=True) + async def ttstoy_group(self, ctx: commands.Context): + """TTS Toy configuration and management. + + Use `[p]help ttstoy` to see all subcommands. + """ + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @commands.group(name="chatterbox", invoke_without_command=True) + async def chatterbox_group(self, ctx: commands.Context): + """Chatterbox TTS voice management โ€” upload, tune, and share cloned voices. + + Use `[p]help chatterbox` to see all subcommands. + """ + if ctx.invoked_subcommand is None: + await ctx.send_help(ctx.command) + + @chatterbox_group.command(name="guide") + async def chatterbox_guide(self, ctx: commands.Context): + """Full guide to Chatterbox TTS features, voice cloning, and special tokens.""" + prefix = ctx.clean_prefix + + part1 = [ + "**๐Ÿ  Chatterbox TTS Guide**", + "", + "**โ”€โ”€ Voice Cloning โ”€โ”€**", + "Upload a voice clip and Chatterbox will clone it for TTS.", + f"โ€ข `{prefix}chatterbox addvoice ` โ€” attach a .wav or .mp3 (5+ sec)", + f"โ€ข `{prefix}chatterbox addvoice ` โ€” or paste a direct link", + f"โ€ข `{prefix}chatterbox removevoice ` โ€” delete a voice", + f"โ€ข `{prefix}chatterbox myvoices` โ€” see your uploaded voices", + f"โ€ข `{prefix}ttstoy myvoice ` โ€” switch your active voice", + "", + "Tips: 5โ€“15s of clear speech, one speaker, no background noise.", + ".wav works best, .mp3 accepted. Short clips auto-loop to 5s.", + "", + "**โ”€โ”€ Special Tokens (Turbo model) โ”€โ”€**", + "`[laugh]` `[chuckle]` `[sigh]` `[gasp]` `[cough]`", + "`[clear throat]` `[sniff]` `[groan]` `[shush]`", + f"Example: `{prefix}tts Hey [chuckle] thanks for calling back`", + ] + + part2 = [ + "**โ”€โ”€ Voice Settings โ”€โ”€**", + "All saved per voice per user. Each voice keeps its own tuning.", + "", + f"โ€ข `{prefix}chatterbox temp [0.0โ€“1.5]` โ€” randomness", + f"โ€ข `{prefix}chatterbox exag [0.25โ€“2.0]` โ€” expressiveness", + f"โ€ข `{prefix}chatterbox volume [-20โ€“20]` โ€” dB loudness offset", + "Append `reset` to any of the above to clear it.", + "", + "**โ”€โ”€ Voice Sharing โ”€โ”€**", + f"โ€ข `{prefix}chatterbox sharevoice @User`", + "", + "**โ”€โ”€ Quick Start โ”€โ”€**", + f"1. Upload: `{prefix}chatterbox addvoice MyVoice` (attach audio)", + f"2. Speak: `{prefix}tts Hello world [laugh] cloned voice`", + f"3. Tweak: `{prefix}chatterbox exag 1.5`", + ] + + await ctx.send("\n".join(part1)) + await ctx.send("\n".join(part2)) + + @chatterbox_group.command(name="model") + @commands.is_owner() + async def chatterbox_model_cmd(self, ctx: commands.Context, model: Optional[str] = None): + """ + Show or switch the Chatterbox model (owner only). + + - `[p]chatterbox model` โ†’ show current model + - `[p]chatterbox model turbo` โ†’ fast, supports [laugh] tags + - `[p]chatterbox model original` โ†’ better voice cloning, slower + """ + chatterbox_url = await self._get_chatterbox_api_url() + + if model is None: + # Fetch current model info + try: + r = await asyncio.to_thread( + requests.get, f"{chatterbox_url.rstrip('/')}/api/model-info", timeout=5 + ) + r.raise_for_status() + info = r.json() + current = info.get("type", "unknown") + class_name = info.get("class_name", "unknown") + loaded = info.get("loaded", False) + except Exception as e: + return await ctx.send(f"โŒ Cannot reach Chatterbox server: {e}") + + lines = [ + f"**Current model:** `{current}` ({class_name})", + f"**Loaded:** {'โœ…' if loaded else 'โŒ'}", + "", + "Available models:", + "โ€ข `turbo` โ€” fast, 350M params, supports [laugh]/[cough]/etc tags", + "โ€ข `original` โ€” better voice cloning, 0.5B params, stronger emotion control", + "", + f"Switch: `{ctx.clean_prefix}chatterbox model turbo` or `{ctx.clean_prefix}chatterbox model original`", + ] + return await ctx.send("\n".join(lines)) + + model = model.lower().strip() + model_map = { + "turbo": "chatterbox-turbo", + "original": "chatterbox", + "chatterbox-turbo": "chatterbox-turbo", + "chatterbox": "chatterbox", + } + repo_id = model_map.get(model) + if not repo_id: + return await ctx.send("โŒ Model must be `turbo` or `original`.") + + msg = await ctx.send(f"๐Ÿ”„ Switching to `{model}`... this may take a moment.") + + try: + # Save the new model to config + r = await asyncio.to_thread( + requests.post, + f"{chatterbox_url.rstrip('/')}/save_settings", + json={"model": {"repo_id": repo_id}}, + timeout=10, + ) + r.raise_for_status() + + # Trigger hot-swap + r2 = await asyncio.to_thread( + requests.post, + f"{chatterbox_url.rstrip('/')}/restart_server", + timeout=120, + ) + r2.raise_for_status() + result = r2.json() + + await msg.edit(content=f"โœ… {result.get('message', f'Switched to {model}')}") + except Exception as e: + log.exception("Model switch error: %s", e) + await msg.edit(content=f"โŒ Failed to switch model: {e}") + + @ttstoy_group.command(name="sfxvolume") + @commands.is_owner() + async def sfx_volume(self, ctx: commands.Context, volume: Optional[int] = None): + """Show or set sound effect volume (0-100).""" + if volume is None: + current = await self.config.sfx_volume() + return await ctx.send(f"Current SFX volume is `{current}`.") + + if not 0 <= volume <= 100: + return await ctx.send("โŒ SFX volume must be between 0 and 100.") + + await self.config.sfx_volume.set(volume) + await ctx.send(f"โœ… SFX volume set to `{volume}`.") + + @ttstoy_group.command(name="sfx") + async def sfx_list(self, ctx: commands.Context): + """List all available sound effects and their triggers.""" + lines = ["**Available Sound Effects:**", ""] + + # Group by folder and show emoji triggers + folder_to_emojis = {} + for emoji, folder in EMOJI_SFX_FOLDERS.items(): + if folder not in folder_to_emojis: + folder_to_emojis[folder] = [] + folder_to_emojis[folder].append(emoji) + + # Check which folders have audio files + for folder_name in sorted(folder_to_emojis.keys()): + folder_path = self.sfx_root / folder_name + audio_files = list(folder_path.glob("*.mp3")) if folder_path.exists() else [] + + emojis = folder_to_emojis[folder_name] + emoji_str = " ".join(emojis) + + if audio_files: + file_count = len(audio_files) + status = f"โœ… ({file_count} file{'s' if file_count > 1 else ''})" + else: + status = "โŒ (no audio)" + + lines.append(f"{emoji_str} โ†’ `{folder_name}` {status}") + + lines.append("") + lines.append("**Usage:** Include emoji in your TTS text") + lines.append(f"**Example:** `{ctx.clean_prefix}tts Hello ๐ŸŽ‰ world ๐Ÿ˜‚`") + + await ctx.send("\n".join(lines)) + + @ttstoy_group.command(name="mode") + @commands.is_owner() + async def tts_mode(self, ctx: commands.Context, mode: Optional[str] = None): + """ + Show or set TTS mode (minimax, chatterbox, dectalk, morshu, or vox). + + - `[p]ttstoy mode` โ†’ show current mode + - `[p]ttstoy mode minimax` โ†’ use MiniMax API + - `[p]ttstoy mode chatterbox` โ†’ use local Chatterbox TTS server + - `[p]ttstoy mode dectalk` โ†’ use DECTalk API + - `[p]ttstoy mode morshu` โ†’ use MorshuTalk + - `[p]ttstoy mode vox` โ†’ use Black Mesa VOX announcer + """ + if mode is None: + current = await self._get_tts_mode() + dectalk_url = await self._get_dectalk_api_url() + chatterbox_url = await self._get_chatterbox_api_url() + + mode_emoji = {"minimax": "๐ŸŒ", "chatterbox": "๐Ÿ ", "dectalk": "๐Ÿค–", "morshu": "๐Ÿ›’", "vox": "๐Ÿ“ข"}.get(current, "โ“") + + lines = [ + f"**Current TTS Mode: {mode_emoji} {current.upper()}**", + "", + "Available modes:", + "- `minimax` โ€“ Use MiniMax cloud API (requires API key)", + "- `chatterbox` โ€“ Use local Chatterbox TTS server (AI voice cloning)", + "- `dectalk` โ€“ Use DECTalk API (classic robotic voice)", + f"- `morshu` โ€“ Use MorshuTalk ({'available' if MORSHU_AVAILABLE else 'not installed'})", + f"- `vox` โ€“ Black Mesa VOX announcer ({'available' if VOX_AVAILABLE else 'not installed'})", + "", + ] + + if current == "chatterbox": + lines.append(f"Chatterbox URL: `{chatterbox_url}`") + lines.append(f"Change with: `{ctx.clean_prefix}ttstoy chatterboxurl `") + elif current == "dectalk": + lines.append(f"DECTalk API URL: `{dectalk_url}`") + lines.append(f"Change with: `{ctx.clean_prefix}ttstoy dectalkurl `") + elif current == "morshu": + lines.append("MorshuTalk generates speech from Morshu's voice lines.") + lines.append("No voice selection needed - it's always Morshu.") + elif current == "vox": + vox_pack = await self.config.vox_pack() + lines.append(f"Black Mesa VOX announcer. Current pack: `{vox_pack}`") + lines.append(f"Change pack: `{ctx.clean_prefix}ttstoy voxpack [vox|vox2]`") + lines.append(f"List words: `{ctx.clean_prefix}ttstoy voxwords`") + else: + lines.append("Switch to chatterbox, dectalk, morshu, or vox mode to use alternative TTS.") + + return await ctx.send("\n".join(lines)) + + mode = mode.lower().strip() + # Accept "local" as alias for "chatterbox" for backwards compat + if mode == "local": + mode = "chatterbox" + if mode not in ["minimax", "chatterbox", "dectalk", "morshu", "vox"]: + return await ctx.send("Mode must be `minimax`, `chatterbox`, `dectalk`, `morshu`, or `vox`.") + + if mode == "morshu" and not MORSHU_AVAILABLE: + return await ctx.send( + "โŒ MorshuTalk engine failed to load.\n" + "Make sure `g2p_en`, `numpy`, and `pydub` are installed." + ) + + if mode == "vox" and not VOX_AVAILABLE: + return await ctx.send("โŒ VOX engine is not available. Check bot logs for details.") + + await self.config.tts_mode.set(mode) + mode_emoji = {"minimax": "๐ŸŒ", "chatterbox": "๐Ÿ ", "dectalk": "๐Ÿค–", "morshu": "๐Ÿ›’", "vox": "๐Ÿ“ข"}.get(mode, "โ“") + + if mode == "morshu": + await ctx.send( + f"TTS mode set to {mode_emoji} **MORSHU**\n" + f"Lamp, oil, rope, bombs? You want it? It's yours, my friend." + ) + elif mode == "vox": + vox_pack = await self.config.vox_pack() + await ctx.send( + f"TTS mode set to {mode_emoji} **VOX**\n" + f"Black Mesa VOX announcer (pack: `{vox_pack}`).\n" + f"Only words in the VOX dictionary will be spoken. Unknown words are skipped." + ) + elif mode == "chatterbox": + chatterbox_url = await self._get_chatterbox_api_url() + await ctx.send( + f"โœ… TTS mode set to {mode_emoji} **CHATTERBOX**\n" + f"Using Chatterbox TTS at: `{chatterbox_url}`\n" + f"Make sure your Chatterbox TTS server is running!" + ) + elif mode == "dectalk": + dectalk_url = await self._get_dectalk_api_url() + auto_start = await self.config.dectalk_auto_start() + + msg = f"โœ… TTS mode set to {mode_emoji} **DECTALK**\n" + msg += f"Using DECTalk API at: `{dectalk_url}`\n" + + if auto_start: + msg += "๐Ÿš€ Auto-starting DECTalk server..." + await ctx.send(msg) + + if await self._start_dectalk_server(): + await ctx.send("โœ… DECTalk server started successfully!") + else: + await ctx.send( + "โŒ Failed to start DECTalk server.\n" + f"Try manually: `{ctx.clean_prefix}ttstoy dectalkstart`\n" + f"Or install: `{ctx.clean_prefix}ttstoy dectalkinstall`" + ) + else: + msg += f"Make sure your DECTalk server is running!\n" + msg += f"Or enable auto-start: `{ctx.clean_prefix}ttstoy dectalkautotoggle`" + await ctx.send(msg) + else: + await ctx.send( + f"โœ… TTS mode set to {mode_emoji} **MINIMAX**\n" + f"Using MiniMax cloud API. Make sure your API key is set." + ) + + @chatterbox_group.command(name="url") + @commands.is_owner() + async def chatterbox_url(self, ctx: commands.Context, url: Optional[str] = None): + """ + Show or set Chatterbox TTS Server URL. + + - `[p]chatterbox url` โ†’ show current URL + - `[p]chatterbox url http://127.0.0.1:8099` โ†’ set URL + """ + if url is None: + current = await self._get_chatterbox_api_url() + mode = await self._get_tts_mode() + + lines = [ + f"**Chatterbox TTS URL:** `{current}`", + "", + ] + + if mode == "chatterbox": + lines.append("โœ… Currently using chatterbox mode") + try: + test_url = f"{current.rstrip('/')}/api/ui/initial-data" + r = requests.get(test_url, timeout=3) + if r.status_code == 200: + lines.append("โœ… Chatterbox server is reachable") + else: + lines.append(f"โš ๏ธ Chatterbox returned status {r.status_code}") + except Exception as e: + lines.append(f"โŒ Cannot reach Chatterbox: {e}") + else: + lines.append(f"โ„น๏ธ Currently using {mode} mode") + lines.append(f"Switch with: `{ctx.clean_prefix}ttstoy mode chatterbox`") + + return await ctx.send("\n".join(lines)) + + url = url.strip().rstrip('/') + if not url.startswith(('http://', 'https://')): + return await ctx.send("โŒ URL must start with http:// or https://") + + try: + test_url = f"{url}/api/ui/initial-data" + r = requests.get(test_url, timeout=5) + if r.status_code != 200: + return await ctx.send( + f"โš ๏ธ Warning: URL returned status {r.status_code}\n" + f"Saving anyway. Make sure your Chatterbox server is running." + ) + except Exception as e: + return await ctx.send( + f"โš ๏ธ Warning: Cannot reach URL: {e}\n" + f"Saving anyway. Make sure your Chatterbox server is running." + ) + + await self.config.chatterbox_api_url.set(url) + await ctx.send(f"โœ… Chatterbox URL set to: `{url}`") + + @chatterbox_group.command(name="addvoice") + async def add_voice(self, ctx: commands.Context, name: Optional[str] = None, url: Optional[str] = None): + """ + Upload a voice clip to Chatterbox and add it to your voice list. + + Attach a .wav or .mp3 file, or paste a direct link to one. + The voice is automatically set as your active voice. + - `[p]chatterbox addvoice MyVoice` (with audio attachment) + - `[p]chatterbox addvoice MyVoice https://example.com/clip.mp3` + """ + # Detect if name is actually a URL + if name and re.match(r'") + + has_attachment = bool(ctx.message.attachments) + has_url = bool(url and re.match(r'https?://', url, re.IGNORECASE)) + + if not has_attachment and not has_url: + return await ctx.send( + "โŒ Attach a `.wav` or `.mp3` audio clip, or paste a direct link.\n" + f"Example: `{ctx.clean_prefix}chatterbox addvoice CoolVoice` with a file attached\n" + f"Example: `{ctx.clean_prefix}chatterbox addvoice CoolVoice https://example.com/clip.mp3`" + ) + + if has_attachment: + attachment = ctx.message.attachments[0] + lower_name = attachment.filename.lower() + if not (lower_name.endswith(".wav") or lower_name.endswith(".mp3")): + return await ctx.send("โŒ Only `.wav` and `.mp3` files are supported.") + source_filename = attachment.filename + else: + # Extract filename from URL path + url_path = url.split("?")[0].split("#")[0] + source_filename = url_path.rsplit("/", 1)[-1] + lower_url = source_filename.lower() + # Accept .wav, .mp3, or extensionless (we'll convert later via pydub) + if "." in source_filename and not (lower_url.endswith(".wav") or lower_url.endswith(".mp3")): + return await ctx.send("โŒ URL must point to a `.wav` or `.mp3` file.") + if "." not in source_filename: + source_filename = source_filename + ".wav" + + # Build filename: prefix with user ID to avoid collisions + if name: + safe_name = re.sub(r'[^a-zA-Z0-9_]', '', name) + if not safe_name: + return await ctx.send("โŒ Voice name must contain at least one letter or number.") + else: + safe_name = re.sub(r'[^a-zA-Z0-9_]', '', source_filename.rsplit('.', 1)[0]) + if not safe_name: + safe_name = "voice" + + ext = ".wav" if source_filename.lower().endswith(".wav") else ".mp3" + filename = f"{ctx.author.id}_{safe_name}{ext}" + + chatterbox_url = await self._get_chatterbox_api_url() + + async with ctx.typing(): + try: + if has_attachment: + audio_bytes = await ctx.message.attachments[0].read() + else: + try: + r = await asyncio.to_thread(requests.get, url, timeout=30) + r.raise_for_status() + audio_bytes = r.content + except Exception as e: + return await ctx.send(f"โŒ Failed to download audio from URL: {e}") + + # Ensure minimum duration (Chatterbox requires >5 seconds) + # Loop short clips until they're long enough + try: + from pydub import AudioSegment + import io as _io + audio_seg = AudioSegment.from_file(_io.BytesIO(audio_bytes)) + min_ms = 6000 # 6 seconds to be safe + if len(audio_seg) < min_ms: + loops_needed = (min_ms // len(audio_seg)) + 1 + audio_seg = audio_seg * loops_needed + log.info(f"[Chatterbox] Looped short clip {loops_needed}x to {len(audio_seg)/1000:.1f}s") + # Always export as wav for best compatibility + buf = _io.BytesIO() + audio_seg.export(buf, format="wav") + audio_bytes = buf.getvalue() + ext = ".wav" + filename = f"{ctx.author.id}_{safe_name}{ext}" + except ImportError: + log.warning("[Chatterbox] pydub not available, skipping duration check") + except Exception as e_dur: + log.warning(f"[Chatterbox] Could not check/loop audio duration: {e_dur}") + + upload_url = f"{chatterbox_url.rstrip('/')}/upload_predefined_voice" + files = {"files": (filename, audio_bytes, "audio/wav")} + r = await asyncio.to_thread( + requests.post, upload_url, files=files, timeout=30 + ) + r.raise_for_status() + data = r.json() + + errors = data.get("errors", []) + uploaded = data.get("uploaded_files", []) + + if errors and not uploaded: + error_msg = errors[0].get("error", "Unknown error") + return await ctx.send(f"โŒ Upload failed: {error_msg}") + + # Add to user's voice list + user_voices = await self.config.user(ctx.author).chatterbox_voices() + if filename not in user_voices: + user_voices.append(filename) + await self.config.user(ctx.author).chatterbox_voices.set(user_voices) + + # Auto-set as active voice + await self.config.user(ctx.author).minimax_voice.set(filename) + + await ctx.send( + f"โœ… Voice `{safe_name}` uploaded and set as your active voice!\n" + f"Switch voices: `{ctx.clean_prefix}chatterbox myvoices`" + ) + except Exception as e: + log.exception("Voice upload error: %s", e) + await ctx.send(f"โŒ Failed to upload voice: {e}") + + @chatterbox_group.command(name="removevoice") + async def remove_voice(self, ctx: commands.Context, name: str): + """ + Remove a voice from your personal voice list and delete it from the server. + + - `[p]chatterbox removevoice CoolVoice` + """ + user_voices = await self.config.user(ctx.author).chatterbox_voices() + + if not user_voices: + return await ctx.send("You don't have any uploaded voices.") + + # Find matching voice (user can type just the name without ID prefix/extension) + match = None + for v in user_voices: + # Match by exact filename, or by the name part after the user ID prefix + parts = v.split("_", 1) + voice_name = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else v.rsplit(".", 1)[0] + if v == name or voice_name.lower() == name.lower() or v.lower() == name.lower(): + match = v + break + + if not match: + voice_names = ", ".join( + v.split("_", 1)[1].rsplit(".", 1)[0] if "_" in v else v.rsplit(".", 1)[0] + for v in user_voices + ) + return await ctx.send(f"โŒ Voice `{name}` not found in your list.\nYour voices: {voice_names}") + + user_voices.remove(match) + await self.config.user(ctx.author).chatterbox_voices.set(user_voices) + + # Clean up per-voice settings + for offsets_key in ("chatterbox_temperature_offsets", "chatterbox_exaggeration_offsets", "chatterbox_volume_offsets"): + offsets = await getattr(self.config.user(ctx.author), offsets_key)() + if match in offsets: + offsets.pop(match) + await getattr(self.config.user(ctx.author), offsets_key).set(offsets) + + # Only delete from server if this user is the original uploader + # (filename starts with their user ID). Shared voices stay on the server. + is_owner_of_file = match.startswith(f"{ctx.author.id}_") + if is_owner_of_file: + chatterbox_url = await self._get_chatterbox_api_url() + try: + del_url = f"{chatterbox_url.rstrip('/')}/delete_predefined_voice/{match}" + r = await asyncio.to_thread(requests.delete, del_url, timeout=10) + if r.status_code not in (200, 204, 404): + log.warning(f"[Chatterbox] Failed to delete voice file {match}: HTTP {r.status_code}") + except Exception as e: + log.warning(f"[Chatterbox] Could not delete voice file {match} from server: {e}") + + # If this was the active voice, clear it + current = await self.config.user(ctx.author).minimax_voice() + if current == match: + if user_voices: + await self.config.user(ctx.author).minimax_voice.set(user_voices[0]) + new_name = user_voices[0].split("_", 1)[1].rsplit(".", 1)[0] if "_" in user_voices[0] else user_voices[0] + await ctx.send(f"โœ… Voice `{name}` removed. Switched to `{new_name}`.") + else: + await self.config.user(ctx.author).minimax_voice.set(None) + await ctx.send(f"โœ… Voice `{name}` removed. Using default voice now.") + else: + await ctx.send(f"โœ… Voice `{name}` removed from your list.") + + @chatterbox_group.command(name="myvoices") + async def my_voices(self, ctx: commands.Context): + """List your uploaded Chatterbox voices and switch between them.""" + mode = await self._get_tts_mode() + if mode != "chatterbox": + return await ctx.send(f"Voice management is only available in chatterbox mode.\nSwitch with: `{ctx.clean_prefix}ttstoy mode chatterbox`") + + user_voices = await self.config.user(ctx.author).chatterbox_voices() + current = await self.config.user(ctx.author).minimax_voice() + + if not user_voices: + return await ctx.send( + "You haven't uploaded any voices yet.\n" + f"Upload one: `{ctx.clean_prefix}chatterbox addvoice MyVoice` (attach a .wav or .mp3)" + ) + + lines = ["**Your Voices:**", ""] + for v in user_voices: + # Extract display name from filename (strip user ID prefix and extension) + parts = v.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else v.rsplit(".", 1)[0] + marker = " โ† active" if v == current else "" + lines.append(f" `{display}`{marker}") + + lines.append("") + lines.append(f"Switch: `{ctx.clean_prefix}ttstoy myvoice `") + lines.append(f"Add: `{ctx.clean_prefix}chatterbox addvoice ` (attach audio)") + lines.append(f"Remove: `{ctx.clean_prefix}chatterbox removevoice `") + + await ctx.send("\n".join(lines)) + + + @chatterbox_group.command(name="temp") + async def chatterbox_temperature(self, ctx: commands.Context, value: Optional[float] = None): + """ + Show or set Chatterbox temperature for your current voice (0.0โ€“1.5). + + Saved per voice per user โ€” each voice remembers its own setting. + Lower = more consistent, higher = more varied. + - `[p]chatterbox temp` โ†’ show current + - `[p]chatterbox temp 0.8` โ†’ set value + - `[p]chatterbox temp reset` โ†’ use server default + """ + voice = await self._get_effective_minimax_voice(ctx.author) + offsets = await self.config.user(ctx.author).chatterbox_temperature_offsets() + + # Migrate old global setting on first use + if not offsets: + old_val = await self.config.user(ctx.author).chatterbox_temperature() + if old_val is not None: + offsets[voice] = old_val + await self.config.user(ctx.author).chatterbox_temperature_offsets.set(offsets) + await self.config.user(ctx.author).chatterbox_temperature.set(None) + + if value is None: + current = offsets.get(voice) + if current is None: + return await ctx.send( + f"Temperature for `{voice}`: `server default`\n" + f"Set with: `{ctx.clean_prefix}chatterbox temp 0.8`" + ) + return await ctx.send( + f"Temperature for `{voice}`: `{current}`\n" + f"Reset with: `{ctx.clean_prefix}chatterbox temp reset`" + ) + + offsets[voice] = value + await self.config.user(ctx.author).chatterbox_temperature_offsets.set(offsets) + await ctx.send(f"โœ… Temperature for `{voice}` set to `{value}`.") + + @chatterbox_temperature.error + async def chatterbox_temperature_error(self, ctx, error): + """Handle 'reset' being passed as a non-float.""" + if isinstance(error, commands.BadArgument): + msg_content = ctx.message.content.lower() + if "reset" in msg_content: + voice = await self._get_effective_minimax_voice(ctx.author) + offsets = await self.config.user(ctx.author).chatterbox_temperature_offsets() + offsets.pop(voice, None) + await self.config.user(ctx.author).chatterbox_temperature_offsets.set(offsets) + await ctx.send(f"โœ… Temperature for `{voice}` reset to server default.") + else: + await ctx.send("โŒ Value must be a number (0.0โ€“1.5) or `reset`.") + + @chatterbox_group.command(name="exag") + async def chatterbox_exaggeration(self, ctx: commands.Context, value: Optional[float] = None): + """ + Show or set Chatterbox exaggeration for your current voice (0.25โ€“2.0). + + Saved per voice per user โ€” each voice remembers its own setting. + Higher = more expressive/dramatic delivery. + - `[p]chatterbox exag` โ†’ show current + - `[p]chatterbox exag 1.5` โ†’ set value + - `[p]chatterbox exag reset` โ†’ use server default + """ + voice = await self._get_effective_minimax_voice(ctx.author) + offsets = await self.config.user(ctx.author).chatterbox_exaggeration_offsets() + + # Migrate old global setting on first use + if not offsets: + old_val = await self.config.user(ctx.author).chatterbox_exaggeration() + if old_val is not None: + offsets[voice] = old_val + await self.config.user(ctx.author).chatterbox_exaggeration_offsets.set(offsets) + await self.config.user(ctx.author).chatterbox_exaggeration.set(None) + + if value is None: + current = offsets.get(voice) + if current is None: + return await ctx.send( + f"Exaggeration for `{voice}`: `server default`\n" + f"Set with: `{ctx.clean_prefix}chatterbox exag 1.5`" + ) + return await ctx.send( + f"Exaggeration for `{voice}`: `{current}`\n" + f"Reset with: `{ctx.clean_prefix}chatterbox exag reset`" + ) + + offsets[voice] = value + await self.config.user(ctx.author).chatterbox_exaggeration_offsets.set(offsets) + await ctx.send(f"โœ… Exaggeration for `{voice}` set to `{value}`.") + + @chatterbox_exaggeration.error + async def chatterbox_exaggeration_error(self, ctx, error): + """Handle 'reset' being passed as a non-float.""" + if isinstance(error, commands.BadArgument): + msg_content = ctx.message.content.lower() + if "reset" in msg_content: + voice = await self._get_effective_minimax_voice(ctx.author) + offsets = await self.config.user(ctx.author).chatterbox_exaggeration_offsets() + offsets.pop(voice, None) + await self.config.user(ctx.author).chatterbox_exaggeration_offsets.set(offsets) + await ctx.send(f"โœ… Exaggeration for `{voice}` reset to server default.") + else: + await ctx.send("โŒ Value must be a number (0.25โ€“2.0) or `reset`.") + + @chatterbox_group.command(name="volume") + async def chatterbox_volume(self, ctx: commands.Context, value: Optional[float] = None): + """ + Show or set a volume offset (in dB) for your current Chatterbox voice. + + Each voice can have its own offset so they all sound equally loud. + - `[p]chatterbox volume` โ†’ show offset for your active voice + - `[p]chatterbox volume -3` โ†’ make current voice 3 dB quieter + - `[p]chatterbox volume 4.5` โ†’ make current voice 4.5 dB louder + - `[p]chatterbox volume reset` โ†’ remove offset for current voice + Range: -20.0 to +20.0 dB. + """ + voice = await self._get_effective_minimax_voice(ctx.author) + offsets = await self.config.user(ctx.author).chatterbox_volume_offsets() + + if value is None: + current = offsets.get(voice, 0.0) + if current == 0.0: + return await ctx.send( + f"๐Ÿ”Š Voice `{voice}` has no volume offset (0 dB).\n" + f"Set one with: `{ctx.clean_prefix}chatterbox volume `" + ) + sign = "+" if current > 0 else "" + return await ctx.send(f"๐Ÿ”Š Voice `{voice}` volume offset: `{sign}{current} dB`") + + if value < -20.0 or value > 20.0: + return await ctx.send("โŒ Volume offset must be between -20.0 and +20.0 dB.") + + offsets[voice] = value + await self.config.user(ctx.author).chatterbox_volume_offsets.set(offsets) + sign = "+" if value > 0 else "" + await ctx.send(f"โœ… Volume offset for `{voice}` set to `{sign}{value} dB`.") + + @chatterbox_volume.error + async def chatterbox_volume_error(self, ctx, error): + """Handle 'reset' being passed as a non-float.""" + if isinstance(error, commands.BadArgument): + msg_content = ctx.message.content.lower() + if "reset" in msg_content: + voice = await self._get_effective_minimax_voice(ctx.author) + offsets = await self.config.user(ctx.author).chatterbox_volume_offsets() + offsets.pop(voice, None) + await self.config.user(ctx.author).chatterbox_volume_offsets.set(offsets) + await ctx.send(f"โœ… Volume offset for `{voice}` reset to 0 dB.") + else: + await ctx.send("โŒ Value must be a number (-20.0 to +20.0) or `reset`.") + + @chatterbox_group.command(name="speed") + async def chatterbox_speed(self, ctx: commands.Context, value: Optional[float] = None): + """ + Show or set speed for your current voice (0.25โ€“4.0). + + Saved per-voice โ€” each voice remembers its own speed. + - `[p]chatterbox speed` โ†’ show current voice's speed + - `[p]chatterbox speed 1.2` โ†’ set speed (1.0 = normal) + - `[p]chatterbox speed reset` โ†’ remove speed override + """ + voice = await self._get_effective_minimax_voice(ctx.author) + offsets = await self.config.user(ctx.author).chatterbox_speed_offsets() + + parts = voice.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 and parts[0].isdigit() else voice.rsplit(".", 1)[0] + + if value is None: + current = offsets.get(voice) + if current is None: + return await ctx.send( + f"Speed for `{display}`: `default (1.0)`\n" + f"Set with: `{ctx.clean_prefix}chatterbox speed 1.2`" + ) + return await ctx.send( + f"Speed for `{display}`: `{current}`\n" + f"Reset with: `{ctx.clean_prefix}chatterbox speed reset`" + ) + + if not 0.25 <= value <= 4.0: + return await ctx.send("โŒ Speed must be between 0.25 and 4.0.") + + offsets[voice] = value + await self.config.user(ctx.author).chatterbox_speed_offsets.set(offsets) + await ctx.send(f"โœ… Speed for `{display}` set to `{value}`.") + + @chatterbox_speed.error + async def chatterbox_speed_error(self, ctx, error): + """Handle 'reset' being passed as a non-float.""" + if isinstance(error, commands.BadArgument): + msg_content = ctx.message.content.lower() + if "reset" in msg_content: + voice = await self._get_effective_minimax_voice(ctx.author) + offsets = await self.config.user(ctx.author).chatterbox_speed_offsets() + offsets.pop(voice, None) + await self.config.user(ctx.author).chatterbox_speed_offsets.set(offsets) + parts = voice.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 and parts[0].isdigit() else voice.rsplit(".", 1)[0] + await ctx.send(f"โœ… Speed for `{display}` reset to default.") + else: + await ctx.send("โŒ Value must be a number (0.25โ€“4.0) or `reset`.") + + @chatterbox_group.command(name="sharevoice") + async def share_voice(self, ctx: commands.Context, name: str, target: discord.Member): + """ + Share one of your Chatterbox voices with another user. + + Adds the voice to their library so they can use it too. + - `[p]chatterbox sharevoice CoolVoice @Friend` + """ + if target.bot: + return await ctx.send("โŒ You can't share voices with bots.") + if target.id == ctx.author.id: + return await ctx.send("โŒ You already have that voice.") + + user_voices = await self.config.user(ctx.author).chatterbox_voices() + if not user_voices: + return await ctx.send("You don't have any uploaded voices to share.") + + # Find matching voice + match = None + for v in user_voices: + parts = v.split("_", 1) + voice_name = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else v.rsplit(".", 1)[0] + if v == name or voice_name.lower() == name.lower() or v.lower() == name.lower(): + match = v + break + + if not match: + voice_names = ", ".join( + v.split("_", 1)[1].rsplit(".", 1)[0] if "_" in v else v.rsplit(".", 1)[0] + for v in user_voices + ) + return await ctx.send(f"โŒ Voice `{name}` not found in your list.\nYour voices: {voice_names}") + + # Check if target already has this voice + target_voices = await self.config.user(target).chatterbox_voices() + if match in target_voices: + parts = match.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else match.rsplit(".", 1)[0] + return await ctx.send(f"โŒ {target.display_name} already has `{display}`.") + + # Add the same voice file to the target user's library + target_voices.append(match) + await self.config.user(target).chatterbox_voices.set(target_voices) + + parts = match.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else match.rsplit(".", 1)[0] + await ctx.send( + f"โœ… Voice `{display}` shared with {target.display_name}!\n" + f"They can switch to it with: `{ctx.clean_prefix}ttstoy myvoice {display}`" + ) + + @chatterbox_group.command(name="reset") + async def chatterbox_reset(self, ctx: commands.Context): + """ + Reset all per-voice parameters (temp, exag, volume, speed) for your current voice to server defaults. + + - `[p]chatterbox reset` + """ + voice = await self._get_effective_minimax_voice(ctx.author) + parts = voice.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 and parts[0].isdigit() else voice.rsplit(".", 1)[0] + + cleared = [] + for key in ("chatterbox_temperature_offsets", "chatterbox_exaggeration_offsets", "chatterbox_volume_offsets", "chatterbox_speed_offsets"): + offsets = await getattr(self.config.user(ctx.author), key)() + if voice in offsets: + offsets.pop(voice) + await getattr(self.config.user(ctx.author), key).set(offsets) + cleared.append(key.replace("chatterbox_", "").replace("_offsets", "")) + + if cleared: + await ctx.send(f"โœ… Reset {', '.join(cleared)} for `{display}` to server defaults.") + else: + await ctx.send(f"โ„น๏ธ `{display}` is already using all server defaults.") + + + @ttstoy_group.command(name="dectalkurl") + @commands.is_owner() + async def dectalk_url(self, ctx: commands.Context, url: Optional[str] = None): + """ + Show or set DECTalk API URL. + + - `[p]ttstoy dectalkurl` โ†’ show current URL + - `[p]ttstoy dectalkurl http://127.0.0.1:3001` โ†’ set URL + """ + if url is None: + current = await self._get_dectalk_api_url() + mode = await self._get_tts_mode() + + lines = [ + f"**DECTalk API URL:** `{current}`", + "", + ] + + if mode == "dectalk": + lines.append("โœ… Currently using DECTalk mode") + # Test connection + try: + test_url = f"{current.rstrip('/')}/health" + r = requests.get(test_url, timeout=2) + if r.status_code == 200: + lines.append("โœ… DECTalk API is reachable") + else: + lines.append(f"โš ๏ธ DECTalk API returned status {r.status_code}") + except Exception as e: + lines.append(f"โŒ Cannot reach DECTalk API: {e}") + else: + lines.append("โ„น๏ธ Not currently using DECTalk mode") + lines.append(f"Switch with: `{ctx.clean_prefix}ttstoy mode dectalk`") + + return await ctx.send("\n".join(lines)) + + url = url.strip().rstrip('/') + if not url.startswith(('http://', 'https://')): + return await ctx.send("โŒ URL must start with http:// or https://") + + # Test the URL + try: + test_url = f"{url}/health" + r = requests.get(test_url, timeout=5) + if r.status_code != 200: + return await ctx.send( + f"โš ๏ธ Warning: URL returned status {r.status_code}\n" + f"Saving anyway. Make sure your DECTalk server is running." + ) + except Exception as e: + return await ctx.send( + f"โš ๏ธ Warning: Cannot reach URL: {e}\n" + f"Saving anyway. Make sure your DECTalk server is running." + ) + + await self.config.dectalk_api_url.set(url) + await ctx.send(f"โœ… DECTalk API URL set to: `{url}`") + + @ttstoy_group.command(name="dectalkinstall") + @commands.is_owner() + async def dectalk_install(self, ctx: commands.Context): + """Install the built-in DECTalk server.""" + await ctx.send("๐Ÿ“ฆ Installing DECTalk server (this may take a minute)...") + + success = await self._install_dectalk_server() + + if success: + await ctx.send( + "โœ… DECTalk server installed successfully!\n" + f"Start it with: `{ctx.clean_prefix}ttstoy dectalkstart`\n" + f"Or enable auto-start: `{ctx.clean_prefix}ttstoy dectalkautotoggle`" + ) + else: + await ctx.send( + "โŒ Failed to install DECTalk server.\n" + "Make sure Node.js and npm are installed.\n" + "Check the bot logs for details." + ) + + @ttstoy_group.command(name="dectalkstart") + @commands.is_owner() + async def dectalk_start(self, ctx: commands.Context): + """Start the built-in DECTalk server.""" + await ctx.send("๐Ÿš€ Starting DECTalk server...") + + success = await self._start_dectalk_server() + + if success: + await ctx.send( + f"โœ… DECTalk server started on port {self.dectalk_port}!\n" + f"Switch to DECTalk mode: `{ctx.clean_prefix}ttstoy mode dectalk`" + ) + else: + await ctx.send( + "โŒ Failed to start DECTalk server.\n" + f"Try installing first: `{ctx.clean_prefix}ttstoy dectalkinstall`\n" + "Check the bot logs for details." + ) + + @ttstoy_group.command(name="dectalkstop") + @commands.is_owner() + async def dectalk_stop(self, ctx: commands.Context): + """Stop the built-in DECTalk server.""" + if not self.dectalk_process or self.dectalk_process.poll() is not None: + return await ctx.send("โŒ DECTalk server is not running") + + await self._stop_dectalk_server() + await ctx.send("โœ… DECTalk server stopped") + + @ttstoy_group.command(name="dectalkstatus") + @commands.is_owner() + async def dectalk_status(self, ctx: commands.Context): + """Check DECTalk server status.""" + auto_start = await self.config.dectalk_auto_start() + dectalk_url = await self._get_dectalk_api_url() + + # Check if installed + server_js = self.dectalk_dir / "server.js" + node_modules = self.dectalk_dir / "node_modules" + installed = server_js.exists() and node_modules.exists() + + # Check if process is running + process_running = self.dectalk_process and self.dectalk_process.poll() is None + + # Check if API is responding + api_responding = False + try: + test_url = f"{dectalk_url.rstrip('/')}/health" + r = requests.get(test_url, timeout=2) + api_responding = r.status_code == 200 + except: + pass + + lines = [ + "**DECTalk Server Status**", + f"Installed: {'โœ… Yes' if installed else 'โŒ No'}", + f"Process Running: {'โœ… Yes' if process_running else 'โŒ No'}", + f"API Responding: {'โœ… Yes' if api_responding else 'โŒ No'}", + f"Auto-start: {'โœ… Enabled' if auto_start else 'โŒ Disabled'}", + f"URL: `{dectalk_url}`", + "", + ] + + if not installed: + lines.append(f"Install with: `{ctx.clean_prefix}ttstoy dectalkinstall`") + elif not process_running: + lines.append(f"Start with: `{ctx.clean_prefix}ttstoy dectalkstart`") + + await ctx.send("\n".join(lines)) + + @ttstoy_group.command(name="dectalkautotoggle") + @commands.is_owner() + async def dectalk_auto_toggle(self, ctx: commands.Context): + """Toggle auto-start for built-in DECTalk server.""" + current = await self.config.dectalk_auto_start() + new_state = not current + await self.config.dectalk_auto_start.set(new_state) + + status = "enabled" if new_state else "disabled" + await ctx.send(f"โœ… DECTalk auto-start {status}") + + if new_state: + await ctx.send("DECTalk server will start automatically when switching to DECTalk mode.") + else: + await ctx.send(f"You'll need to manually start DECTalk with `{ctx.clean_prefix}ttstoy dectalkstart`") + + @ttstoy_group.command(name="accessibility") + @commands.is_owner() + async def accessibility_toggle(self, ctx: commands.Context): + """Toggle accessibility mode (forces DECTalk, disables SFX emojis).""" + current = await self.config.accessibility_mode() + new_state = not current + await self.config.accessibility_mode.set(new_state) + + if new_state: + await ctx.send( + "โ™ฟ **Accessibility mode enabled.**\n" + "โ€ข TTS forced to DECTalk\n" + "โ€ข Emoji SFX disabled (emojis read as text instead)" + ) + else: + await ctx.send("โ™ฟ **Accessibility mode disabled.** Normal TTS settings restored.") + + @ttstoy_group.command(name="voxpack") + @commands.is_owner() + async def vox_pack_cmd(self, ctx: commands.Context, pack: Optional[str] = None): + """Show or set the VOX voice pack (vox or vox2).""" + if pack is None: + current = await self.config.vox_pack() + packs = get_available_packs() if VOX_AVAILABLE else [] + return await ctx.send( + f"Current VOX pack: `{current}`\n" + f"Available packs: {', '.join(f'`{p}`' for p in packs) or 'none'}" + ) + + pack = pack.lower().strip() + if not VOX_AVAILABLE: + return await ctx.send("โŒ VOX engine is not available.") + + available = get_available_packs() + if pack not in available: + return await ctx.send(f"โŒ Pack `{pack}` not found. Available: {', '.join(f'`{p}`' for p in available)}") + + await self.config.vox_pack.set(pack) + await ctx.send(f"โœ… VOX pack set to `{pack}`.") + + @ttstoy_group.command(name="voxwords") + async def vox_words_cmd(self, ctx: commands.Context): + """List available words in the current VOX pack.""" + if not VOX_AVAILABLE: + return await ctx.send("โŒ VOX engine is not available.") + + pack = await self.config.vox_pack() + words = get_available_words(pack) + + if not words: + return await ctx.send(f"No words found in VOX pack `{pack}`.") + + # Split into chunks to avoid message length limits + header = f"**VOX Words ({pack}) โ€” {len(words)} words:**\n" + chunk = header + for w in words: + entry = f"`{w}` " + if len(chunk) + len(entry) > 1900: + await ctx.send(chunk) + chunk = "" + chunk += entry + + if chunk: + await ctx.send(chunk) + + @ttstoy_group.command(name="info") + async def ttstoy_info(self, ctx: commands.Context): + """DM usage instructions for TTS Toy.""" + prefix = ctx.clean_prefix + is_owner = await self.bot.is_owner(ctx.author) + mode = await self._get_tts_mode() + + lines = [] + if is_owner: + lines.extend( + [ + "## Setup Instructions (Owner)", + f"1. Choose TTS mode with `{prefix}ttstoy mode [minimax|chatterbox|dectalk|morshu|vox]`.", + "", + "**For MiniMax mode:**", + f"- Set your MiniMax API key with `{prefix}ttstoy key`.", + f"- Pick a model with `{prefix}ttstoy model `.", + "", + "**For Chatterbox mode:**", + f"- Set Chatterbox URL with `{prefix}ttstoy chatterboxurl ` (default: http://127.0.0.1:8004).", + f"- Make sure your Chatterbox TTS server is running.", + "", + "**For DECTalk mode:**", + f"- Set DECTalk API URL with `{prefix}ttstoy dectalkurl `.", + f"- Make sure your DECTalk server is running.", + f"- Use voice commands in your text: `[:np]` Paul, `[:nb]` Betty, `[:nh]` Harry, etc.", + f"- Run `{prefix}ttstoy myvoice` to see all voice commands.", + "", + "**For Morshu mode:**", + f"- Just switch to morshu mode โ€” no setup needed.", + f"- All text is spoken in Morshu's voice from the CD-i Zelda games.", + "", + "**For VOX mode:**", + f"- Black Mesa VOX announcer from Half-Life.", + f"- Only words in the VOX dictionary are spoken; unknown words are skipped.", + f"- Choose pack with `{prefix}ttstoy voxpack [vox|vox2]`.", + f"- List words with `{prefix}ttstoy voxwords`.", + "", + f"2. Set a global default voice with `{prefix}ttstoy voice `.", + f"3. (Optional) Tune emoji SFX volume with `{prefix}ttstoy sfxvolume <0-100>`.", + "", + ] + ) + + mode_emoji = {"minimax": "๐ŸŒ", "chatterbox": "๐Ÿ ", "dectalk": "๐Ÿค–", "morshu": "๐Ÿ›’", "vox": "๐Ÿ“ข"}.get(mode, "โ“") + lines.extend( + [ + "## End User Instructions", + f"Current mode: **{mode.upper()}** {mode_emoji}", + "", + f"1. (Optional) Set your own voice with `{prefix}ttstoy myvoice `.", + f"2. Check your current/personal voice with `{prefix}ttstoy myvoice`.", + f"3. Speak in voice chat with `{prefix}tts `.", + "4. Include supported emoji in your text to trigger SFX clips.", + ] + ) + + try: + await ctx.author.send("\n".join(lines)) + except discord.Forbidden: + await ctx.send("โŒ I couldn't DM you. Please enable DMs and try again.") + return + + await ctx.send("โœ… I sent you a DM with ttstoy instructions.") + + # --------- API KEY VIA BUTTON + MODAL --------- + @ttstoy_group.command(name="key") + @commands.is_owner() + async def minimax_key(self, ctx: commands.Context): + """ + Opens a button that launches a dialog to enter the MiniMax API key. + """ + view = MinimaxKeyButton(self.config) + await ctx.send("Click the button to enter your MiniMax API key:", view=view) + + # --------- MODEL SET / LIST --------- + @ttstoy_group.command(name="model") + @commands.is_owner() + async def minimax_model(self, ctx: commands.Context, model: Optional[str] = None): + """ + Show or set the MiniMax TTS model. + + - `[p]ttstoy model` โ†’ show known models & current setting + - `[p]ttstoy model speech-01-turbo` โ†’ set model + """ + # If no model given, show list + current + if model is None: + current = await self.config.minimax_model() + + lines = [ + "**MiniMax TTS Models**", + "", + "These are example model names you can use (subject to your MiniMax account):", + "- `speech-01-turbo` โ€“ fast & cheap", + "- `speech-01-hd` โ€“ higher quality, slower", + "- `speech-02-turbo` โ€“ newer fast model", + "- `speech-02-hd` โ€“ newer high quality model", + "", + f"Current configured model: `{current}`", + "", + f"To change it, run:\n`{ctx.clean_prefix}ttstoy model `", + ] + return await ctx.send("\n".join(lines)) + + # Model given: set it + model = model.strip() + await self.config.minimax_model.set(model) + await ctx.send(f"โœ… MiniMax model set to `{model}`.") + + # --------- GLOBAL VOICE --------- + @ttstoy_group.command(name="voice") + @commands.is_owner() + async def minimax_voice(self, ctx: commands.Context, *, voice_id: Optional[str] = None): + """ + Set or show the global default voice. + Usage: [p]ttstoy voice [voice_name] + """ + mode = await self._get_tts_mode() + + # Show voice list if no voice provided + if not voice_id: + current = await self.config.minimax_voice() + + if mode == "dectalk": + lines = [ + "**DECTalk Voice Commands** (use in text):", + "`[:np]` Paul (default) โ€ข `[:nb]` Betty โ€ข `[:nh]` Harry โ€ข `[:nf]` Frank", + "`[:nd]` Dennis โ€ข `[:nk]` Kit โ€ข `[:nu]` Ursula โ€ข `[:nr]` Rita โ€ข `[:nw]` Wendy", + "", + "**Example:**", + f"`{ctx.clean_prefix}tts [:nh]Deep voice [:nb]now female`", + "", + "โ„น๏ธ DECTalk doesn't use voice selection - control voices in your text." + ] + return await ctx.send("\n".join(lines)) + + elif mode == "chatterbox": + return await ctx.send( + f"Current global voice: `{current or 'Emily.wav'}`\n" + f"List voices: `{ctx.clean_prefix}ttstoy voices`\n" + f"Set with: `{ctx.clean_prefix}ttstoy voice `" + ) + + elif mode == "morshu": + return await ctx.send( + "**MorshuTalk Mode**\n" + "No voice selection - it's always Morshu.\n" + "Lamp, oil, rope, bombs? You want it? It's yours, my friend." + ) + + elif mode == "vox": + vox_pack = await self.config.vox_pack() + words = get_available_words(vox_pack) if VOX_AVAILABLE else [] + return await ctx.send( + f"**๐Ÿ“ข VOX Mode** (pack: `{vox_pack}`)\n" + f"No voice selection. {len(words)} words available.\n" + f"Switch pack: `{ctx.clean_prefix}ttstoy voxpack [vox|vox2]`\n" + f"List words: `{ctx.clean_prefix}ttstoy voxwords`" + ) + + else: # minimax mode + voice_list = "\n".join([f"โ€ข `{name}` โ†’ {vid}" for name, vid in sorted(MINIMAX_VOICES.items())]) + lines = [ + "**Available MiniMax Voices:**", + voice_list, + "", + f"Current global voice: `{current or DEFAULT_MINIMAX_VOICE}`", + f"Set with: `{ctx.clean_prefix}ttstoy voice `" + ] + return await ctx.send("\n".join(lines)) + + # DECTalk doesn't support voice selection - users use commands in text + if mode == "dectalk": + return await ctx.send( + "DECTalk mode doesn't use voice selection.\n" + "Users control voices with commands in their text:\n" + "`[:np]` Paul, `[:nb]` Betty, `[:nh]` Harry, `[:nf]` Frank, `[:nd]` Dennis, " + "`[:nk]` Kit, `[:nu]` Ursula, `[:nr]` Rita, `[:nw]` Wendy\n\n" + "Example: `[p]tts [:nh]Deep voice [:nb]now female`" + ) + + # Morshu doesn't support voice selection + if mode == "morshu": + return await ctx.send("MorshuTalk doesn't use voice selection - it's always Morshu.") + + # VOX doesn't support voice selection - use voxpack instead + if mode == "vox": + return await ctx.send("๐Ÿ“ข VOX mode doesn't use voice selection. Use `[p]ttstoy voxpack` to switch packs.") + + voice_id = voice_id.strip() + + await self.config.minimax_voice.set(voice_id) + + if mode == "chatterbox": + resolved = voice_id + else: + resolved = resolve_minimax_voice_id(voice_id) + + await ctx.send(f"โœ… Global voice set โ†’ `{resolved}`.") + + # --------- USER VOICE --------- + @ttstoy_group.command(name="myvoice") + @commands.guild_only() + async def my_minimax_voice(self, ctx: commands.Context, *, voice_id: Optional[str] = None): + """ + Set or show your personal voice. + Usage: [p]ttstoy myvoice [voice_name] + """ + mode = await self._get_tts_mode() + + # Show voice list if no voice provided + if not voice_id: + cur = await self.config.user(ctx.author).minimax_voice() + glob = await self.config.minimax_voice() + + if mode == "dectalk": + lines = [ + "**DECTalk Voice Commands** (use in your text):", + "`[:np]` Paul (default) โ€ข `[:nb]` Betty โ€ข `[:nh]` Harry โ€ข `[:nf]` Frank", + "`[:nd]` Dennis โ€ข `[:nk]` Kit โ€ข `[:nu]` Ursula โ€ข `[:nr]` Rita โ€ข `[:nw]` Wendy", + "", + "**Example:**", + f"`{ctx.clean_prefix}tts [:nh]I'm gonna eat a pizza. [:dial67589340] Hi, can i order a pizza?`", + "", + "โ„น๏ธ DECTalk doesn't use voice selection - control voices in your text." + ] + return await ctx.send("\n".join(lines)) + + elif mode == "chatterbox": + user_voices = await self.config.user(ctx.author).chatterbox_voices() + cur_display = "None (using default)" + if cur: + parts = cur.split("_", 1) + cur_display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else cur.rsplit(".", 1)[0] + + lines = [f"**๐Ÿ  Chatterbox Mode**"] + lines.append(f"Your active voice: `{cur_display}`") + + if user_voices: + lines.append("\n**Your voices:**") + for v in user_voices: + parts = v.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else v.rsplit(".", 1)[0] + marker = " โ† active" if v == cur else "" + lines.append(f" `{display}`{marker}") + else: + lines.append("\nNo uploaded voices yet.") + + lines.append(f"\nSwitch: `{ctx.clean_prefix}ttstoy myvoice `") + lines.append(f"Upload: `{ctx.clean_prefix}chatterbox addvoice ` (attach audio)") + return await ctx.send("\n".join(lines)) + + elif mode == "morshu": + return await ctx.send( + "**๐Ÿ›’ MorshuTalk Mode**\n" + "No voice selection - it's always Morshu.\n" + "Lamp, oil, rope, bombs? You want it? It's yours, my friend." + ) + + elif mode == "vox": + vox_pack = await self.config.vox_pack() + return await ctx.send( + f"**๐Ÿ“ข VOX Mode** (pack: `{vox_pack}`)\n" + f"No voice selection. Use `{ctx.clean_prefix}ttstoy voxpack` to switch packs." + ) + + else: # minimax mode + lines = [f"**๐ŸŒ MiniMax Mode**"] + lines.append(f"Your voice: `{cur or 'None (using global)'}`") + lines.append(f"Global default: `{glob or DEFAULT_MINIMAX_VOICE}`") + lines.append("\n**Available voice labels:**") + for k, v in sorted(MINIMAX_VOICES.items()): + lines.append(f"โ€ข `{k}` โ†’ {v}") + lines.append(f"\nSet with: `{ctx.clean_prefix}ttstoy myvoice `") + + return await ctx.send("\n".join(lines)) + + # Setting a voice - DECTalk doesn't support voice selection + if mode == "dectalk": + return await ctx.send( + "โ„น๏ธ DECTalk mode doesn't use voice selection.\n" + "Control voices with commands in your text instead." + ) + + # Morshu doesn't support voice selection + if mode == "morshu": + return await ctx.send("๐Ÿ›’ MorshuTalk doesn't use voice selection - it's always Morshu.") + + # VOX doesn't support voice selection + if mode == "vox": + return await ctx.send("๐Ÿ“ข VOX mode doesn't use voice selection. Use `[p]ttstoy voxpack` to switch packs.") + + voice_id = voice_id.strip() + + # Chatterbox: match against user's uploaded voices by display name + if mode == "chatterbox": + user_voices = await self.config.user(ctx.author).chatterbox_voices() + if not user_voices: + return await ctx.send( + f"You don't have any uploaded voices.\n" + f"Upload one: `{ctx.clean_prefix}chatterbox addvoice ` (attach audio)" + ) + + match = None + for v in user_voices: + parts = v.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else v.rsplit(".", 1)[0] + if display.lower() == voice_id.lower() or v.lower() == voice_id.lower(): + match = v + break + + if not match: + voice_names = ", ".join( + v.split("_", 1)[1].rsplit(".", 1)[0] if "_" in v else v.rsplit(".", 1)[0] + for v in user_voices + ) + return await ctx.send(f"โŒ Voice `{voice_id}` not found.\nYour voices: {voice_names}") + + await self.config.user(ctx.author).minimax_voice.set(match) + display = match.split("_", 1)[1].rsplit(".", 1)[0] if "_" in match else match.rsplit(".", 1)[0] + return await ctx.send(f"โœ… Your voice โ†’ `{display}`.") + + await self.config.user(ctx.author).minimax_voice.set(voice_id.strip()) + + if mode == "chatterbox": + resolved = voice_id.strip() + else: + resolved = resolve_minimax_voice_id(voice_id.strip()) + + return await ctx.send(f"โœ… Your voice โ†’ `{resolved}`.") + + async def _process_tts_queue(self, guild_id: int): + """Process TTS queue for a guild - plays TTS items in order.""" + queue = self.tts_queues[guild_id] + + # Track music state across all TTS in a batch + music_paused = False + saved_state = None + + while True: + try: + # Wait for next TTS item + tts_item = await queue.get() + + if tts_item is None: # Shutdown signal + break + + ctx, audio_path = tts_item + + try: + player = lavalink.get_player(guild_id) + player.store("channel", ctx.channel.id) + + loaded = await player.load_tracks(audio_path) + if loaded.has_error or loaded.load_type != lavalink.enums.LoadType.TRACK_LOADED: + log.error(f"Failed to load TTS audio: {audio_path}") + queue.task_done() + continue + + # Only pause and save state on the FIRST TTS in a batch + if not music_paused: + was_playing = player.is_playing + current_position = player.position if player.current else 0 + original_queue = list(player.queue) + original_current = player.current + + saved_state = { + 'was_playing': was_playing, + 'position': current_position, + 'queue': original_queue, + 'current': original_current + } + + if was_playing: + await player.pause() + + # Store current track temporarily and clear queue + if original_current: + player.queue.clear() + + music_paused = True + + # Play TTS + player.add(requester=ctx.author, track=loaded.tracks[0]) + await player.play() + + # Wait for TTS to finish + while player.is_playing: + await asyncio.sleep(0.1) + + except Exception as e: + log.exception(f"Error processing TTS for guild {guild_id}: {e}") + finally: + queue.task_done() + + # Check if queue is empty - if so, restore music + if queue.empty() and music_paused and saved_state: + try: + # Restore the original queue and position + if saved_state['current']: + player.add(requester=saved_state['current'].requester, track=saved_state['current']) + for track in saved_state['queue']: + player.add(requester=track.requester, track=track) + + # Resume from where we left off + if saved_state['was_playing']: + await player.play() + await player.seek(saved_state['position']) + except Exception as e: + log.exception(f"Error restoring music for guild {guild_id}: {e}") + finally: + music_paused = False + saved_state = None + + except asyncio.CancelledError: + break + except Exception as e: + log.exception(f"Unexpected error in TTS queue processor for guild {guild_id}: {e}") + + def _get_or_create_tts_queue(self, guild_id: int): + """Get or create TTS queue and processor for a guild.""" + if guild_id not in self.tts_queues: + self.tts_queues[guild_id] = asyncio.Queue() + self.tts_locks[guild_id] = asyncio.Lock() + # Start processor task for this guild + self.tts_processors[guild_id] = asyncio.create_task( + self._process_tts_queue(guild_id) + ) + return self.tts_queues[guild_id] + + # --------------------------------------------------------------------- + # SPEAK COMMAND (JUST [p]tts) + # --------------------------------------------------------------------- + @commands.hybrid_command(name="tts") + @commands.guild_only() + async def tts(self, ctx: commands.Context, *, text: str): + """ + Generate TTS audio and upload the MP3. Also plays in voice chat if you're in one. + """ + async with ctx.typing(): + # Check if user is in a VC โ€” we'll play there if so, but it's not required + user_in_vc = ctx.author.voice and ctx.author.voice.channel + audio: Optional[Audio] = self.bot.get_cog("Audio") + + if user_in_vc and audio and not ctx.guild.me.voice: + await ctx.invoke(audio.command_summon) + + mode = await self._get_tts_mode() + + # Not in VC: only chatterbox is allowed + if not user_in_vc and mode != "chatterbox": + return await ctx.send("โŒ You must be in a voice channel to use **{}** mode. Join a VC, or switch to **chatterbox** mode (`[p]ttstoy mode chatterbox`).".format(mode)) + + chatterbox_url = await self._get_chatterbox_api_url() + dectalk_url = await self._get_dectalk_api_url() + api_key, model, _ = await self._get_minimax_settings() + + # Accessibility mode: force dectalk, strip emoji SFX + accessibility = await self.config.accessibility_mode() + if accessibility: + mode = "dectalk" + # Strip emoji triggers so they're read as text, not played as SFX + text = EMOJI_CANDIDATE_PATTERN.sub( + lambda m: _normalize_sfx_trigger(m.group(0)) if _normalize_sfx_trigger(m.group(0)) in EMOJI_SFX_FOLDERS else m.group(0), + text + ) + text = re.sub(r'[\U0001F300-\U0001FAFF\U00002600-\U000026FF\U00002700-\U000027BF]', '', text).strip() + + # Check requirements based on mode + if mode == "minimax" and not api_key: + return await ctx.send("โŒ MiniMax API key not set. Use `[p]ttstoy key` or switch to chatterbox/dectalk mode.") + + if mode == "morshu" and not MORSHU_AVAILABLE: + return await ctx.send("โŒ MorshuTalk engine failed to load. Check bot logs for details (missing g2p_en, numpy, or pydub?).") + + if mode == "vox" and not VOX_AVAILABLE: + return await ctx.send("โŒ VOX engine is not available.") + + if mode == "chatterbox": + try: + test_url = f"{chatterbox_url.rstrip('/')}/api/ui/initial-data" + r = requests.get(test_url, timeout=3) + if r.status_code != 200: + return await ctx.send(f"โŒ Chatterbox TTS not responding (status {r.status_code}). Check if server is running.") + # Also verify /tts endpoint exists + openapi_url = f"{chatterbox_url.rstrip('/')}/openapi.json" + r2 = requests.get(openapi_url, timeout=3) + if r2.status_code == 200: + paths = list(r2.json().get("paths", {}).keys()) + log.info(f"[Chatterbox] Server routes: {paths}") + if "/tts" not in paths: + return await ctx.send(f"โŒ Chatterbox server is running but `/tts` endpoint not found.\nAvailable routes: {', '.join(paths[:10])}") + except Exception as e: + return await ctx.send(f"โŒ Cannot reach Chatterbox TTS: {e}\nMake sure the server is running at `{chatterbox_url}`") + + if mode == "dectalk": + # Test DECTalk API availability + try: + test_url = f"{dectalk_url.rstrip('/')}/health" + r = requests.get(test_url, timeout=2) + if r.status_code != 200: + return await ctx.send(f"โŒ DECTalk API not responding (status {r.status_code}). Check if server is running.") + except Exception as e: + return await ctx.send(f"โŒ Cannot reach DECTalk API: {e}\nMake sure the server is running at `{dectalk_url}`") + + voice_id = await self._get_effective_minimax_voice(ctx.author) + sfx_volume = await self.config.sfx_volume() + vox_pack = await self.config.vox_pack() + + # Fetch per-user chatterbox settings (per-voice) + cb_temp = None + cb_exag = None + cb_vol_db = 0.0 + cb_speed = None + if mode == "chatterbox": + temp_offsets = await self.config.user(ctx.author).chatterbox_temperature_offsets() + exag_offsets = await self.config.user(ctx.author).chatterbox_exaggeration_offsets() + vol_offsets = await self.config.user(ctx.author).chatterbox_volume_offsets() + speed_offsets = await self.config.user(ctx.author).chatterbox_speed_offsets() + cb_temp = temp_offsets.get(voice_id) + cb_exag = exag_offsets.get(voice_id) + cb_vol_db = vol_offsets.get(voice_id, 0.0) + cb_speed = speed_offsets.get(voice_id) + + # Fallback: check old global settings for migration + if cb_temp is None: + cb_temp = await self.config.user(ctx.author).chatterbox_temperature() + if cb_exag is None: + cb_exag = await self.config.user(ctx.author).chatterbox_exaggeration() + + self.tts_storage.mkdir(parents=True, exist_ok=True) + audio_path = str(self.tts_storage.joinpath(f"{ctx.message.id}.mp3")) + + # Collect all chatterbox voices for inline voice resolution + all_cb_voices = await self._get_all_chatterbox_voices() + + try: + result = await asyncio.to_thread( + self._save_prompt_audio, api_key, model, voice_id, text, audio_path, sfx_volume, mode, dectalk_url, vox_pack, chatterbox_url, cb_temp, cb_exag, cb_vol_db, cb_speed, all_cb_voices + ) + detected_language = result.get("language") if result else None + except Exception as e: + log.exception("TTS error: %s", e) + mode_name = {"minimax": "MiniMax", "chatterbox": "Chatterbox", "dectalk": "DECTalk", "morshu": "MorshuTalk", "vox": "VOX"}.get(mode, "TTS") + return await ctx.send(f"{mode_name} error generating audio: {e}") + + view = discord.ui.View() + view.add_item( + discord.ui.Button( + label="PLS DONATE", + style=discord.ButtonStyle.link, + url="https://account.venmo.com/u/kingstonscyd", + ) + ) + + filename = f"{mode}-tts-{ctx.message.id}.mp3" + + # Build response message with language detection + LANG_NAMES = { + "EN": "English", "ZH": "Chinese", "JA": "Japanese", + "ES": "Spanish", "FR": "French", "DE": "German", + "KO": "Korean", "PT": "Portuguese", "RU": "Russian", + "IT": "Italian", "AR": "Arabic", "HI": "Hindi", + "TH": "Thai", "TR": "Turkish", "VI": "Vietnamese", + "NL": "Dutch", "PL": "Polish", "SV": "Swedish", + } + if detected_language: + code = detected_language.upper() + lang_display = LANG_NAMES.get(code, code) + lang_line = f"Language detected: {lang_display} ({code})" + else: + lang_line = "Language detected: Unknown" + + await ctx.send( + content=lang_line, + file=discord.File(audio_path, filename=filename), + view=view, + ) + + # Play in VC only if user is in a voice channel + if user_in_vc and audio and ctx.guild.me.voice: + queue = self._get_or_create_tts_queue(ctx.guild.id) + queue_position = queue.qsize() + 1 + + if queue_position > 1: + await ctx.send(f"Added to TTS queue (position {queue_position})") + + await queue.put((ctx, audio_path)) + + if ctx.interaction: + await ctx.reply("๐Ÿ—ฃ") + else: + await ctx.react_quietly("๐Ÿ—ฃ") + + # ========================================================================= + # WEB UI โ€” login, webui commands, and shared-state processor + # ========================================================================= + + WEBUI_SHARED_STATE = Path(os.environ.get("TTSTOY_SHARED_STATE", "/tmp/ttstoy_webui_state.json")) + + def _read_shared_state(self) -> dict: + try: + with open(self.WEBUI_SHARED_STATE) as f: + return json.load(f) + except Exception: + return {} + + def _write_shared_state(self, state: dict): + try: + self.WEBUI_SHARED_STATE.parent.mkdir(parents=True, exist_ok=True) + with open(self.WEBUI_SHARED_STATE, "w") as f: + json.dump(state, f) + except Exception as e: + log.warning(f"[WebUI] Could not write shared state: {e}") + + def _get_webui_url(self) -> str: + state = self._read_shared_state() + return state.get("webui_url", os.environ.get("WEBUI_URL", "https://ttstoy.kingstons-scrapyard.net")) + + def _get_internal_secret(self) -> str: + state = self._read_shared_state() + return state.get("internal_secret", "") + + @ttstoy_group.command(name="login") + @commands.guild_only() + async def webui_login(self, ctx: commands.Context): + """Get a one-time login key for Kingston's Scrapyard. Must be used in a server so the key knows which server you're in. The key is DM'd to you.""" + webui_url = self._get_webui_url() + secret = self._get_internal_secret() + if not secret: + return await ctx.send( + "โŒ The web UI is not running. Ask the bot owner to start it.", + ephemeral=True, + ) + + # Register token with ttstoy webui โ€” it writes to shared_auth.json. + # The user logs in once on the Homepage; the session cookie is shared + # across all *.kingstons-scrapyard.net subdomains. + avatar_url = str(ctx.author.display_avatar.url) if ctx.author.display_avatar else "" + try: + r = await asyncio.to_thread( + requests.post, + f"{webui_url.rstrip('/')}/api/register_token", + headers={"X-Internal-Secret": secret, "Content-Type": "application/json"}, + json={ + "user_id": str(ctx.author.id), + "discord_name": str(ctx.author), + "avatar_url": avatar_url, + "is_owner": await self.bot.is_owner(ctx.author), + "guild_id": str(ctx.guild.id), + "guild_name": ctx.guild.name, + }, + timeout=5, + ) + r.raise_for_status() + token = r.json()["token"] + except Exception as e: + log.exception("[WebUI] Failed to get login token: %s", e) + return await ctx.send("โŒ Could not reach the web UI. Is it running?", ephemeral=True) + + try: + await ctx.author.send( + f"**Kingston's Scrapyard โ€” Login**\n\n" + f"Go to: https://homepage.kingstons-scrapyard.net/login\n\n" + f"Your key (valid for 5 minutes):\n```\n{token}\n```\n" + f"Log in once and you'll be signed in across all Scrapyard sites." + ) + except discord.Forbidden: + return await ctx.send("โŒ I couldn't DM you. Enable DMs from server members and try again.") + + await ctx.send("โœ… I sent you a DM with your login key.", ephemeral=True) + + @ttstoy_group.command(name="webui") + async def webui_url_cmd(self, ctx: commands.Context): + """Get the link to the TtsToy web UI.""" + webui_url = self._get_webui_url() + await ctx.send( + f"๐ŸŒ **TtsToy Web UI:** {webui_url}\n" + f"Run `{ctx.clean_prefix}ttstoy login` to get your login key.", + ephemeral=True, + ) + + @ttstoy_group.command(name="webuistatus") + @commands.is_owner() + async def webui_status_cmd(self, ctx: commands.Context): + """Check the status of the TtsToy web UI.""" + webui_url = self._get_webui_url() + process_running = self._webui_process and self._webui_process.poll() is None + pid = self._webui_process.pid if process_running else None + + # Try to hit the login page + reachable = False + try: + r = await asyncio.to_thread(requests.get, f"{webui_url}/login", timeout=3) + reachable = r.status_code == 200 + except Exception: + pass + + lines = [ + "**TtsToy Web UI Status**", + f"Process running: {'โœ… Yes' if process_running else 'โŒ No'}{f' (pid {pid})' if pid else ''}", + f"Reachable at URL: {'โœ… Yes' if reachable else 'โŒ No'}", + f"URL: {webui_url}", + ] + if not process_running: + lines.append(f"\nRestart with: `{ctx.clean_prefix}ttstoy webuirestart`") + await ctx.send("\n".join(lines)) + + @ttstoy_group.command(name="webuirestart") + @commands.is_owner() + async def webui_restart_cmd(self, ctx: commands.Context): + """Restart the TtsToy web UI.""" + await ctx.send("๐Ÿ”„ Restarting web UI...") + await self._stop_webui() + success = await self._start_webui() + if success: + await ctx.send(f"โœ… Web UI restarted at {self._get_webui_url()}") + else: + await ctx.send("โŒ Failed to start web UI. Check bot logs.") + + # ------------------------------------------------------------------------- + # Background task โ€” poll shared state for pending commands and posts + # ------------------------------------------------------------------------- + + @tasks.loop(seconds=3) + async def _webui_state_processor(self): + """Poll the shared state file and execute pending commands/posts from the web UI.""" + try: + state = self._read_shared_state() + changed = False + + # --- Process pending commands (voice/mode changes from web UI) --- + pending_cmds = state.pop("pending_commands", []) + if pending_cmds: + changed = True + for cmd in pending_cmds: + await self._handle_webui_command(cmd) + + # --- Process pending posts (TTS audio to post to Discord channel) --- + pending_posts = state.pop("pending_posts", []) + if pending_posts: + changed = True + for post in pending_posts: + await self._handle_webui_post(post) + + # --- Write updated user info so the profile page stays fresh --- + users_info = state.get("users", {}) + for guild in self.bot.guilds: + for member in guild.members: + uid = str(member.id) + if uid not in users_info: + users_info[uid] = {} + users_info[uid]["display_name"] = member.display_name + users_info[uid]["username"] = str(member) + users_info[uid]["joined_at"] = str(member.joined_at)[:10] if member.joined_at else "unknown" + users_info[uid]["roles"] = ", ".join(r.name for r in member.roles if r.name != "@everyone") + state["users"] = users_info + + if changed or True: # always write to keep user info fresh + self._write_shared_state(state) + + # Reload emojiโ†’SFX mapping in case it was changed via the web UI + _reload_emoji_sfx_map() + + except Exception as e: + log.exception("[WebUI] State processor error: %s", e) + + async def _handle_webui_command(self, cmd: dict): + """Execute a command queued by the web UI.""" + user_id = int(cmd.get("user_id", 0)) + action = cmd.get("cmd") + payload = cmd.get("payload", {}) + + user = self.bot.get_user(user_id) + if not user: + log.warning(f"[WebUI] Unknown user {user_id} for command {action}") + return + + try: + if action == "speak_in_vc": + await self._webui_speak_in_vc(user, payload) + + elif action == "set_voice": + voice = payload.get("voice", "") + await self.config.user(user).minimax_voice.set(voice) + log.info(f"[WebUI] Set voice for {user} โ†’ {voice}") + + elif action == "set_mode": + if await self.bot.is_owner(user): + mode = payload.get("mode", "minimax") + await self.config.tts_mode.set(mode) + log.info(f"[WebUI] Set global TTS mode โ†’ {mode}") + + elif action == "set_chatterbox_params": + voice = payload.get("voice", "") + params = payload.get("params", {}) + if "temperature" in params: + offsets = await self.config.user(user).chatterbox_temperature_offsets() + offsets[voice] = params["temperature"] + await self.config.user(user).chatterbox_temperature_offsets.set(offsets) + if "exaggeration" in params: + offsets = await self.config.user(user).chatterbox_exaggeration_offsets() + offsets[voice] = params["exaggeration"] + await self.config.user(user).chatterbox_exaggeration_offsets.set(offsets) + if "volume_db" in params: + offsets = await self.config.user(user).chatterbox_volume_offsets() + offsets[voice] = params["volume_db"] + await self.config.user(user).chatterbox_volume_offsets.set(offsets) + if "speed" in params: + offsets = await self.config.user(user).chatterbox_speed_offsets() + offsets[voice] = params["speed"] + await self.config.user(user).chatterbox_speed_offsets.set(offsets) + log.info(f"[WebUI] Updated chatterbox params for {user} voice={voice}") + + elif action == "add_voice": + filename = payload.get("filename", "") + voices = await self.config.user(user).chatterbox_voices() + if filename and filename not in voices: + voices.append(filename) + await self.config.user(user).chatterbox_voices.set(voices) + await self.config.user(user).minimax_voice.set(filename) + log.info(f"[WebUI] Added voice {filename} for {user}") + + elif action == "remove_voice": + filename = payload.get("filename", "") + voices = await self.config.user(user).chatterbox_voices() + if filename in voices: + voices.remove(filename) + await self.config.user(user).chatterbox_voices.set(voices) + for key in ("chatterbox_temperature_offsets", "chatterbox_exaggeration_offsets", + "chatterbox_volume_offsets", "chatterbox_speed_offsets"): + offsets = await getattr(self.config.user(user), key)() + offsets.pop(filename, None) + await getattr(self.config.user(user), key).set(offsets) + current = await self.config.user(user).minimax_voice() + if current == filename: + await self.config.user(user).minimax_voice.set(voices[0] if voices else None) + log.info(f"[WebUI] Removed voice {filename} for {user}") + + except Exception as e: + log.exception(f"[WebUI] Error handling command {action} for user {user_id}: {e}") + + async def _webui_speak_in_vc(self, user: discord.User, payload: dict): + """Generate TTS from a web UI request and play it in the user's current VC.""" + job_id = payload.get("job_id") + text = payload.get("text", "") + discord_name = payload.get("user", str(user)) + mode_override = payload.get("mode_override") # per-request mode from web UI + voice_override = payload.get("voice_override") # per-request voice from web UI + skip_vc = payload.get("skip_vc", False) + guild_id_str = payload.get("guild_id", "") + log.info(f"[WebUI] speak_in_vc: user={user} job={job_id} guild={guild_id_str} mode_override={mode_override} voice_override={voice_override} skip_vc={skip_vc} text={text[:60]!r}") + + webui_url = self._get_webui_url() + secret = self._get_internal_secret() + + def _update_job(status, **kwargs): + try: + requests.post( + f"{webui_url.rstrip('/')}/api/tts/{job_id}/update", + headers={"X-Internal-Secret": secret, "Content-Type": "application/json"}, + json={"status": status, **kwargs}, + timeout=5, + ) + log.debug(f"[WebUI] Job {job_id} updated โ†’ {status}") + except Exception as e: + log.warning(f"[WebUI] Could not update job {job_id}: {e}") + + # Find which guild/VC the user is currently in + # Prefer the guild they logged in from + member_vc = None + member_guild = None + target_guild_id = int(guild_id_str) if guild_id_str else None + if target_guild_id: + guild = self.bot.get_guild(target_guild_id) + if guild: + member_guild = guild + member = guild.get_member(user.id) + if member and member.voice and member.voice.channel: + member_vc = member.voice.channel + # Fallback: search all guilds for VC presence + if not member_vc: + for guild in self.bot.guilds: + member = guild.get_member(user.id) + if member and member.voice and member.voice.channel: + member_vc = member.voice.channel + if not member_guild: + member_guild = guild + break + + if not member_vc: + log.info(f"[WebUI] {user} is not in a VC โ€” will generate audio only") + + log.info(f"[WebUI] Target VC: {member_vc} in {member_guild}") + + # Gather TTS settings โ€” respect web UI overrides + mode = mode_override if mode_override else await self._get_tts_mode() + chatterbox_url = await self._get_chatterbox_api_url() + dectalk_url = await self._get_dectalk_api_url() + api_key, model, _ = await self._get_minimax_settings() + # Voice: use override if provided, otherwise user's saved voice + if voice_override: + voice_id = voice_override + else: + voice_id = await self._get_effective_minimax_voice(user) + sfx_volume = await self.config.sfx_volume() + vox_pack = await self.config.vox_pack() + cb_temp = cb_exag = cb_speed = None + cb_vol_db = 0.0 + if mode == "chatterbox": + cb_temp = (await self.config.user(user).chatterbox_temperature_offsets()).get(voice_id) + cb_exag = (await self.config.user(user).chatterbox_exaggeration_offsets()).get(voice_id) + cb_vol_db = (await self.config.user(user).chatterbox_volume_offsets()).get(voice_id, 0.0) + cb_speed = (await self.config.user(user).chatterbox_speed_offsets()).get(voice_id) + + log.debug(f"[WebUI] TTS params: mode={mode} voice={voice_id} temp={cb_temp} exag={cb_exag}") + + self.tts_storage.mkdir(parents=True, exist_ok=True) + audio_path = str(self.tts_storage / f"webui_{job_id}.mp3") + + all_cb_voices = await self._get_all_chatterbox_voices() + + try: + await asyncio.to_thread( + self._save_prompt_audio, + api_key, model, voice_id, text, audio_path, + sfx_volume, mode, dectalk_url, vox_pack, + chatterbox_url, cb_temp, cb_exag, cb_vol_db, cb_speed, + all_cb_voices, + ) + log.info(f"[WebUI] Audio generated: {audio_path}") + except Exception as e: + log.exception(f"[WebUI] TTS generation failed for job {job_id}: {e}") + await asyncio.to_thread(_update_job, "error", error=str(e)) + return + + # Queue for VC playback only if user is in a voice channel and skip_vc is not set + if member_vc and member_guild and not skip_vc: + try: + audio_cog: Optional[Audio] = self.bot.get_cog("Audio") + if audio_cog: + text_channel = member_guild.system_channel or next( + (c for c in member_guild.text_channels + if c.permissions_for(member_guild.me).send_messages), None + ) + if text_channel: + if not member_guild.me.voice or member_guild.me.voice.channel != member_vc: + await member_vc.connect() + + class _FakeCtx: + guild = member_guild + channel = text_channel + author = member_guild.get_member(user.id) or user + interaction = None + async def react_quietly(self, *a, **kw): pass + + queue = self._get_or_create_tts_queue(member_guild.id) + await queue.put((_FakeCtx(), audio_path)) + log.info(f"[WebUI] Queued audio for VC playback: job={job_id}") + except Exception as e: + log.exception(f"[WebUI] VC playback failed for job {job_id}: {e}") + + # Post transcript to Discord channel and mark job done + # Resolve the best guild ID for posting: login guild > VC guild > empty + post_guild_id = guild_id_str or (str(member_guild.id) if member_guild else "") + await self._handle_webui_post({ + "job_id": job_id, "path": audio_path, + "text": text, "user": discord_name, "engine": mode, + "guild_id": post_guild_id, + }) + await asyncio.to_thread(_update_job, "done", path=audio_path) + + async def _handle_webui_post(self, post: dict): + """Post a TTS audio file to the configured bot channel.""" + try: + audio_path = post.get("path") + text = post.get("text", "") + user = post.get("user", "Unknown") + engine = post.get("engine", "tts") + guild_id_str = post.get("guild_id", "") + + if not audio_path or not os.path.exists(audio_path): + log.warning(f"[WebUI] Audio file not found: {audio_path}") + return + + # Find the designated TTS channel in the user's login guild + target_guild_id = int(guild_id_str) if guild_id_str else None + channel = await self._get_webui_tts_channel(target_guild_id) + if not channel: + log.warning("[WebUI] No TTS channel configured. Set TTSTOY_WEBUI_CHANNEL_ID env var.") + return + + filename = f"webui-{engine}-tts.mp3" + content = f"๐Ÿ—ฃ **{discord.utils.escape_markdown(user)}** via web UI [{engine}]\n> {discord.utils.escape_markdown(text[:200])}" + await channel.send(content=content, file=discord.File(audio_path, filename=filename)) + log.info(f"[WebUI] Posted TTS from {user} to channel {channel.id}") + + except Exception as e: + log.exception(f"[WebUI] Error posting TTS: {e}") + + async def _get_webui_tts_channel(self, guild_id: int = None) -> Optional[discord.TextChannel]: + """Get the channel to post web UI TTS messages to. + + Checks per-guild configured channel, then global configured channel, + then looks for a channel named tts/ttstoy/tts-toy in the target guild. + """ + # Per-guild configured channel + if guild_id: + guild = self.bot.get_guild(guild_id) + if guild: + guild_channel_id = await self.config.guild(guild).webui_tts_channel_id() + if guild_channel_id: + ch = self.bot.get_channel(int(guild_channel_id)) + if ch: + return ch + + # Global configured channel (use if it's in the target guild, or no guild specified) + channel_id = await self.config.webui_tts_channel_id() + if not channel_id: + channel_id = int(os.environ.get("TTSTOY_WEBUI_CHANNEL_ID", 0)) + if channel_id: + ch = self.bot.get_channel(int(channel_id)) + if ch: + if not guild_id or (hasattr(ch, 'guild') and ch.guild.id == guild_id): + return ch + + # Search the target guild for a tts-named channel + if guild_id: + guild = self.bot.get_guild(guild_id) + if guild: + for ch in guild.text_channels: + if ch.name.lower() in ("tts", "ttstoy", "tts-toy"): + return ch + + # No guild specified โ€” search all guilds + for guild in self.bot.guilds: + for ch in guild.text_channels: + if ch.name.lower() in ("tts", "ttstoy", "tts-toy"): + return ch + return None + + @ttstoy_group.command(name="webuichannel") + @commands.is_owner() + @commands.guild_only() + async def webui_channel_cmd(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None): + """Set or show the channel where web UI TTS audio is posted in this server. + + Usage: + [p]ttstoy webuichannel โ€” show current channel + [p]ttstoy webuichannel #channel โ€” set channel for this server + """ + if channel is None: + guild_channel_id = await self.config.guild(ctx.guild).webui_tts_channel_id() + if guild_channel_id: + ch = self.bot.get_channel(int(guild_channel_id)) + name = ch.mention if ch else f"ID {guild_channel_id} (not found)" + else: + name = "Not set (falls back to any channel named `tts`)" + return await ctx.send(f"Web UI TTS channel for this server: {name}") + + await self.config.guild(ctx.guild).webui_tts_channel_id.set(channel.id) + await ctx.send(f"โœ… Web UI TTS audio will be posted to {channel.mention} in this server") + + # ------------------------------------------------------------------------- + # Web UI subprocess management + # ------------------------------------------------------------------------- + + def _webui_app_path(self) -> Path: + return Path(__file__).resolve().parent / "webui" / "app.py" + + async def _start_webui(self): + """Launch the Flask web UI as a subprocess.""" + if self._webui_process and self._webui_process.poll() is None: + log.info("[WebUI] Already running") + return True + + app_path = self._webui_app_path() + if not app_path.exists(): + log.error(f"[WebUI] app.py not found at {app_path}") + return False + + env = os.environ.copy() + env.setdefault("WEBUI_PORT", "8098") + env.setdefault("WEBUI_URL", f"http://localhost:{env['WEBUI_PORT']}") + env.setdefault("TTSTOY_SHARED_STATE", str(self.WEBUI_SHARED_STATE)) + if "BOT_INSTANCE" not in env: + try: + from redbot.core.data_manager import instance_name + env["BOT_INSTANCE"] = instance_name() + except Exception: + env.setdefault("BOT_INSTANCE", "redbot") + + try: + import sys + python = sys.executable + req_file = app_path.parent / "requirements.txt" + if req_file.exists(): + subprocess.run( + [python, "-m", "pip", "install", "-q", "-r", str(req_file)], + check=True, capture_output=True, + ) + self._webui_process = subprocess.Popen( + [python, str(app_path)], + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + await asyncio.sleep(2) + if self._webui_process.poll() is not None: + out = self._webui_process.stdout.read(500) if self._webui_process.stdout else "" + log.error(f"[WebUI] Process exited immediately: {out}") + return False + log.info(f"[WebUI] Started on port {env['WEBUI_PORT']} (pid {self._webui_process.pid})") + + # Pipe subprocess stdout โ†’ RedBot logger in a background thread + def _pipe_logs(proc): + for line in proc.stdout: + line = line.rstrip() + if line: + log.info(f"[WebUI] {line}") + threading.Thread(target=_pipe_logs, args=(self._webui_process,), daemon=True).start() + + return True + except Exception as e: + log.exception(f"[WebUI] Failed to start: {e}") + return False + + async def _stop_webui(self): + """Stop the Flask web UI subprocess.""" + if self._webui_process and self._webui_process.poll() is None: + try: + self._webui_process.terminate() + await asyncio.sleep(1) + if self._webui_process.poll() is None: + self._webui_process.kill() + log.info("[WebUI] Stopped") + except Exception as e: + log.exception(f"[WebUI] Error stopping: {e}") + self._webui_process = None + + # ------------------------------------------------------------------------- + # Cog lifecycle + # ------------------------------------------------------------------------- + + async def cog_load(self): + """Called when cog is loaded.""" + mode = await self._get_tts_mode() + auto_start = await self.config.dectalk_auto_start() + + if auto_start: + server_js = self.dectalk_dir / "server.js" + node_modules = self.dectalk_dir / "node_modules" + if server_js.exists() and node_modules.exists(): + log.info("DECTalk auto-start: server is installed, starting...") + await self._start_dectalk_server() + elif mode == "dectalk": + log.warning("DECTalk mode active but server not installed. Run [p]ttstoy dectalkinstall") + + # Start health HTTP server + try: + _HealthHandler.cog_ref = self + self._health_server = HTTPServer(("0.0.0.0", TTSTOY_HEALTH_PORT), _HealthHandler) + self._health_thread = threading.Thread( + target=self._health_server.serve_forever, daemon=True + ) + self._health_thread.start() + log.info(f"TtsToy health server started on port {TTSTOY_HEALTH_PORT}") + except Exception: + log.exception("Failed to start TtsToy health server") + + # Start web UI subprocess + await self._start_webui() + + # Start web UI state processor + self._webui_state_processor.start() + log.info("[WebUI] State processor started") diff --git a/ttstoy/vox2_words/00_error_beep01.wav b/ttstoy/vox2_words/00_error_beep01.wav new file mode 100644 index 0000000..8a78799 Binary files /dev/null and b/ttstoy/vox2_words/00_error_beep01.wav differ diff --git a/ttstoy/vox2_words/00_signon_beep01.wav b/ttstoy/vox2_words/00_signon_beep01.wav new file mode 100644 index 0000000..fc1ebef Binary files /dev/null and b/ttstoy/vox2_words/00_signon_beep01.wav differ diff --git a/ttstoy/vox2_words/PC Computer - Black Mesa - Announcement System Military.zip b/ttstoy/vox2_words/PC Computer - Black Mesa - Announcement System Military.zip new file mode 100644 index 0000000..7d85a6b Binary files /dev/null and b/ttstoy/vox2_words/PC Computer - Black Mesa - Announcement System Military.zip differ diff --git a/ttstoy/vox2_words/_comma.wav b/ttstoy/vox2_words/_comma.wav new file mode 100644 index 0000000..31a47dc Binary files /dev/null and b/ttstoy/vox2_words/_comma.wav differ diff --git a/ttstoy/vox2_words/access.wav b/ttstoy/vox2_words/access.wav new file mode 100644 index 0000000..e5d521e Binary files /dev/null and b/ttstoy/vox2_words/access.wav differ diff --git a/ttstoy/vox2_words/acknowledged.wav b/ttstoy/vox2_words/acknowledged.wav new file mode 100644 index 0000000..96246c8 Binary files /dev/null and b/ttstoy/vox2_words/acknowledged.wav differ diff --git a/ttstoy/vox2_words/activate.wav b/ttstoy/vox2_words/activate.wav new file mode 100644 index 0000000..9936b07 Binary files /dev/null and b/ttstoy/vox2_words/activate.wav differ diff --git a/ttstoy/vox2_words/activated.wav b/ttstoy/vox2_words/activated.wav new file mode 100644 index 0000000..a869d4c Binary files /dev/null and b/ttstoy/vox2_words/activated.wav differ diff --git a/ttstoy/vox2_words/activity.wav b/ttstoy/vox2_words/activity.wav new file mode 100644 index 0000000..718c46c Binary files /dev/null and b/ttstoy/vox2_words/activity.wav differ diff --git a/ttstoy/vox2_words/advanced.wav b/ttstoy/vox2_words/advanced.wav new file mode 100644 index 0000000..0278bcd Binary files /dev/null and b/ttstoy/vox2_words/advanced.wav differ diff --git a/ttstoy/vox2_words/alert.wav b/ttstoy/vox2_words/alert.wav new file mode 100644 index 0000000..7ed9d04 Binary files /dev/null and b/ttstoy/vox2_words/alert.wav differ diff --git a/ttstoy/vox2_words/alien.wav b/ttstoy/vox2_words/alien.wav new file mode 100644 index 0000000..53362c9 Binary files /dev/null and b/ttstoy/vox2_words/alien.wav differ diff --git a/ttstoy/vox2_words/all.wav b/ttstoy/vox2_words/all.wav new file mode 100644 index 0000000..5ad050c Binary files /dev/null and b/ttstoy/vox2_words/all.wav differ diff --git a/ttstoy/vox2_words/alpha.wav b/ttstoy/vox2_words/alpha.wav new file mode 100644 index 0000000..f766cf8 Binary files /dev/null and b/ttstoy/vox2_words/alpha.wav differ diff --git a/ttstoy/vox2_words/an.wav b/ttstoy/vox2_words/an.wav new file mode 100644 index 0000000..d3fc83a Binary files /dev/null and b/ttstoy/vox2_words/an.wav differ diff --git a/ttstoy/vox2_words/and.wav b/ttstoy/vox2_words/and.wav new file mode 100644 index 0000000..978bf00 Binary files /dev/null and b/ttstoy/vox2_words/and.wav differ diff --git a/ttstoy/vox2_words/announcement.wav b/ttstoy/vox2_words/announcement.wav new file mode 100644 index 0000000..97d55da Binary files /dev/null and b/ttstoy/vox2_words/announcement.wav differ diff --git a/ttstoy/vox2_words/antenna.wav b/ttstoy/vox2_words/antenna.wav new file mode 100644 index 0000000..d537980 Binary files /dev/null and b/ttstoy/vox2_words/antenna.wav differ diff --git a/ttstoy/vox2_words/any.wav b/ttstoy/vox2_words/any.wav new file mode 100644 index 0000000..c010444 Binary files /dev/null and b/ttstoy/vox2_words/any.wav differ diff --git a/ttstoy/vox2_words/approach.wav b/ttstoy/vox2_words/approach.wav new file mode 100644 index 0000000..8911456 Binary files /dev/null and b/ttstoy/vox2_words/approach.wav differ diff --git a/ttstoy/vox2_words/are.wav b/ttstoy/vox2_words/are.wav new file mode 100644 index 0000000..d47c616 Binary files /dev/null and b/ttstoy/vox2_words/are.wav differ diff --git a/ttstoy/vox2_words/area.wav b/ttstoy/vox2_words/area.wav new file mode 100644 index 0000000..d0d59e6 Binary files /dev/null and b/ttstoy/vox2_words/area.wav differ diff --git a/ttstoy/vox2_words/armed.wav b/ttstoy/vox2_words/armed.wav new file mode 100644 index 0000000..8880e72 Binary files /dev/null and b/ttstoy/vox2_words/armed.wav differ diff --git a/ttstoy/vox2_words/armory.wav b/ttstoy/vox2_words/armory.wav new file mode 100644 index 0000000..9e465cb Binary files /dev/null and b/ttstoy/vox2_words/armory.wav differ diff --git a/ttstoy/vox2_words/atomic.wav b/ttstoy/vox2_words/atomic.wav new file mode 100644 index 0000000..60f0eb5 Binary files /dev/null and b/ttstoy/vox2_words/atomic.wav differ diff --git a/ttstoy/vox2_words/attention.wav b/ttstoy/vox2_words/attention.wav new file mode 100644 index 0000000..fde9003 Binary files /dev/null and b/ttstoy/vox2_words/attention.wav differ diff --git a/ttstoy/vox2_words/authorized.wav b/ttstoy/vox2_words/authorized.wav new file mode 100644 index 0000000..f1b8deb Binary files /dev/null and b/ttstoy/vox2_words/authorized.wav differ diff --git a/ttstoy/vox2_words/automatic.wav b/ttstoy/vox2_words/automatic.wav new file mode 100644 index 0000000..66058b2 Binary files /dev/null and b/ttstoy/vox2_words/automatic.wav differ diff --git a/ttstoy/vox2_words/away.wav b/ttstoy/vox2_words/away.wav new file mode 100644 index 0000000..ed549a3 Binary files /dev/null and b/ttstoy/vox2_words/away.wav differ diff --git a/ttstoy/vox2_words/b.wav b/ttstoy/vox2_words/b.wav new file mode 100644 index 0000000..1513a08 Binary files /dev/null and b/ttstoy/vox2_words/b.wav differ diff --git a/ttstoy/vox2_words/back.wav b/ttstoy/vox2_words/back.wav new file mode 100644 index 0000000..9b6f757 Binary files /dev/null and b/ttstoy/vox2_words/back.wav differ diff --git a/ttstoy/vox2_words/base.wav b/ttstoy/vox2_words/base.wav new file mode 100644 index 0000000..15718e5 Binary files /dev/null and b/ttstoy/vox2_words/base.wav differ diff --git a/ttstoy/vox2_words/biohazard.wav b/ttstoy/vox2_words/biohazard.wav new file mode 100644 index 0000000..2e93556 Binary files /dev/null and b/ttstoy/vox2_words/biohazard.wav differ diff --git a/ttstoy/vox2_words/biological.wav b/ttstoy/vox2_words/biological.wav new file mode 100644 index 0000000..7538396 Binary files /dev/null and b/ttstoy/vox2_words/biological.wav differ diff --git a/ttstoy/vox2_words/black.wav b/ttstoy/vox2_words/black.wav new file mode 100644 index 0000000..b2b6477 Binary files /dev/null and b/ttstoy/vox2_words/black.wav differ diff --git a/ttstoy/vox2_words/blast.wav b/ttstoy/vox2_words/blast.wav new file mode 100644 index 0000000..5df88f6 Binary files /dev/null and b/ttstoy/vox2_words/blast.wav differ diff --git a/ttstoy/vox2_words/bloop.wav b/ttstoy/vox2_words/bloop.wav new file mode 100644 index 0000000..2ad00f8 Binary files /dev/null and b/ttstoy/vox2_words/bloop.wav differ diff --git a/ttstoy/vox2_words/blue.wav b/ttstoy/vox2_words/blue.wav new file mode 100644 index 0000000..bd5849b Binary files /dev/null and b/ttstoy/vox2_words/blue.wav differ diff --git a/ttstoy/vox2_words/bravo.wav b/ttstoy/vox2_words/bravo.wav new file mode 100644 index 0000000..e4c983d Binary files /dev/null and b/ttstoy/vox2_words/bravo.wav differ diff --git a/ttstoy/vox2_words/breach.wav b/ttstoy/vox2_words/breach.wav new file mode 100644 index 0000000..9a0881f Binary files /dev/null and b/ttstoy/vox2_words/breach.wav differ diff --git a/ttstoy/vox2_words/buzwarn.wav b/ttstoy/vox2_words/buzwarn.wav new file mode 100644 index 0000000..e2053dc Binary files /dev/null and b/ttstoy/vox2_words/buzwarn.wav differ diff --git a/ttstoy/vox2_words/bypass.wav b/ttstoy/vox2_words/bypass.wav new file mode 100644 index 0000000..94c24c5 Binary files /dev/null and b/ttstoy/vox2_words/bypass.wav differ diff --git a/ttstoy/vox2_words/cable.wav b/ttstoy/vox2_words/cable.wav new file mode 100644 index 0000000..651e5fb Binary files /dev/null and b/ttstoy/vox2_words/cable.wav differ diff --git a/ttstoy/vox2_words/center.wav b/ttstoy/vox2_words/center.wav new file mode 100644 index 0000000..11c7a6b Binary files /dev/null and b/ttstoy/vox2_words/center.wav differ diff --git a/ttstoy/vox2_words/central.wav b/ttstoy/vox2_words/central.wav new file mode 100644 index 0000000..1da0cd6 Binary files /dev/null and b/ttstoy/vox2_words/central.wav differ diff --git a/ttstoy/vox2_words/chamber.wav b/ttstoy/vox2_words/chamber.wav new file mode 100644 index 0000000..e347881 Binary files /dev/null and b/ttstoy/vox2_words/chamber.wav differ diff --git a/ttstoy/vox2_words/check.wav b/ttstoy/vox2_words/check.wav new file mode 100644 index 0000000..7e669da Binary files /dev/null and b/ttstoy/vox2_words/check.wav differ diff --git a/ttstoy/vox2_words/checkpoint.wav b/ttstoy/vox2_words/checkpoint.wav new file mode 100644 index 0000000..c405363 Binary files /dev/null and b/ttstoy/vox2_words/checkpoint.wav differ diff --git a/ttstoy/vox2_words/chemical.wav b/ttstoy/vox2_words/chemical.wav new file mode 100644 index 0000000..7ed8d6d Binary files /dev/null and b/ttstoy/vox2_words/chemical.wav differ diff --git a/ttstoy/vox2_words/clear.wav b/ttstoy/vox2_words/clear.wav new file mode 100644 index 0000000..ce32db5 Binary files /dev/null and b/ttstoy/vox2_words/clear.wav differ diff --git a/ttstoy/vox2_words/code.wav b/ttstoy/vox2_words/code.wav new file mode 100644 index 0000000..dd9c0b2 Binary files /dev/null and b/ttstoy/vox2_words/code.wav differ diff --git a/ttstoy/vox2_words/command.wav b/ttstoy/vox2_words/command.wav new file mode 100644 index 0000000..e19653d Binary files /dev/null and b/ttstoy/vox2_words/command.wav differ diff --git a/ttstoy/vox2_words/communications.wav b/ttstoy/vox2_words/communications.wav new file mode 100644 index 0000000..4f671c9 Binary files /dev/null and b/ttstoy/vox2_words/communications.wav differ diff --git a/ttstoy/vox2_words/complex.wav b/ttstoy/vox2_words/complex.wav new file mode 100644 index 0000000..fb8e2ee Binary files /dev/null and b/ttstoy/vox2_words/complex.wav differ diff --git a/ttstoy/vox2_words/containment.wav b/ttstoy/vox2_words/containment.wav new file mode 100644 index 0000000..2e018f3 Binary files /dev/null and b/ttstoy/vox2_words/containment.wav differ diff --git a/ttstoy/vox2_words/contamination.wav b/ttstoy/vox2_words/contamination.wav new file mode 100644 index 0000000..7d148cc Binary files /dev/null and b/ttstoy/vox2_words/contamination.wav differ diff --git a/ttstoy/vox2_words/control.wav b/ttstoy/vox2_words/control.wav new file mode 100644 index 0000000..4669934 Binary files /dev/null and b/ttstoy/vox2_words/control.wav differ diff --git a/ttstoy/vox2_words/coolant.wav b/ttstoy/vox2_words/coolant.wav new file mode 100644 index 0000000..5ec0db7 Binary files /dev/null and b/ttstoy/vox2_words/coolant.wav differ diff --git a/ttstoy/vox2_words/core.wav b/ttstoy/vox2_words/core.wav new file mode 100644 index 0000000..a7883be Binary files /dev/null and b/ttstoy/vox2_words/core.wav differ diff --git a/ttstoy/vox2_words/crew.wav b/ttstoy/vox2_words/crew.wav new file mode 100644 index 0000000..7d16760 Binary files /dev/null and b/ttstoy/vox2_words/crew.wav differ diff --git a/ttstoy/vox2_words/cross.wav b/ttstoy/vox2_words/cross.wav new file mode 100644 index 0000000..8385287 Binary files /dev/null and b/ttstoy/vox2_words/cross.wav differ diff --git a/ttstoy/vox2_words/d.wav b/ttstoy/vox2_words/d.wav new file mode 100644 index 0000000..8d4252f Binary files /dev/null and b/ttstoy/vox2_words/d.wav differ diff --git a/ttstoy/vox2_words/dadeda.wav b/ttstoy/vox2_words/dadeda.wav new file mode 100644 index 0000000..11d84c2 Binary files /dev/null and b/ttstoy/vox2_words/dadeda.wav differ diff --git a/ttstoy/vox2_words/damage.wav b/ttstoy/vox2_words/damage.wav new file mode 100644 index 0000000..eada72e Binary files /dev/null and b/ttstoy/vox2_words/damage.wav differ diff --git a/ttstoy/vox2_words/danger.wav b/ttstoy/vox2_words/danger.wav new file mode 100644 index 0000000..dcb6387 Binary files /dev/null and b/ttstoy/vox2_words/danger.wav differ diff --git a/ttstoy/vox2_words/day.wav b/ttstoy/vox2_words/day.wav new file mode 100644 index 0000000..abc05a1 Binary files /dev/null and b/ttstoy/vox2_words/day.wav differ diff --git a/ttstoy/vox2_words/deactivated.wav b/ttstoy/vox2_words/deactivated.wav new file mode 100644 index 0000000..aa124dc Binary files /dev/null and b/ttstoy/vox2_words/deactivated.wav differ diff --git a/ttstoy/vox2_words/deeoo.wav b/ttstoy/vox2_words/deeoo.wav new file mode 100644 index 0000000..3e496ac Binary files /dev/null and b/ttstoy/vox2_words/deeoo.wav differ diff --git a/ttstoy/vox2_words/defense.wav b/ttstoy/vox2_words/defense.wav new file mode 100644 index 0000000..25f9697 Binary files /dev/null and b/ttstoy/vox2_words/defense.wav differ diff --git a/ttstoy/vox2_words/delta.wav b/ttstoy/vox2_words/delta.wav new file mode 100644 index 0000000..e98f263 Binary files /dev/null and b/ttstoy/vox2_words/delta.wav differ diff --git a/ttstoy/vox2_words/denied.wav b/ttstoy/vox2_words/denied.wav new file mode 100644 index 0000000..1a817dc Binary files /dev/null and b/ttstoy/vox2_words/denied.wav differ diff --git a/ttstoy/vox2_words/destroy.wav b/ttstoy/vox2_words/destroy.wav new file mode 100644 index 0000000..fc6e651 Binary files /dev/null and b/ttstoy/vox2_words/destroy.wav differ diff --git a/ttstoy/vox2_words/detected.wav b/ttstoy/vox2_words/detected.wav new file mode 100644 index 0000000..5773727 Binary files /dev/null and b/ttstoy/vox2_words/detected.wav differ diff --git a/ttstoy/vox2_words/detonation.wav b/ttstoy/vox2_words/detonation.wav new file mode 100644 index 0000000..8ccc22c Binary files /dev/null and b/ttstoy/vox2_words/detonation.wav differ diff --git a/ttstoy/vox2_words/device.wav b/ttstoy/vox2_words/device.wav new file mode 100644 index 0000000..1ea2162 Binary files /dev/null and b/ttstoy/vox2_words/device.wav differ diff --git a/ttstoy/vox2_words/dimensional.wav b/ttstoy/vox2_words/dimensional.wav new file mode 100644 index 0000000..6c71388 Binary files /dev/null and b/ttstoy/vox2_words/dimensional.wav differ diff --git a/ttstoy/vox2_words/disengaged.wav b/ttstoy/vox2_words/disengaged.wav new file mode 100644 index 0000000..3f7b6e2 Binary files /dev/null and b/ttstoy/vox2_words/disengaged.wav differ diff --git a/ttstoy/vox2_words/do.wav b/ttstoy/vox2_words/do.wav new file mode 100644 index 0000000..24c208a Binary files /dev/null and b/ttstoy/vox2_words/do.wav differ diff --git a/ttstoy/vox2_words/doop.wav b/ttstoy/vox2_words/doop.wav new file mode 100644 index 0000000..22ca349 Binary files /dev/null and b/ttstoy/vox2_words/doop.wav differ diff --git a/ttstoy/vox2_words/door.wav b/ttstoy/vox2_words/door.wav new file mode 100644 index 0000000..7a18683 Binary files /dev/null and b/ttstoy/vox2_words/door.wav differ diff --git a/ttstoy/vox2_words/down.wav b/ttstoy/vox2_words/down.wav new file mode 100644 index 0000000..3d96d65 Binary files /dev/null and b/ttstoy/vox2_words/down.wav differ diff --git a/ttstoy/vox2_words/e.wav b/ttstoy/vox2_words/e.wav new file mode 100644 index 0000000..8244032 Binary files /dev/null and b/ttstoy/vox2_words/e.wav differ diff --git a/ttstoy/vox2_words/echo.wav b/ttstoy/vox2_words/echo.wav new file mode 100644 index 0000000..d90b00f Binary files /dev/null and b/ttstoy/vox2_words/echo.wav differ diff --git a/ttstoy/vox2_words/eight.wav b/ttstoy/vox2_words/eight.wav new file mode 100644 index 0000000..a0404d2 Binary files /dev/null and b/ttstoy/vox2_words/eight.wav differ diff --git a/ttstoy/vox2_words/eighteen.wav b/ttstoy/vox2_words/eighteen.wav new file mode 100644 index 0000000..731e7fe Binary files /dev/null and b/ttstoy/vox2_words/eighteen.wav differ diff --git a/ttstoy/vox2_words/eighty.wav b/ttstoy/vox2_words/eighty.wav new file mode 100644 index 0000000..6c9be33 Binary files /dev/null and b/ttstoy/vox2_words/eighty.wav differ diff --git a/ttstoy/vox2_words/electric.wav b/ttstoy/vox2_words/electric.wav new file mode 100644 index 0000000..7b1d2a9 Binary files /dev/null and b/ttstoy/vox2_words/electric.wav differ diff --git a/ttstoy/vox2_words/eleven.wav b/ttstoy/vox2_words/eleven.wav new file mode 100644 index 0000000..036a508 Binary files /dev/null and b/ttstoy/vox2_words/eleven.wav differ diff --git a/ttstoy/vox2_words/eliminate.wav b/ttstoy/vox2_words/eliminate.wav new file mode 100644 index 0000000..bb89a88 Binary files /dev/null and b/ttstoy/vox2_words/eliminate.wav differ diff --git a/ttstoy/vox2_words/emergency.wav b/ttstoy/vox2_words/emergency.wav new file mode 100644 index 0000000..d00b955 Binary files /dev/null and b/ttstoy/vox2_words/emergency.wav differ diff --git a/ttstoy/vox2_words/energy.wav b/ttstoy/vox2_words/energy.wav new file mode 100644 index 0000000..29e8a97 Binary files /dev/null and b/ttstoy/vox2_words/energy.wav differ diff --git a/ttstoy/vox2_words/engage.wav b/ttstoy/vox2_words/engage.wav new file mode 100644 index 0000000..741703b Binary files /dev/null and b/ttstoy/vox2_words/engage.wav differ diff --git a/ttstoy/vox2_words/engaged.wav b/ttstoy/vox2_words/engaged.wav new file mode 100644 index 0000000..502c16f Binary files /dev/null and b/ttstoy/vox2_words/engaged.wav differ diff --git a/ttstoy/vox2_words/enter.wav b/ttstoy/vox2_words/enter.wav new file mode 100644 index 0000000..723cefe Binary files /dev/null and b/ttstoy/vox2_words/enter.wav differ diff --git a/ttstoy/vox2_words/entry.wav b/ttstoy/vox2_words/entry.wav new file mode 100644 index 0000000..5bb7606 Binary files /dev/null and b/ttstoy/vox2_words/entry.wav differ diff --git a/ttstoy/vox2_words/escape.wav b/ttstoy/vox2_words/escape.wav new file mode 100644 index 0000000..b8ef00e Binary files /dev/null and b/ttstoy/vox2_words/escape.wav differ diff --git a/ttstoy/vox2_words/evacuate.wav b/ttstoy/vox2_words/evacuate.wav new file mode 100644 index 0000000..05bfa24 Binary files /dev/null and b/ttstoy/vox2_words/evacuate.wav differ diff --git a/ttstoy/vox2_words/exchange.wav b/ttstoy/vox2_words/exchange.wav new file mode 100644 index 0000000..253f935 Binary files /dev/null and b/ttstoy/vox2_words/exchange.wav differ diff --git a/ttstoy/vox2_words/experimental.wav b/ttstoy/vox2_words/experimental.wav new file mode 100644 index 0000000..b1cbda6 Binary files /dev/null and b/ttstoy/vox2_words/experimental.wav differ diff --git a/ttstoy/vox2_words/extreme.wav b/ttstoy/vox2_words/extreme.wav new file mode 100644 index 0000000..d0ba69a Binary files /dev/null and b/ttstoy/vox2_words/extreme.wav differ diff --git a/ttstoy/vox2_words/facility.wav b/ttstoy/vox2_words/facility.wav new file mode 100644 index 0000000..62dfd9f Binary files /dev/null and b/ttstoy/vox2_words/facility.wav differ diff --git a/ttstoy/vox2_words/failed.wav b/ttstoy/vox2_words/failed.wav new file mode 100644 index 0000000..8057db5 Binary files /dev/null and b/ttstoy/vox2_words/failed.wav differ diff --git a/ttstoy/vox2_words/failure.wav b/ttstoy/vox2_words/failure.wav new file mode 100644 index 0000000..db48899 Binary files /dev/null and b/ttstoy/vox2_words/failure.wav differ diff --git a/ttstoy/vox2_words/field.wav b/ttstoy/vox2_words/field.wav new file mode 100644 index 0000000..5e083de Binary files /dev/null and b/ttstoy/vox2_words/field.wav differ diff --git a/ttstoy/vox2_words/fifteen.wav b/ttstoy/vox2_words/fifteen.wav new file mode 100644 index 0000000..20dbb2d Binary files /dev/null and b/ttstoy/vox2_words/fifteen.wav differ diff --git a/ttstoy/vox2_words/fifty.wav b/ttstoy/vox2_words/fifty.wav new file mode 100644 index 0000000..2a19416 Binary files /dev/null and b/ttstoy/vox2_words/fifty.wav differ diff --git a/ttstoy/vox2_words/fire.wav b/ttstoy/vox2_words/fire.wav new file mode 100644 index 0000000..cacc423 Binary files /dev/null and b/ttstoy/vox2_words/fire.wav differ diff --git a/ttstoy/vox2_words/five.wav b/ttstoy/vox2_words/five.wav new file mode 100644 index 0000000..62ab18a Binary files /dev/null and b/ttstoy/vox2_words/five.wav differ diff --git a/ttstoy/vox2_words/forbidden.wav b/ttstoy/vox2_words/forbidden.wav new file mode 100644 index 0000000..00fae8b Binary files /dev/null and b/ttstoy/vox2_words/forbidden.wav differ diff --git a/ttstoy/vox2_words/force.wav b/ttstoy/vox2_words/force.wav new file mode 100644 index 0000000..11df306 Binary files /dev/null and b/ttstoy/vox2_words/force.wav differ diff --git a/ttstoy/vox2_words/forms.wav b/ttstoy/vox2_words/forms.wav new file mode 100644 index 0000000..7f18e0e Binary files /dev/null and b/ttstoy/vox2_words/forms.wav differ diff --git a/ttstoy/vox2_words/forty.wav b/ttstoy/vox2_words/forty.wav new file mode 100644 index 0000000..2451130 Binary files /dev/null and b/ttstoy/vox2_words/forty.wav differ diff --git a/ttstoy/vox2_words/four.wav b/ttstoy/vox2_words/four.wav new file mode 100644 index 0000000..ad10426 Binary files /dev/null and b/ttstoy/vox2_words/four.wav differ diff --git a/ttstoy/vox2_words/fourteen .wav b/ttstoy/vox2_words/fourteen .wav new file mode 100644 index 0000000..e9ebc50 Binary files /dev/null and b/ttstoy/vox2_words/fourteen .wav differ diff --git a/ttstoy/vox2_words/freeman.wav b/ttstoy/vox2_words/freeman.wav new file mode 100644 index 0000000..9044106 Binary files /dev/null and b/ttstoy/vox2_words/freeman.wav differ diff --git a/ttstoy/vox2_words/from.wav b/ttstoy/vox2_words/from.wav new file mode 100644 index 0000000..a09a8de Binary files /dev/null and b/ttstoy/vox2_words/from.wav differ diff --git a/ttstoy/vox2_words/fuel.wav b/ttstoy/vox2_words/fuel.wav new file mode 100644 index 0000000..9b18b5d Binary files /dev/null and b/ttstoy/vox2_words/fuel.wav differ diff --git a/ttstoy/vox2_words/get.wav b/ttstoy/vox2_words/get.wav new file mode 100644 index 0000000..7198634 Binary files /dev/null and b/ttstoy/vox2_words/get.wav differ diff --git a/ttstoy/vox2_words/go.wav b/ttstoy/vox2_words/go.wav new file mode 100644 index 0000000..1e92e30 Binary files /dev/null and b/ttstoy/vox2_words/go.wav differ diff --git a/ttstoy/vox2_words/gordon.wav b/ttstoy/vox2_words/gordon.wav new file mode 100644 index 0000000..9d1efe3 Binary files /dev/null and b/ttstoy/vox2_words/gordon.wav differ diff --git a/ttstoy/vox2_words/granted.wav b/ttstoy/vox2_words/granted.wav new file mode 100644 index 0000000..f19abf8 Binary files /dev/null and b/ttstoy/vox2_words/granted.wav differ diff --git a/ttstoy/vox2_words/green.wav b/ttstoy/vox2_words/green.wav new file mode 100644 index 0000000..ee06f75 Binary files /dev/null and b/ttstoy/vox2_words/green.wav differ diff --git a/ttstoy/vox2_words/handling.wav b/ttstoy/vox2_words/handling.wav new file mode 100644 index 0000000..ee48d80 Binary files /dev/null and b/ttstoy/vox2_words/handling.wav differ diff --git a/ttstoy/vox2_words/hanger.wav b/ttstoy/vox2_words/hanger.wav new file mode 100644 index 0000000..b23bec0 Binary files /dev/null and b/ttstoy/vox2_words/hanger.wav differ diff --git a/ttstoy/vox2_words/have.wav b/ttstoy/vox2_words/have.wav new file mode 100644 index 0000000..b7112d4 Binary files /dev/null and b/ttstoy/vox2_words/have.wav differ diff --git a/ttstoy/vox2_words/hazard.wav b/ttstoy/vox2_words/hazard.wav new file mode 100644 index 0000000..47e0b2c Binary files /dev/null and b/ttstoy/vox2_words/hazard.wav differ diff --git a/ttstoy/vox2_words/health.wav b/ttstoy/vox2_words/health.wav new file mode 100644 index 0000000..6210f99 Binary files /dev/null and b/ttstoy/vox2_words/health.wav differ diff --git a/ttstoy/vox2_words/heat.wav b/ttstoy/vox2_words/heat.wav new file mode 100644 index 0000000..2cefa2b Binary files /dev/null and b/ttstoy/vox2_words/heat.wav differ diff --git a/ttstoy/vox2_words/helecopter.wav b/ttstoy/vox2_words/helecopter.wav new file mode 100644 index 0000000..eaebe49 Binary files /dev/null and b/ttstoy/vox2_words/helecopter.wav differ diff --git a/ttstoy/vox2_words/helium.wav b/ttstoy/vox2_words/helium.wav new file mode 100644 index 0000000..1b981fd Binary files /dev/null and b/ttstoy/vox2_words/helium.wav differ diff --git a/ttstoy/vox2_words/high.wav b/ttstoy/vox2_words/high.wav new file mode 100644 index 0000000..f348560 Binary files /dev/null and b/ttstoy/vox2_words/high.wav differ diff --git a/ttstoy/vox2_words/hostal.wav b/ttstoy/vox2_words/hostal.wav new file mode 100644 index 0000000..645bcf4 Binary files /dev/null and b/ttstoy/vox2_words/hostal.wav differ diff --git a/ttstoy/vox2_words/hostile.wav b/ttstoy/vox2_words/hostile.wav new file mode 100644 index 0000000..96dc1b0 Binary files /dev/null and b/ttstoy/vox2_words/hostile.wav differ diff --git a/ttstoy/vox2_words/hotel.wav b/ttstoy/vox2_words/hotel.wav new file mode 100644 index 0000000..cba6af2 Binary files /dev/null and b/ttstoy/vox2_words/hotel.wav differ diff --git a/ttstoy/vox2_words/hundred.wav b/ttstoy/vox2_words/hundred.wav new file mode 100644 index 0000000..da2a1a3 Binary files /dev/null and b/ttstoy/vox2_words/hundred.wav differ diff --git a/ttstoy/vox2_words/hydro.wav b/ttstoy/vox2_words/hydro.wav new file mode 100644 index 0000000..c0aca15 Binary files /dev/null and b/ttstoy/vox2_words/hydro.wav differ diff --git a/ttstoy/vox2_words/illegal.wav b/ttstoy/vox2_words/illegal.wav new file mode 100644 index 0000000..be6010f Binary files /dev/null and b/ttstoy/vox2_words/illegal.wav differ diff --git a/ttstoy/vox2_words/immediate.wav b/ttstoy/vox2_words/immediate.wav new file mode 100644 index 0000000..14ab922 Binary files /dev/null and b/ttstoy/vox2_words/immediate.wav differ diff --git a/ttstoy/vox2_words/immediately.wav b/ttstoy/vox2_words/immediately.wav new file mode 100644 index 0000000..7ab7e26 Binary files /dev/null and b/ttstoy/vox2_words/immediately.wav differ diff --git a/ttstoy/vox2_words/in.wav b/ttstoy/vox2_words/in.wav new file mode 100644 index 0000000..95a1dd5 Binary files /dev/null and b/ttstoy/vox2_words/in.wav differ diff --git a/ttstoy/vox2_words/india.wav b/ttstoy/vox2_words/india.wav new file mode 100644 index 0000000..3462c21 Binary files /dev/null and b/ttstoy/vox2_words/india.wav differ diff --git a/ttstoy/vox2_words/inoperative.wav b/ttstoy/vox2_words/inoperative.wav new file mode 100644 index 0000000..ae95555 Binary files /dev/null and b/ttstoy/vox2_words/inoperative.wav differ diff --git a/ttstoy/vox2_words/inside.wav b/ttstoy/vox2_words/inside.wav new file mode 100644 index 0000000..a48ad8e Binary files /dev/null and b/ttstoy/vox2_words/inside.wav differ diff --git a/ttstoy/vox2_words/inspection.wav b/ttstoy/vox2_words/inspection.wav new file mode 100644 index 0000000..d266090 Binary files /dev/null and b/ttstoy/vox2_words/inspection.wav differ diff --git a/ttstoy/vox2_words/is.wav b/ttstoy/vox2_words/is.wav new file mode 100644 index 0000000..2a2e3b8 Binary files /dev/null and b/ttstoy/vox2_words/is.wav differ diff --git a/ttstoy/vox2_words/kilo01.wav b/ttstoy/vox2_words/kilo01.wav new file mode 100644 index 0000000..ec93b02 Binary files /dev/null and b/ttstoy/vox2_words/kilo01.wav differ diff --git a/ttstoy/vox2_words/kilo02.wav b/ttstoy/vox2_words/kilo02.wav new file mode 100644 index 0000000..ecdfa67 Binary files /dev/null and b/ttstoy/vox2_words/kilo02.wav differ diff --git a/ttstoy/vox2_words/lambda.wav b/ttstoy/vox2_words/lambda.wav new file mode 100644 index 0000000..95f28c5 Binary files /dev/null and b/ttstoy/vox2_words/lambda.wav differ diff --git a/ttstoy/vox2_words/laser.wav b/ttstoy/vox2_words/laser.wav new file mode 100644 index 0000000..92f86cb Binary files /dev/null and b/ttstoy/vox2_words/laser.wav differ diff --git a/ttstoy/vox2_words/launch.wav b/ttstoy/vox2_words/launch.wav new file mode 100644 index 0000000..ac06ed7 Binary files /dev/null and b/ttstoy/vox2_words/launch.wav differ diff --git a/ttstoy/vox2_words/leak.wav b/ttstoy/vox2_words/leak.wav new file mode 100644 index 0000000..fd6e902 Binary files /dev/null and b/ttstoy/vox2_words/leak.wav differ diff --git a/ttstoy/vox2_words/level.wav b/ttstoy/vox2_words/level.wav new file mode 100644 index 0000000..87b81ad Binary files /dev/null and b/ttstoy/vox2_words/level.wav differ diff --git a/ttstoy/vox2_words/lima.wav b/ttstoy/vox2_words/lima.wav new file mode 100644 index 0000000..6372aef Binary files /dev/null and b/ttstoy/vox2_words/lima.wav differ diff --git a/ttstoy/vox2_words/lima_alt.wav b/ttstoy/vox2_words/lima_alt.wav new file mode 100644 index 0000000..0bb0182 Binary files /dev/null and b/ttstoy/vox2_words/lima_alt.wav differ diff --git a/ttstoy/vox2_words/liquid.wav b/ttstoy/vox2_words/liquid.wav new file mode 100644 index 0000000..bd2e66c Binary files /dev/null and b/ttstoy/vox2_words/liquid.wav differ diff --git a/ttstoy/vox2_words/lock.wav b/ttstoy/vox2_words/lock.wav new file mode 100644 index 0000000..134477a Binary files /dev/null and b/ttstoy/vox2_words/lock.wav differ diff --git a/ttstoy/vox2_words/locked.wav b/ttstoy/vox2_words/locked.wav new file mode 100644 index 0000000..02f3849 Binary files /dev/null and b/ttstoy/vox2_words/locked.wav differ diff --git a/ttstoy/vox2_words/lockout.wav b/ttstoy/vox2_words/lockout.wav new file mode 100644 index 0000000..3717538 Binary files /dev/null and b/ttstoy/vox2_words/lockout.wav differ diff --git a/ttstoy/vox2_words/lower.wav b/ttstoy/vox2_words/lower.wav new file mode 100644 index 0000000..ceea65e Binary files /dev/null and b/ttstoy/vox2_words/lower.wav differ diff --git a/ttstoy/vox2_words/main.wav b/ttstoy/vox2_words/main.wav new file mode 100644 index 0000000..2a367f3 Binary files /dev/null and b/ttstoy/vox2_words/main.wav differ diff --git a/ttstoy/vox2_words/maintenance.wav b/ttstoy/vox2_words/maintenance.wav new file mode 100644 index 0000000..7a870ae Binary files /dev/null and b/ttstoy/vox2_words/maintenance.wav differ diff --git a/ttstoy/vox2_words/malfunction.wav b/ttstoy/vox2_words/malfunction.wav new file mode 100644 index 0000000..1a852b5 Binary files /dev/null and b/ttstoy/vox2_words/malfunction.wav differ diff --git a/ttstoy/vox2_words/materials.wav b/ttstoy/vox2_words/materials.wav new file mode 100644 index 0000000..eef4297 Binary files /dev/null and b/ttstoy/vox2_words/materials.wav differ diff --git a/ttstoy/vox2_words/may.wav b/ttstoy/vox2_words/may.wav new file mode 100644 index 0000000..0c6a8f7 Binary files /dev/null and b/ttstoy/vox2_words/may.wav differ diff --git a/ttstoy/vox2_words/medical.wav b/ttstoy/vox2_words/medical.wav new file mode 100644 index 0000000..bb35eb9 Binary files /dev/null and b/ttstoy/vox2_words/medical.wav differ diff --git a/ttstoy/vox2_words/men.wav b/ttstoy/vox2_words/men.wav new file mode 100644 index 0000000..b7f4037 Binary files /dev/null and b/ttstoy/vox2_words/men.wav differ diff --git a/ttstoy/vox2_words/mesa.wav b/ttstoy/vox2_words/mesa.wav new file mode 100644 index 0000000..f565db9 Binary files /dev/null and b/ttstoy/vox2_words/mesa.wav differ diff --git a/ttstoy/vox2_words/message.wav b/ttstoy/vox2_words/message.wav new file mode 100644 index 0000000..5472466 Binary files /dev/null and b/ttstoy/vox2_words/message.wav differ diff --git a/ttstoy/vox2_words/mic_mike.wav b/ttstoy/vox2_words/mic_mike.wav new file mode 100644 index 0000000..529579a Binary files /dev/null and b/ttstoy/vox2_words/mic_mike.wav differ diff --git a/ttstoy/vox2_words/mike.wav b/ttstoy/vox2_words/mike.wav new file mode 100644 index 0000000..529579a Binary files /dev/null and b/ttstoy/vox2_words/mike.wav differ diff --git a/ttstoy/vox2_words/military.wav b/ttstoy/vox2_words/military.wav new file mode 100644 index 0000000..be17c5f Binary files /dev/null and b/ttstoy/vox2_words/military.wav differ diff --git a/ttstoy/vox2_words/motorpool.wav b/ttstoy/vox2_words/motorpool.wav new file mode 100644 index 0000000..a0433de Binary files /dev/null and b/ttstoy/vox2_words/motorpool.wav differ diff --git a/ttstoy/vox2_words/move.wav b/ttstoy/vox2_words/move.wav new file mode 100644 index 0000000..5fe4527 Binary files /dev/null and b/ttstoy/vox2_words/move.wav differ diff --git a/ttstoy/vox2_words/must.wav b/ttstoy/vox2_words/must.wav new file mode 100644 index 0000000..93f13bf Binary files /dev/null and b/ttstoy/vox2_words/must.wav differ diff --git a/ttstoy/vox2_words/nearest.wav b/ttstoy/vox2_words/nearest.wav new file mode 100644 index 0000000..7d56db7 Binary files /dev/null and b/ttstoy/vox2_words/nearest.wav differ diff --git a/ttstoy/vox2_words/nine.wav b/ttstoy/vox2_words/nine.wav new file mode 100644 index 0000000..16b5f89 Binary files /dev/null and b/ttstoy/vox2_words/nine.wav differ diff --git a/ttstoy/vox2_words/nineteen.wav b/ttstoy/vox2_words/nineteen.wav new file mode 100644 index 0000000..8f8ea2f Binary files /dev/null and b/ttstoy/vox2_words/nineteen.wav differ diff --git a/ttstoy/vox2_words/ninety.wav b/ttstoy/vox2_words/ninety.wav new file mode 100644 index 0000000..a816633 Binary files /dev/null and b/ttstoy/vox2_words/ninety.wav differ diff --git a/ttstoy/vox2_words/no.wav b/ttstoy/vox2_words/no.wav new file mode 100644 index 0000000..4eb03a3 Binary files /dev/null and b/ttstoy/vox2_words/no.wav differ diff --git a/ttstoy/vox2_words/noe.wav b/ttstoy/vox2_words/noe.wav new file mode 100644 index 0000000..6810611 Binary files /dev/null and b/ttstoy/vox2_words/noe.wav differ diff --git a/ttstoy/vox2_words/not.wav b/ttstoy/vox2_words/not.wav new file mode 100644 index 0000000..13aecf9 Binary files /dev/null and b/ttstoy/vox2_words/not.wav differ diff --git a/ttstoy/vox2_words/now.wav b/ttstoy/vox2_words/now.wav new file mode 100644 index 0000000..72dc06f Binary files /dev/null and b/ttstoy/vox2_words/now.wav differ diff --git a/ttstoy/vox2_words/objective.wav b/ttstoy/vox2_words/objective.wav new file mode 100644 index 0000000..0dd49df Binary files /dev/null and b/ttstoy/vox2_words/objective.wav differ diff --git a/ttstoy/vox2_words/of.wav b/ttstoy/vox2_words/of.wav new file mode 100644 index 0000000..81ef3c3 Binary files /dev/null and b/ttstoy/vox2_words/of.wav differ diff --git a/ttstoy/vox2_words/on.wav b/ttstoy/vox2_words/on.wav new file mode 100644 index 0000000..9654bc1 Binary files /dev/null and b/ttstoy/vox2_words/on.wav differ diff --git a/ttstoy/vox2_words/one.wav b/ttstoy/vox2_words/one.wav new file mode 100644 index 0000000..a3a25fd Binary files /dev/null and b/ttstoy/vox2_words/one.wav differ diff --git a/ttstoy/vox2_words/open.wav b/ttstoy/vox2_words/open.wav new file mode 100644 index 0000000..22c7eda Binary files /dev/null and b/ttstoy/vox2_words/open.wav differ diff --git a/ttstoy/vox2_words/operating.wav b/ttstoy/vox2_words/operating.wav new file mode 100644 index 0000000..eae0de5 Binary files /dev/null and b/ttstoy/vox2_words/operating.wav differ diff --git a/ttstoy/vox2_words/option.wav b/ttstoy/vox2_words/option.wav new file mode 100644 index 0000000..1b67212 Binary files /dev/null and b/ttstoy/vox2_words/option.wav differ diff --git a/ttstoy/vox2_words/override.wav b/ttstoy/vox2_words/override.wav new file mode 100644 index 0000000..5a43ba9 Binary files /dev/null and b/ttstoy/vox2_words/override.wav differ diff --git a/ttstoy/vox2_words/percent.wav b/ttstoy/vox2_words/percent.wav new file mode 100644 index 0000000..915312f Binary files /dev/null and b/ttstoy/vox2_words/percent.wav differ diff --git a/ttstoy/vox2_words/perimeter.wav b/ttstoy/vox2_words/perimeter.wav new file mode 100644 index 0000000..b215455 Binary files /dev/null and b/ttstoy/vox2_words/perimeter.wav differ diff --git a/ttstoy/vox2_words/period.wav b/ttstoy/vox2_words/period.wav new file mode 100644 index 0000000..7eeb1eb Binary files /dev/null and b/ttstoy/vox2_words/period.wav differ diff --git a/ttstoy/vox2_words/permitted.wav b/ttstoy/vox2_words/permitted.wav new file mode 100644 index 0000000..72029d5 Binary files /dev/null and b/ttstoy/vox2_words/permitted.wav differ diff --git a/ttstoy/vox2_words/perpulsion.wav b/ttstoy/vox2_words/perpulsion.wav new file mode 100644 index 0000000..6c1c2b7 Binary files /dev/null and b/ttstoy/vox2_words/perpulsion.wav differ diff --git a/ttstoy/vox2_words/personnel.wav b/ttstoy/vox2_words/personnel.wav new file mode 100644 index 0000000..4d6af17 Binary files /dev/null and b/ttstoy/vox2_words/personnel.wav differ diff --git a/ttstoy/vox2_words/plant.wav b/ttstoy/vox2_words/plant.wav new file mode 100644 index 0000000..9112742 Binary files /dev/null and b/ttstoy/vox2_words/plant.wav differ diff --git a/ttstoy/vox2_words/please.wav b/ttstoy/vox2_words/please.wav new file mode 100644 index 0000000..f50043c Binary files /dev/null and b/ttstoy/vox2_words/please.wav differ diff --git a/ttstoy/vox2_words/portal.wav b/ttstoy/vox2_words/portal.wav new file mode 100644 index 0000000..62591e9 Binary files /dev/null and b/ttstoy/vox2_words/portal.wav differ diff --git a/ttstoy/vox2_words/power.wav b/ttstoy/vox2_words/power.wav new file mode 100644 index 0000000..2321e0c Binary files /dev/null and b/ttstoy/vox2_words/power.wav differ diff --git a/ttstoy/vox2_words/primary.wav b/ttstoy/vox2_words/primary.wav new file mode 100644 index 0000000..9c1e107 Binary files /dev/null and b/ttstoy/vox2_words/primary.wav differ diff --git a/ttstoy/vox2_words/prosecute.wav b/ttstoy/vox2_words/prosecute.wav new file mode 100644 index 0000000..62dd486 Binary files /dev/null and b/ttstoy/vox2_words/prosecute.wav differ diff --git a/ttstoy/vox2_words/questioning.wav b/ttstoy/vox2_words/questioning.wav new file mode 100644 index 0000000..0343459 Binary files /dev/null and b/ttstoy/vox2_words/questioning.wav differ diff --git a/ttstoy/vox2_words/radiation.wav b/ttstoy/vox2_words/radiation.wav new file mode 100644 index 0000000..1c2715a Binary files /dev/null and b/ttstoy/vox2_words/radiation.wav differ diff --git a/ttstoy/vox2_words/radioactive.wav b/ttstoy/vox2_words/radioactive.wav new file mode 100644 index 0000000..3eeb013 Binary files /dev/null and b/ttstoy/vox2_words/radioactive.wav differ diff --git a/ttstoy/vox2_words/reach.wav b/ttstoy/vox2_words/reach.wav new file mode 100644 index 0000000..a09b67a Binary files /dev/null and b/ttstoy/vox2_words/reach.wav differ diff --git a/ttstoy/vox2_words/reactor.wav b/ttstoy/vox2_words/reactor.wav new file mode 100644 index 0000000..8ba51a9 Binary files /dev/null and b/ttstoy/vox2_words/reactor.wav differ diff --git a/ttstoy/vox2_words/relay.wav b/ttstoy/vox2_words/relay.wav new file mode 100644 index 0000000..696a9c2 Binary files /dev/null and b/ttstoy/vox2_words/relay.wav differ diff --git a/ttstoy/vox2_words/released.wav b/ttstoy/vox2_words/released.wav new file mode 100644 index 0000000..0943260 Binary files /dev/null and b/ttstoy/vox2_words/released.wav differ diff --git a/ttstoy/vox2_words/remaining.wav b/ttstoy/vox2_words/remaining.wav new file mode 100644 index 0000000..51c562a Binary files /dev/null and b/ttstoy/vox2_words/remaining.wav differ diff --git a/ttstoy/vox2_words/renegade.wav b/ttstoy/vox2_words/renegade.wav new file mode 100644 index 0000000..d1e975e Binary files /dev/null and b/ttstoy/vox2_words/renegade.wav differ diff --git a/ttstoy/vox2_words/repair.wav b/ttstoy/vox2_words/repair.wav new file mode 100644 index 0000000..d861af8 Binary files /dev/null and b/ttstoy/vox2_words/repair.wav differ diff --git a/ttstoy/vox2_words/report.wav b/ttstoy/vox2_words/report.wav new file mode 100644 index 0000000..feaa5ca Binary files /dev/null and b/ttstoy/vox2_words/report.wav differ diff --git a/ttstoy/vox2_words/reports.wav b/ttstoy/vox2_words/reports.wav new file mode 100644 index 0000000..e80234e Binary files /dev/null and b/ttstoy/vox2_words/reports.wav differ diff --git a/ttstoy/vox2_words/required.wav b/ttstoy/vox2_words/required.wav new file mode 100644 index 0000000..e9d4b2f Binary files /dev/null and b/ttstoy/vox2_words/required.wav differ diff --git a/ttstoy/vox2_words/research.wav b/ttstoy/vox2_words/research.wav new file mode 100644 index 0000000..75c87f2 Binary files /dev/null and b/ttstoy/vox2_words/research.wav differ diff --git a/ttstoy/vox2_words/resistance.wav b/ttstoy/vox2_words/resistance.wav new file mode 100644 index 0000000..192185c Binary files /dev/null and b/ttstoy/vox2_words/resistance.wav differ diff --git a/ttstoy/vox2_words/rocket.wav b/ttstoy/vox2_words/rocket.wav new file mode 100644 index 0000000..5352728 Binary files /dev/null and b/ttstoy/vox2_words/rocket.wav differ diff --git a/ttstoy/vox2_words/safety.wav b/ttstoy/vox2_words/safety.wav new file mode 100644 index 0000000..7658e44 Binary files /dev/null and b/ttstoy/vox2_words/safety.wav differ diff --git a/ttstoy/vox2_words/satellite.wav b/ttstoy/vox2_words/satellite.wav new file mode 100644 index 0000000..84d6f3c Binary files /dev/null and b/ttstoy/vox2_words/satellite.wav differ diff --git a/ttstoy/vox2_words/science.wav b/ttstoy/vox2_words/science.wav new file mode 100644 index 0000000..18c5bc5 Binary files /dev/null and b/ttstoy/vox2_words/science.wav differ diff --git a/ttstoy/vox2_words/search.wav b/ttstoy/vox2_words/search.wav new file mode 100644 index 0000000..f233803 Binary files /dev/null and b/ttstoy/vox2_words/search.wav differ diff --git a/ttstoy/vox2_words/second.wav b/ttstoy/vox2_words/second.wav new file mode 100644 index 0000000..13bbe52 Binary files /dev/null and b/ttstoy/vox2_words/second.wav differ diff --git a/ttstoy/vox2_words/secondary.wav b/ttstoy/vox2_words/secondary.wav new file mode 100644 index 0000000..6aaf943 Binary files /dev/null and b/ttstoy/vox2_words/secondary.wav differ diff --git a/ttstoy/vox2_words/seconds.wav b/ttstoy/vox2_words/seconds.wav new file mode 100644 index 0000000..a6639ac Binary files /dev/null and b/ttstoy/vox2_words/seconds.wav differ diff --git a/ttstoy/vox2_words/sector.wav b/ttstoy/vox2_words/sector.wav new file mode 100644 index 0000000..cbe88b5 Binary files /dev/null and b/ttstoy/vox2_words/sector.wav differ diff --git a/ttstoy/vox2_words/secure.wav b/ttstoy/vox2_words/secure.wav new file mode 100644 index 0000000..45d8e1f Binary files /dev/null and b/ttstoy/vox2_words/secure.wav differ diff --git a/ttstoy/vox2_words/secured.wav b/ttstoy/vox2_words/secured.wav new file mode 100644 index 0000000..81b2d87 Binary files /dev/null and b/ttstoy/vox2_words/secured.wav differ diff --git a/ttstoy/vox2_words/security.wav b/ttstoy/vox2_words/security.wav new file mode 100644 index 0000000..c7d8e89 Binary files /dev/null and b/ttstoy/vox2_words/security.wav differ diff --git a/ttstoy/vox2_words/service.wav b/ttstoy/vox2_words/service.wav new file mode 100644 index 0000000..acfeec4 Binary files /dev/null and b/ttstoy/vox2_words/service.wav differ diff --git a/ttstoy/vox2_words/seven.wav b/ttstoy/vox2_words/seven.wav new file mode 100644 index 0000000..f8755d2 Binary files /dev/null and b/ttstoy/vox2_words/seven.wav differ diff --git a/ttstoy/vox2_words/seventeen.wav b/ttstoy/vox2_words/seventeen.wav new file mode 100644 index 0000000..0d9e6e4 Binary files /dev/null and b/ttstoy/vox2_words/seventeen.wav differ diff --git a/ttstoy/vox2_words/seventy.wav b/ttstoy/vox2_words/seventy.wav new file mode 100644 index 0000000..662a5bb Binary files /dev/null and b/ttstoy/vox2_words/seventy.wav differ diff --git a/ttstoy/vox2_words/severe.wav b/ttstoy/vox2_words/severe.wav new file mode 100644 index 0000000..6838a03 Binary files /dev/null and b/ttstoy/vox2_words/severe.wav differ diff --git a/ttstoy/vox2_words/sheild.wav b/ttstoy/vox2_words/sheild.wav new file mode 100644 index 0000000..a248a7a Binary files /dev/null and b/ttstoy/vox2_words/sheild.wav differ diff --git a/ttstoy/vox2_words/shoot.wav b/ttstoy/vox2_words/shoot.wav new file mode 100644 index 0000000..5e8d7c8 Binary files /dev/null and b/ttstoy/vox2_words/shoot.wav differ diff --git a/ttstoy/vox2_words/sierra.wav b/ttstoy/vox2_words/sierra.wav new file mode 100644 index 0000000..a20110b Binary files /dev/null and b/ttstoy/vox2_words/sierra.wav differ diff --git a/ttstoy/vox2_words/sight.wav b/ttstoy/vox2_words/sight.wav new file mode 100644 index 0000000..4d7b1a9 Binary files /dev/null and b/ttstoy/vox2_words/sight.wav differ diff --git a/ttstoy/vox2_words/silo.wav b/ttstoy/vox2_words/silo.wav new file mode 100644 index 0000000..fa7f41f Binary files /dev/null and b/ttstoy/vox2_words/silo.wav differ diff --git a/ttstoy/vox2_words/six.wav b/ttstoy/vox2_words/six.wav new file mode 100644 index 0000000..d4fb8af Binary files /dev/null and b/ttstoy/vox2_words/six.wav differ diff --git a/ttstoy/vox2_words/sixteen.wav b/ttstoy/vox2_words/sixteen.wav new file mode 100644 index 0000000..ae0fd18 Binary files /dev/null and b/ttstoy/vox2_words/sixteen.wav differ diff --git a/ttstoy/vox2_words/sixty.wav b/ttstoy/vox2_words/sixty.wav new file mode 100644 index 0000000..6ae8362 Binary files /dev/null and b/ttstoy/vox2_words/sixty.wav differ diff --git a/ttstoy/vox2_words/sorry.wav b/ttstoy/vox2_words/sorry.wav new file mode 100644 index 0000000..22a6f73 Binary files /dev/null and b/ttstoy/vox2_words/sorry.wav differ diff --git a/ttstoy/vox2_words/sqaud.wav b/ttstoy/vox2_words/sqaud.wav new file mode 100644 index 0000000..18f27d7 Binary files /dev/null and b/ttstoy/vox2_words/sqaud.wav differ diff --git a/ttstoy/vox2_words/status.wav b/ttstoy/vox2_words/status.wav new file mode 100644 index 0000000..8cfb3e2 Binary files /dev/null and b/ttstoy/vox2_words/status.wav differ diff --git a/ttstoy/vox2_words/sterilization.wav b/ttstoy/vox2_words/sterilization.wav new file mode 100644 index 0000000..34057f4 Binary files /dev/null and b/ttstoy/vox2_words/sterilization.wav differ diff --git a/ttstoy/vox2_words/storage.wav b/ttstoy/vox2_words/storage.wav new file mode 100644 index 0000000..6e96259 Binary files /dev/null and b/ttstoy/vox2_words/storage.wav differ diff --git a/ttstoy/vox2_words/supercooled.wav b/ttstoy/vox2_words/supercooled.wav new file mode 100644 index 0000000..72535f3 Binary files /dev/null and b/ttstoy/vox2_words/supercooled.wav differ diff --git a/ttstoy/vox2_words/surrender.wav b/ttstoy/vox2_words/surrender.wav new file mode 100644 index 0000000..acdf3de Binary files /dev/null and b/ttstoy/vox2_words/surrender.wav differ diff --git a/ttstoy/vox2_words/system.wav b/ttstoy/vox2_words/system.wav new file mode 100644 index 0000000..13a7cd4 Binary files /dev/null and b/ttstoy/vox2_words/system.wav differ diff --git a/ttstoy/vox2_words/systems.wav b/ttstoy/vox2_words/systems.wav new file mode 100644 index 0000000..41871e2 Binary files /dev/null and b/ttstoy/vox2_words/systems.wav differ diff --git a/ttstoy/vox2_words/target.wav b/ttstoy/vox2_words/target.wav new file mode 100644 index 0000000..7af8dce Binary files /dev/null and b/ttstoy/vox2_words/target.wav differ diff --git a/ttstoy/vox2_words/team.wav b/ttstoy/vox2_words/team.wav new file mode 100644 index 0000000..dbbe238 Binary files /dev/null and b/ttstoy/vox2_words/team.wav differ diff --git a/ttstoy/vox2_words/ten.wav b/ttstoy/vox2_words/ten.wav new file mode 100644 index 0000000..ee580dd Binary files /dev/null and b/ttstoy/vox2_words/ten.wav differ diff --git a/ttstoy/vox2_words/terminated.wav b/ttstoy/vox2_words/terminated.wav new file mode 100644 index 0000000..d237ded Binary files /dev/null and b/ttstoy/vox2_words/terminated.wav differ diff --git a/ttstoy/vox2_words/test.wav b/ttstoy/vox2_words/test.wav new file mode 100644 index 0000000..8874af9 Binary files /dev/null and b/ttstoy/vox2_words/test.wav differ diff --git a/ttstoy/vox2_words/the.wav b/ttstoy/vox2_words/the.wav new file mode 100644 index 0000000..e18c975 Binary files /dev/null and b/ttstoy/vox2_words/the.wav differ diff --git a/ttstoy/vox2_words/thirtteen.wav b/ttstoy/vox2_words/thirtteen.wav new file mode 100644 index 0000000..2ca7416 Binary files /dev/null and b/ttstoy/vox2_words/thirtteen.wav differ diff --git a/ttstoy/vox2_words/thirty.wav b/ttstoy/vox2_words/thirty.wav new file mode 100644 index 0000000..f161b8b Binary files /dev/null and b/ttstoy/vox2_words/thirty.wav differ diff --git a/ttstoy/vox2_words/this.wav b/ttstoy/vox2_words/this.wav new file mode 100644 index 0000000..8c4fdbb Binary files /dev/null and b/ttstoy/vox2_words/this.wav differ diff --git a/ttstoy/vox2_words/three.wav b/ttstoy/vox2_words/three.wav new file mode 100644 index 0000000..1755d5a Binary files /dev/null and b/ttstoy/vox2_words/three.wav differ diff --git a/ttstoy/vox2_words/time.wav b/ttstoy/vox2_words/time.wav new file mode 100644 index 0000000..23d312e Binary files /dev/null and b/ttstoy/vox2_words/time.wav differ diff --git a/ttstoy/vox2_words/to.wav b/ttstoy/vox2_words/to.wav new file mode 100644 index 0000000..79644f5 Binary files /dev/null and b/ttstoy/vox2_words/to.wav differ diff --git a/ttstoy/vox2_words/topside.wav b/ttstoy/vox2_words/topside.wav new file mode 100644 index 0000000..833c7f9 Binary files /dev/null and b/ttstoy/vox2_words/topside.wav differ diff --git a/ttstoy/vox2_words/track.wav b/ttstoy/vox2_words/track.wav new file mode 100644 index 0000000..fb2238a Binary files /dev/null and b/ttstoy/vox2_words/track.wav differ diff --git a/ttstoy/vox2_words/train.wav b/ttstoy/vox2_words/train.wav new file mode 100644 index 0000000..bcee4cb Binary files /dev/null and b/ttstoy/vox2_words/train.wav differ diff --git a/ttstoy/vox2_words/turret.wav b/ttstoy/vox2_words/turret.wav new file mode 100644 index 0000000..521d09b Binary files /dev/null and b/ttstoy/vox2_words/turret.wav differ diff --git a/ttstoy/vox2_words/twelve.wav b/ttstoy/vox2_words/twelve.wav new file mode 100644 index 0000000..c0b38ce Binary files /dev/null and b/ttstoy/vox2_words/twelve.wav differ diff --git a/ttstoy/vox2_words/twenty.wav b/ttstoy/vox2_words/twenty.wav new file mode 100644 index 0000000..8891085 Binary files /dev/null and b/ttstoy/vox2_words/twenty.wav differ diff --git a/ttstoy/vox2_words/two.wav b/ttstoy/vox2_words/two.wav new file mode 100644 index 0000000..869b7fc Binary files /dev/null and b/ttstoy/vox2_words/two.wav differ diff --git a/ttstoy/vox2_words/unauthorized.wav b/ttstoy/vox2_words/unauthorized.wav new file mode 100644 index 0000000..9949452 Binary files /dev/null and b/ttstoy/vox2_words/unauthorized.wav differ diff --git a/ttstoy/vox2_words/under.wav b/ttstoy/vox2_words/under.wav new file mode 100644 index 0000000..bb4c939 Binary files /dev/null and b/ttstoy/vox2_words/under.wav differ diff --git a/ttstoy/vox2_words/units.wav b/ttstoy/vox2_words/units.wav new file mode 100644 index 0000000..4641f5c Binary files /dev/null and b/ttstoy/vox2_words/units.wav differ diff --git a/ttstoy/vox2_words/until.wav b/ttstoy/vox2_words/until.wav new file mode 100644 index 0000000..444d52f Binary files /dev/null and b/ttstoy/vox2_words/until.wav differ diff --git a/ttstoy/vox2_words/up.wav b/ttstoy/vox2_words/up.wav new file mode 100644 index 0000000..13cf6e0 Binary files /dev/null and b/ttstoy/vox2_words/up.wav differ diff --git a/ttstoy/vox2_words/uranium.wav b/ttstoy/vox2_words/uranium.wav new file mode 100644 index 0000000..5b0b77c Binary files /dev/null and b/ttstoy/vox2_words/uranium.wav differ diff --git a/ttstoy/vox2_words/use.wav b/ttstoy/vox2_words/use.wav new file mode 100644 index 0000000..9d8981d Binary files /dev/null and b/ttstoy/vox2_words/use.wav differ diff --git a/ttstoy/vox2_words/violation.wav b/ttstoy/vox2_words/violation.wav new file mode 100644 index 0000000..33a509e Binary files /dev/null and b/ttstoy/vox2_words/violation.wav differ diff --git a/ttstoy/vox2_words/voltage.wav b/ttstoy/vox2_words/voltage.wav new file mode 100644 index 0000000..4a75a64 Binary files /dev/null and b/ttstoy/vox2_words/voltage.wav differ diff --git a/ttstoy/vox2_words/wanted.wav b/ttstoy/vox2_words/wanted.wav new file mode 100644 index 0000000..b9ccacc Binary files /dev/null and b/ttstoy/vox2_words/wanted.wav differ diff --git a/ttstoy/vox2_words/warning.wav b/ttstoy/vox2_words/warning.wav new file mode 100644 index 0000000..838945a Binary files /dev/null and b/ttstoy/vox2_words/warning.wav differ diff --git a/ttstoy/vox2_words/we.wav b/ttstoy/vox2_words/we.wav new file mode 100644 index 0000000..f49a305 Binary files /dev/null and b/ttstoy/vox2_words/we.wav differ diff --git a/ttstoy/vox2_words/weapon.wav b/ttstoy/vox2_words/weapon.wav new file mode 100644 index 0000000..1195fe0 Binary files /dev/null and b/ttstoy/vox2_words/weapon.wav differ diff --git a/ttstoy/vox2_words/will.wav b/ttstoy/vox2_words/will.wav new file mode 100644 index 0000000..f81b1ca Binary files /dev/null and b/ttstoy/vox2_words/will.wav differ diff --git a/ttstoy/vox2_words/with.wav b/ttstoy/vox2_words/with.wav new file mode 100644 index 0000000..3489792 Binary files /dev/null and b/ttstoy/vox2_words/with.wav differ diff --git a/ttstoy/vox2_words/yellow.wav b/ttstoy/vox2_words/yellow.wav new file mode 100644 index 0000000..7a439f2 Binary files /dev/null and b/ttstoy/vox2_words/yellow.wav differ diff --git a/ttstoy/vox2_words/you.wav b/ttstoy/vox2_words/you.wav new file mode 100644 index 0000000..4971d6f Binary files /dev/null and b/ttstoy/vox2_words/you.wav differ diff --git a/ttstoy/vox2_words/your.wav b/ttstoy/vox2_words/your.wav new file mode 100644 index 0000000..3982fdd Binary files /dev/null and b/ttstoy/vox2_words/your.wav differ diff --git a/ttstoy/vox2_words/zero.wav b/ttstoy/vox2_words/zero.wav new file mode 100644 index 0000000..c02cedb Binary files /dev/null and b/ttstoy/vox2_words/zero.wav differ diff --git a/ttstoy/vox2_words/zone.wav b/ttstoy/vox2_words/zone.wav new file mode 100644 index 0000000..70d2b33 Binary files /dev/null and b/ttstoy/vox2_words/zone.wav differ diff --git a/ttstoy/vox_engine.py b/ttstoy/vox_engine.py new file mode 100644 index 0000000..f36c653 --- /dev/null +++ b/ttstoy/vox_engine.py @@ -0,0 +1,104 @@ +""" +Bundled VOX engine for ttstoy. +Based on VOXGen by wphillips (https://github.com/wphillips/VOXGen) +Black Mesa / Half-Life VOX announcer text-to-speech via word-level WAV concatenation. +""" +import os +import wave +import struct +from pathlib import Path +from typing import List, Optional + +_BASE_DIR = Path(__file__).resolve().parent + +VOX_PACKS = { + "vox": _BASE_DIR / "vox_words", + "vox2": _BASE_DIR / "vox2_words", +} + + +def get_available_packs() -> List[str]: + """Return list of installed VOX packs.""" + return [name for name, p in VOX_PACKS.items() if p.exists()] + + +def get_available_words(pack: str = "vox") -> List[str]: + """Return sorted list of available words for a VOX pack.""" + pack_dir = VOX_PACKS.get(pack) + if not pack_dir or not pack_dir.exists(): + return [] + return sorted( + p.stem for p in pack_dir.glob("*.wav") + if not p.stem.startswith("00_") and not p.stem.startswith("_") + ) + + +def generate_vox(text: str, output_path: str, pack: str = "vox") -> List[str]: + """ + Generate VOX audio from text by concatenating word WAV files. + + Args: + text: Input sentence (words separated by spaces). + output_path: Path to write the output WAV/MP3 file. + pack: Which VOX pack to use ("vox" or "vox2"). + + Returns: + List of words that were actually found and used. + + Raises: + RuntimeError: If no words matched or pack not found. + """ + pack_dir = VOX_PACKS.get(pack) + if not pack_dir or not pack_dir.exists(): + raise RuntimeError(f"VOX pack '{pack}' not found at {pack_dir}") + + words = text.lower().split() + accepted = [] + splice = bytes() + + sample_rate = 11025 + sample_width = 1 + channels = 1 + + for word in words: + wav_path = pack_dir / f"{word}.wav" + if not wav_path.exists(): + continue + + with wave.open(str(wav_path), 'rb') as snd: + sample_rate = snd.getframerate() + sample_width = snd.getsampwidth() + channels = snd.getnchannels() + splice += snd.readframes(snd.getnframes()) + accepted.append(word) + + if not accepted: + raise RuntimeError( + f"No matching VOX words found. Available words include: " + f"{', '.join(get_available_words(pack)[:20])}..." + ) + + # Write intermediate WAV + wav_out = output_path.rsplit('.', 1)[0] + '_tmp.wav' + with wave.open(wav_out, 'wb') as out: + out.setnchannels(channels) + out.setsampwidth(sample_width) + out.setframerate(sample_rate) + out.writeframes(splice) + + # Convert to MP3 using ffmpeg for consistency with the rest of ttstoy + import subprocess + cmd = [ + "ffmpeg", "-y", "-i", wav_out, + "-ac", "1", "-ar", "32000", "-b:a", "128k", + output_path, + ] + subprocess.run(cmd, check=True, capture_output=True, text=True) + + # Clean up temp WAV + try: + os.remove(wav_out) + except OSError: + pass + + return accepted diff --git a/ttstoy/vox_words/_comma.wav b/ttstoy/vox_words/_comma.wav new file mode 100755 index 0000000..31a47dc Binary files /dev/null and b/ttstoy/vox_words/_comma.wav differ diff --git a/ttstoy/vox_words/a.wav b/ttstoy/vox_words/a.wav new file mode 100755 index 0000000..c6981d2 Binary files /dev/null and b/ttstoy/vox_words/a.wav differ diff --git a/ttstoy/vox_words/accelerating.wav b/ttstoy/vox_words/accelerating.wav new file mode 100755 index 0000000..e86b18a Binary files /dev/null and b/ttstoy/vox_words/accelerating.wav differ diff --git a/ttstoy/vox_words/accelerator.wav b/ttstoy/vox_words/accelerator.wav new file mode 100755 index 0000000..be097b6 Binary files /dev/null and b/ttstoy/vox_words/accelerator.wav differ diff --git a/ttstoy/vox_words/accepted.wav b/ttstoy/vox_words/accepted.wav new file mode 100755 index 0000000..5916084 Binary files /dev/null and b/ttstoy/vox_words/accepted.wav differ diff --git a/ttstoy/vox_words/access.wav b/ttstoy/vox_words/access.wav new file mode 100755 index 0000000..25fd098 Binary files /dev/null and b/ttstoy/vox_words/access.wav differ diff --git a/ttstoy/vox_words/acknowledge.wav b/ttstoy/vox_words/acknowledge.wav new file mode 100755 index 0000000..14946cc Binary files /dev/null and b/ttstoy/vox_words/acknowledge.wav differ diff --git a/ttstoy/vox_words/acknowledged.wav b/ttstoy/vox_words/acknowledged.wav new file mode 100755 index 0000000..232d9c3 Binary files /dev/null and b/ttstoy/vox_words/acknowledged.wav differ diff --git a/ttstoy/vox_words/acquired.wav b/ttstoy/vox_words/acquired.wav new file mode 100755 index 0000000..7eecdf9 Binary files /dev/null and b/ttstoy/vox_words/acquired.wav differ diff --git a/ttstoy/vox_words/acquisition.wav b/ttstoy/vox_words/acquisition.wav new file mode 100755 index 0000000..11be7dc Binary files /dev/null and b/ttstoy/vox_words/acquisition.wav differ diff --git a/ttstoy/vox_words/across.wav b/ttstoy/vox_words/across.wav new file mode 100755 index 0000000..1591e89 Binary files /dev/null and b/ttstoy/vox_words/across.wav differ diff --git a/ttstoy/vox_words/activate.wav b/ttstoy/vox_words/activate.wav new file mode 100755 index 0000000..32a45cb Binary files /dev/null and b/ttstoy/vox_words/activate.wav differ diff --git a/ttstoy/vox_words/activated.wav b/ttstoy/vox_words/activated.wav new file mode 100755 index 0000000..154e038 Binary files /dev/null and b/ttstoy/vox_words/activated.wav differ diff --git a/ttstoy/vox_words/activity.wav b/ttstoy/vox_words/activity.wav new file mode 100755 index 0000000..5920e6d Binary files /dev/null and b/ttstoy/vox_words/activity.wav differ diff --git a/ttstoy/vox_words/adios.wav b/ttstoy/vox_words/adios.wav new file mode 100755 index 0000000..86e268c Binary files /dev/null and b/ttstoy/vox_words/adios.wav differ diff --git a/ttstoy/vox_words/administration.wav b/ttstoy/vox_words/administration.wav new file mode 100755 index 0000000..d85d67e Binary files /dev/null and b/ttstoy/vox_words/administration.wav differ diff --git a/ttstoy/vox_words/advanced.wav b/ttstoy/vox_words/advanced.wav new file mode 100755 index 0000000..987cd05 Binary files /dev/null and b/ttstoy/vox_words/advanced.wav differ diff --git a/ttstoy/vox_words/after.wav b/ttstoy/vox_words/after.wav new file mode 100755 index 0000000..f63abdf Binary files /dev/null and b/ttstoy/vox_words/after.wav differ diff --git a/ttstoy/vox_words/agent.wav b/ttstoy/vox_words/agent.wav new file mode 100755 index 0000000..5949329 Binary files /dev/null and b/ttstoy/vox_words/agent.wav differ diff --git a/ttstoy/vox_words/alarm.wav b/ttstoy/vox_words/alarm.wav new file mode 100755 index 0000000..296ca77 Binary files /dev/null and b/ttstoy/vox_words/alarm.wav differ diff --git a/ttstoy/vox_words/alert.wav b/ttstoy/vox_words/alert.wav new file mode 100755 index 0000000..1a749b7 Binary files /dev/null and b/ttstoy/vox_words/alert.wav differ diff --git a/ttstoy/vox_words/alien.wav b/ttstoy/vox_words/alien.wav new file mode 100755 index 0000000..5890955 Binary files /dev/null and b/ttstoy/vox_words/alien.wav differ diff --git a/ttstoy/vox_words/aligned.wav b/ttstoy/vox_words/aligned.wav new file mode 100755 index 0000000..20bbe5b Binary files /dev/null and b/ttstoy/vox_words/aligned.wav differ diff --git a/ttstoy/vox_words/all.wav b/ttstoy/vox_words/all.wav new file mode 100755 index 0000000..5bfc4de Binary files /dev/null and b/ttstoy/vox_words/all.wav differ diff --git a/ttstoy/vox_words/alpha.wav b/ttstoy/vox_words/alpha.wav new file mode 100755 index 0000000..d64239e Binary files /dev/null and b/ttstoy/vox_words/alpha.wav differ diff --git a/ttstoy/vox_words/am.wav b/ttstoy/vox_words/am.wav new file mode 100755 index 0000000..c273e52 Binary files /dev/null and b/ttstoy/vox_words/am.wav differ diff --git a/ttstoy/vox_words/amigo.wav b/ttstoy/vox_words/amigo.wav new file mode 100755 index 0000000..03ec617 Binary files /dev/null and b/ttstoy/vox_words/amigo.wav differ diff --git a/ttstoy/vox_words/ammunition.wav b/ttstoy/vox_words/ammunition.wav new file mode 100755 index 0000000..148605c Binary files /dev/null and b/ttstoy/vox_words/ammunition.wav differ diff --git a/ttstoy/vox_words/an.wav b/ttstoy/vox_words/an.wav new file mode 100755 index 0000000..5755607 Binary files /dev/null and b/ttstoy/vox_words/an.wav differ diff --git a/ttstoy/vox_words/and.wav b/ttstoy/vox_words/and.wav new file mode 100755 index 0000000..6e155c0 Binary files /dev/null and b/ttstoy/vox_words/and.wav differ diff --git a/ttstoy/vox_words/announcement.wav b/ttstoy/vox_words/announcement.wav new file mode 100755 index 0000000..9d185ea Binary files /dev/null and b/ttstoy/vox_words/announcement.wav differ diff --git a/ttstoy/vox_words/anomalous.wav b/ttstoy/vox_words/anomalous.wav new file mode 100755 index 0000000..1db9a4f Binary files /dev/null and b/ttstoy/vox_words/anomalous.wav differ diff --git a/ttstoy/vox_words/antenna.wav b/ttstoy/vox_words/antenna.wav new file mode 100755 index 0000000..fa82d58 Binary files /dev/null and b/ttstoy/vox_words/antenna.wav differ diff --git a/ttstoy/vox_words/any.wav b/ttstoy/vox_words/any.wav new file mode 100755 index 0000000..73bed24 Binary files /dev/null and b/ttstoy/vox_words/any.wav differ diff --git a/ttstoy/vox_words/apprehend.wav b/ttstoy/vox_words/apprehend.wav new file mode 100755 index 0000000..874ef83 Binary files /dev/null and b/ttstoy/vox_words/apprehend.wav differ diff --git a/ttstoy/vox_words/approach.wav b/ttstoy/vox_words/approach.wav new file mode 100755 index 0000000..5c5e199 Binary files /dev/null and b/ttstoy/vox_words/approach.wav differ diff --git a/ttstoy/vox_words/are.wav b/ttstoy/vox_words/are.wav new file mode 100755 index 0000000..bacdb31 Binary files /dev/null and b/ttstoy/vox_words/are.wav differ diff --git a/ttstoy/vox_words/area.wav b/ttstoy/vox_words/area.wav new file mode 100755 index 0000000..f2ac3dd Binary files /dev/null and b/ttstoy/vox_words/area.wav differ diff --git a/ttstoy/vox_words/arm.wav b/ttstoy/vox_words/arm.wav new file mode 100755 index 0000000..225abed Binary files /dev/null and b/ttstoy/vox_words/arm.wav differ diff --git a/ttstoy/vox_words/armed.wav b/ttstoy/vox_words/armed.wav new file mode 100755 index 0000000..55e4a67 Binary files /dev/null and b/ttstoy/vox_words/armed.wav differ diff --git a/ttstoy/vox_words/armor.wav b/ttstoy/vox_words/armor.wav new file mode 100755 index 0000000..2ca5170 Binary files /dev/null and b/ttstoy/vox_words/armor.wav differ diff --git a/ttstoy/vox_words/armory.wav b/ttstoy/vox_words/armory.wav new file mode 100755 index 0000000..63fff9e Binary files /dev/null and b/ttstoy/vox_words/armory.wav differ diff --git a/ttstoy/vox_words/arrest.wav b/ttstoy/vox_words/arrest.wav new file mode 100755 index 0000000..cdc24ec Binary files /dev/null and b/ttstoy/vox_words/arrest.wav differ diff --git a/ttstoy/vox_words/ass.wav b/ttstoy/vox_words/ass.wav new file mode 100755 index 0000000..2b6698a Binary files /dev/null and b/ttstoy/vox_words/ass.wav differ diff --git a/ttstoy/vox_words/at.wav b/ttstoy/vox_words/at.wav new file mode 100755 index 0000000..0b1b939 Binary files /dev/null and b/ttstoy/vox_words/at.wav differ diff --git a/ttstoy/vox_words/atomic.wav b/ttstoy/vox_words/atomic.wav new file mode 100755 index 0000000..6a00304 Binary files /dev/null and b/ttstoy/vox_words/atomic.wav differ diff --git a/ttstoy/vox_words/attention.wav b/ttstoy/vox_words/attention.wav new file mode 100755 index 0000000..e9078e6 Binary files /dev/null and b/ttstoy/vox_words/attention.wav differ diff --git a/ttstoy/vox_words/authorize.wav b/ttstoy/vox_words/authorize.wav new file mode 100755 index 0000000..3dcff12 Binary files /dev/null and b/ttstoy/vox_words/authorize.wav differ diff --git a/ttstoy/vox_words/authorized.wav b/ttstoy/vox_words/authorized.wav new file mode 100755 index 0000000..67672ab Binary files /dev/null and b/ttstoy/vox_words/authorized.wav differ diff --git a/ttstoy/vox_words/automatic.wav b/ttstoy/vox_words/automatic.wav new file mode 100755 index 0000000..8e676b0 Binary files /dev/null and b/ttstoy/vox_words/automatic.wav differ diff --git a/ttstoy/vox_words/away.wav b/ttstoy/vox_words/away.wav new file mode 100755 index 0000000..e1a3bee Binary files /dev/null and b/ttstoy/vox_words/away.wav differ diff --git a/ttstoy/vox_words/b.wav b/ttstoy/vox_words/b.wav new file mode 100755 index 0000000..751c7b7 Binary files /dev/null and b/ttstoy/vox_words/b.wav differ diff --git a/ttstoy/vox_words/back.wav b/ttstoy/vox_words/back.wav new file mode 100755 index 0000000..b2f4e56 Binary files /dev/null and b/ttstoy/vox_words/back.wav differ diff --git a/ttstoy/vox_words/backman.wav b/ttstoy/vox_words/backman.wav new file mode 100755 index 0000000..60e149e Binary files /dev/null and b/ttstoy/vox_words/backman.wav differ diff --git a/ttstoy/vox_words/bad.wav b/ttstoy/vox_words/bad.wav new file mode 100755 index 0000000..b591005 Binary files /dev/null and b/ttstoy/vox_words/bad.wav differ diff --git a/ttstoy/vox_words/bag.wav b/ttstoy/vox_words/bag.wav new file mode 100755 index 0000000..ccbf3f8 Binary files /dev/null and b/ttstoy/vox_words/bag.wav differ diff --git a/ttstoy/vox_words/bailey.wav b/ttstoy/vox_words/bailey.wav new file mode 100755 index 0000000..deca97d Binary files /dev/null and b/ttstoy/vox_words/bailey.wav differ diff --git a/ttstoy/vox_words/barracks.wav b/ttstoy/vox_words/barracks.wav new file mode 100755 index 0000000..c7cc651 Binary files /dev/null and b/ttstoy/vox_words/barracks.wav differ diff --git a/ttstoy/vox_words/base.wav b/ttstoy/vox_words/base.wav new file mode 100755 index 0000000..a4c6c82 Binary files /dev/null and b/ttstoy/vox_words/base.wav differ diff --git a/ttstoy/vox_words/bay.wav b/ttstoy/vox_words/bay.wav new file mode 100755 index 0000000..769dfc3 Binary files /dev/null and b/ttstoy/vox_words/bay.wav differ diff --git a/ttstoy/vox_words/be.wav b/ttstoy/vox_words/be.wav new file mode 100755 index 0000000..3807471 Binary files /dev/null and b/ttstoy/vox_words/be.wav differ diff --git a/ttstoy/vox_words/been.wav b/ttstoy/vox_words/been.wav new file mode 100755 index 0000000..959f08e Binary files /dev/null and b/ttstoy/vox_words/been.wav differ diff --git a/ttstoy/vox_words/before.wav b/ttstoy/vox_words/before.wav new file mode 100755 index 0000000..a7c2cdd Binary files /dev/null and b/ttstoy/vox_words/before.wav differ diff --git a/ttstoy/vox_words/beyond.wav b/ttstoy/vox_words/beyond.wav new file mode 100755 index 0000000..d0641c0 Binary files /dev/null and b/ttstoy/vox_words/beyond.wav differ diff --git a/ttstoy/vox_words/biohazard.wav b/ttstoy/vox_words/biohazard.wav new file mode 100755 index 0000000..6ee3aca Binary files /dev/null and b/ttstoy/vox_words/biohazard.wav differ diff --git a/ttstoy/vox_words/biological.wav b/ttstoy/vox_words/biological.wav new file mode 100755 index 0000000..fef8645 Binary files /dev/null and b/ttstoy/vox_words/biological.wav differ diff --git a/ttstoy/vox_words/birdwell.wav b/ttstoy/vox_words/birdwell.wav new file mode 100755 index 0000000..6436f72 Binary files /dev/null and b/ttstoy/vox_words/birdwell.wav differ diff --git a/ttstoy/vox_words/bizwarn.wav b/ttstoy/vox_words/bizwarn.wav new file mode 100755 index 0000000..ff392b4 Binary files /dev/null and b/ttstoy/vox_words/bizwarn.wav differ diff --git a/ttstoy/vox_words/black.wav b/ttstoy/vox_words/black.wav new file mode 100755 index 0000000..52dd088 Binary files /dev/null and b/ttstoy/vox_words/black.wav differ diff --git a/ttstoy/vox_words/blast.wav b/ttstoy/vox_words/blast.wav new file mode 100755 index 0000000..fa8d79f Binary files /dev/null and b/ttstoy/vox_words/blast.wav differ diff --git a/ttstoy/vox_words/blocked.wav b/ttstoy/vox_words/blocked.wav new file mode 100755 index 0000000..5e086f4 Binary files /dev/null and b/ttstoy/vox_words/blocked.wav differ diff --git a/ttstoy/vox_words/bloop.wav b/ttstoy/vox_words/bloop.wav new file mode 100755 index 0000000..49cf90c Binary files /dev/null and b/ttstoy/vox_words/bloop.wav differ diff --git a/ttstoy/vox_words/blue.wav b/ttstoy/vox_words/blue.wav new file mode 100755 index 0000000..83a6e7c Binary files /dev/null and b/ttstoy/vox_words/blue.wav differ diff --git a/ttstoy/vox_words/bottom.wav b/ttstoy/vox_words/bottom.wav new file mode 100755 index 0000000..2330a74 Binary files /dev/null and b/ttstoy/vox_words/bottom.wav differ diff --git a/ttstoy/vox_words/bravo.wav b/ttstoy/vox_words/bravo.wav new file mode 100755 index 0000000..560af14 Binary files /dev/null and b/ttstoy/vox_words/bravo.wav differ diff --git a/ttstoy/vox_words/breach.wav b/ttstoy/vox_words/breach.wav new file mode 100755 index 0000000..89a6178 Binary files /dev/null and b/ttstoy/vox_words/breach.wav differ diff --git a/ttstoy/vox_words/breached.wav b/ttstoy/vox_words/breached.wav new file mode 100755 index 0000000..f4028d1 Binary files /dev/null and b/ttstoy/vox_words/breached.wav differ diff --git a/ttstoy/vox_words/break.wav b/ttstoy/vox_words/break.wav new file mode 100755 index 0000000..1247202 Binary files /dev/null and b/ttstoy/vox_words/break.wav differ diff --git a/ttstoy/vox_words/bridge.wav b/ttstoy/vox_words/bridge.wav new file mode 100755 index 0000000..cbb0cb4 Binary files /dev/null and b/ttstoy/vox_words/bridge.wav differ diff --git a/ttstoy/vox_words/bust.wav b/ttstoy/vox_words/bust.wav new file mode 100755 index 0000000..6af0f12 Binary files /dev/null and b/ttstoy/vox_words/bust.wav differ diff --git a/ttstoy/vox_words/but.wav b/ttstoy/vox_words/but.wav new file mode 100755 index 0000000..0728ed3 Binary files /dev/null and b/ttstoy/vox_words/but.wav differ diff --git a/ttstoy/vox_words/button.wav b/ttstoy/vox_words/button.wav new file mode 100755 index 0000000..bc58323 Binary files /dev/null and b/ttstoy/vox_words/button.wav differ diff --git a/ttstoy/vox_words/buzwarn.wav b/ttstoy/vox_words/buzwarn.wav new file mode 100755 index 0000000..82ddd2c Binary files /dev/null and b/ttstoy/vox_words/buzwarn.wav differ diff --git a/ttstoy/vox_words/bypass.wav b/ttstoy/vox_words/bypass.wav new file mode 100755 index 0000000..b5155d3 Binary files /dev/null and b/ttstoy/vox_words/bypass.wav differ diff --git a/ttstoy/vox_words/c.wav b/ttstoy/vox_words/c.wav new file mode 100755 index 0000000..9fcfc5e Binary files /dev/null and b/ttstoy/vox_words/c.wav differ diff --git a/ttstoy/vox_words/cable.wav b/ttstoy/vox_words/cable.wav new file mode 100755 index 0000000..d761092 Binary files /dev/null and b/ttstoy/vox_words/cable.wav differ diff --git a/ttstoy/vox_words/call.wav b/ttstoy/vox_words/call.wav new file mode 100755 index 0000000..aa06767 Binary files /dev/null and b/ttstoy/vox_words/call.wav differ diff --git a/ttstoy/vox_words/called.wav b/ttstoy/vox_words/called.wav new file mode 100755 index 0000000..f0c2897 Binary files /dev/null and b/ttstoy/vox_words/called.wav differ diff --git a/ttstoy/vox_words/canal.wav b/ttstoy/vox_words/canal.wav new file mode 100755 index 0000000..b426a8f Binary files /dev/null and b/ttstoy/vox_words/canal.wav differ diff --git a/ttstoy/vox_words/cap.wav b/ttstoy/vox_words/cap.wav new file mode 100755 index 0000000..9985051 Binary files /dev/null and b/ttstoy/vox_words/cap.wav differ diff --git a/ttstoy/vox_words/captain.wav b/ttstoy/vox_words/captain.wav new file mode 100755 index 0000000..61222de Binary files /dev/null and b/ttstoy/vox_words/captain.wav differ diff --git a/ttstoy/vox_words/capture.wav b/ttstoy/vox_words/capture.wav new file mode 100755 index 0000000..c7bfaad Binary files /dev/null and b/ttstoy/vox_words/capture.wav differ diff --git a/ttstoy/vox_words/ceiling.wav b/ttstoy/vox_words/ceiling.wav new file mode 100755 index 0000000..381db75 Binary files /dev/null and b/ttstoy/vox_words/ceiling.wav differ diff --git a/ttstoy/vox_words/celsius.wav b/ttstoy/vox_words/celsius.wav new file mode 100755 index 0000000..ac59448 Binary files /dev/null and b/ttstoy/vox_words/celsius.wav differ diff --git a/ttstoy/vox_words/center.wav b/ttstoy/vox_words/center.wav new file mode 100755 index 0000000..35bdcb5 Binary files /dev/null and b/ttstoy/vox_words/center.wav differ diff --git a/ttstoy/vox_words/centi.wav b/ttstoy/vox_words/centi.wav new file mode 100755 index 0000000..efccef0 Binary files /dev/null and b/ttstoy/vox_words/centi.wav differ diff --git a/ttstoy/vox_words/central.wav b/ttstoy/vox_words/central.wav new file mode 100755 index 0000000..1c9c58b Binary files /dev/null and b/ttstoy/vox_words/central.wav differ diff --git a/ttstoy/vox_words/chamber.wav b/ttstoy/vox_words/chamber.wav new file mode 100755 index 0000000..38dbf9f Binary files /dev/null and b/ttstoy/vox_words/chamber.wav differ diff --git a/ttstoy/vox_words/charlie.wav b/ttstoy/vox_words/charlie.wav new file mode 100755 index 0000000..353ce19 Binary files /dev/null and b/ttstoy/vox_words/charlie.wav differ diff --git a/ttstoy/vox_words/check.wav b/ttstoy/vox_words/check.wav new file mode 100755 index 0000000..709f9a6 Binary files /dev/null and b/ttstoy/vox_words/check.wav differ diff --git a/ttstoy/vox_words/checkpoint.wav b/ttstoy/vox_words/checkpoint.wav new file mode 100755 index 0000000..15aa5e4 Binary files /dev/null and b/ttstoy/vox_words/checkpoint.wav differ diff --git a/ttstoy/vox_words/chemical.wav b/ttstoy/vox_words/chemical.wav new file mode 100755 index 0000000..eeb32be Binary files /dev/null and b/ttstoy/vox_words/chemical.wav differ diff --git a/ttstoy/vox_words/cleanup.wav b/ttstoy/vox_words/cleanup.wav new file mode 100755 index 0000000..a47ddb2 Binary files /dev/null and b/ttstoy/vox_words/cleanup.wav differ diff --git a/ttstoy/vox_words/clear.wav b/ttstoy/vox_words/clear.wav new file mode 100755 index 0000000..a23cfc5 Binary files /dev/null and b/ttstoy/vox_words/clear.wav differ diff --git a/ttstoy/vox_words/clearance.wav b/ttstoy/vox_words/clearance.wav new file mode 100755 index 0000000..a35e069 Binary files /dev/null and b/ttstoy/vox_words/clearance.wav differ diff --git a/ttstoy/vox_words/close.wav b/ttstoy/vox_words/close.wav new file mode 100755 index 0000000..6a46d7f Binary files /dev/null and b/ttstoy/vox_words/close.wav differ diff --git a/ttstoy/vox_words/code.wav b/ttstoy/vox_words/code.wav new file mode 100755 index 0000000..63bdfbe Binary files /dev/null and b/ttstoy/vox_words/code.wav differ diff --git a/ttstoy/vox_words/coded.wav b/ttstoy/vox_words/coded.wav new file mode 100755 index 0000000..ec876b2 Binary files /dev/null and b/ttstoy/vox_words/coded.wav differ diff --git a/ttstoy/vox_words/collider.wav b/ttstoy/vox_words/collider.wav new file mode 100755 index 0000000..a26ff0a Binary files /dev/null and b/ttstoy/vox_words/collider.wav differ diff --git a/ttstoy/vox_words/command.wav b/ttstoy/vox_words/command.wav new file mode 100755 index 0000000..ba0c7e8 Binary files /dev/null and b/ttstoy/vox_words/command.wav differ diff --git a/ttstoy/vox_words/communication.wav b/ttstoy/vox_words/communication.wav new file mode 100755 index 0000000..3516535 Binary files /dev/null and b/ttstoy/vox_words/communication.wav differ diff --git a/ttstoy/vox_words/complex.wav b/ttstoy/vox_words/complex.wav new file mode 100755 index 0000000..d2c01cc Binary files /dev/null and b/ttstoy/vox_words/complex.wav differ diff --git a/ttstoy/vox_words/computer.wav b/ttstoy/vox_words/computer.wav new file mode 100755 index 0000000..89808c2 Binary files /dev/null and b/ttstoy/vox_words/computer.wav differ diff --git a/ttstoy/vox_words/condition.wav b/ttstoy/vox_words/condition.wav new file mode 100755 index 0000000..b32c836 Binary files /dev/null and b/ttstoy/vox_words/condition.wav differ diff --git a/ttstoy/vox_words/containment.wav b/ttstoy/vox_words/containment.wav new file mode 100755 index 0000000..563cb4e Binary files /dev/null and b/ttstoy/vox_words/containment.wav differ diff --git a/ttstoy/vox_words/contamination.wav b/ttstoy/vox_words/contamination.wav new file mode 100755 index 0000000..d597267 Binary files /dev/null and b/ttstoy/vox_words/contamination.wav differ diff --git a/ttstoy/vox_words/control.wav b/ttstoy/vox_words/control.wav new file mode 100755 index 0000000..e7bf57b Binary files /dev/null and b/ttstoy/vox_words/control.wav differ diff --git a/ttstoy/vox_words/coolant.wav b/ttstoy/vox_words/coolant.wav new file mode 100755 index 0000000..d4474b9 Binary files /dev/null and b/ttstoy/vox_words/coolant.wav differ diff --git a/ttstoy/vox_words/coomer.wav b/ttstoy/vox_words/coomer.wav new file mode 100755 index 0000000..1e4b379 Binary files /dev/null and b/ttstoy/vox_words/coomer.wav differ diff --git a/ttstoy/vox_words/core.wav b/ttstoy/vox_words/core.wav new file mode 100755 index 0000000..da6874b Binary files /dev/null and b/ttstoy/vox_words/core.wav differ diff --git a/ttstoy/vox_words/correct.wav b/ttstoy/vox_words/correct.wav new file mode 100755 index 0000000..cde927b Binary files /dev/null and b/ttstoy/vox_words/correct.wav differ diff --git a/ttstoy/vox_words/corridor.wav b/ttstoy/vox_words/corridor.wav new file mode 100755 index 0000000..defdcda Binary files /dev/null and b/ttstoy/vox_words/corridor.wav differ diff --git a/ttstoy/vox_words/crew.wav b/ttstoy/vox_words/crew.wav new file mode 100755 index 0000000..4f48a08 Binary files /dev/null and b/ttstoy/vox_words/crew.wav differ diff --git a/ttstoy/vox_words/cross.wav b/ttstoy/vox_words/cross.wav new file mode 100755 index 0000000..c393dff Binary files /dev/null and b/ttstoy/vox_words/cross.wav differ diff --git a/ttstoy/vox_words/cryogenic.wav b/ttstoy/vox_words/cryogenic.wav new file mode 100755 index 0000000..eb1a62a Binary files /dev/null and b/ttstoy/vox_words/cryogenic.wav differ diff --git a/ttstoy/vox_words/d.wav b/ttstoy/vox_words/d.wav new file mode 100755 index 0000000..f977a49 Binary files /dev/null and b/ttstoy/vox_words/d.wav differ diff --git a/ttstoy/vox_words/dadeda.wav b/ttstoy/vox_words/dadeda.wav new file mode 100755 index 0000000..2ac0ff5 Binary files /dev/null and b/ttstoy/vox_words/dadeda.wav differ diff --git a/ttstoy/vox_words/damage.wav b/ttstoy/vox_words/damage.wav new file mode 100755 index 0000000..83fce3d Binary files /dev/null and b/ttstoy/vox_words/damage.wav differ diff --git a/ttstoy/vox_words/damaged.wav b/ttstoy/vox_words/damaged.wav new file mode 100755 index 0000000..e9f2ff5 Binary files /dev/null and b/ttstoy/vox_words/damaged.wav differ diff --git a/ttstoy/vox_words/danger.wav b/ttstoy/vox_words/danger.wav new file mode 100755 index 0000000..40ff8b8 Binary files /dev/null and b/ttstoy/vox_words/danger.wav differ diff --git a/ttstoy/vox_words/day.wav b/ttstoy/vox_words/day.wav new file mode 100755 index 0000000..92e3fe8 Binary files /dev/null and b/ttstoy/vox_words/day.wav differ diff --git a/ttstoy/vox_words/deactivated.wav b/ttstoy/vox_words/deactivated.wav new file mode 100755 index 0000000..f587f26 Binary files /dev/null and b/ttstoy/vox_words/deactivated.wav differ diff --git a/ttstoy/vox_words/decompression.wav b/ttstoy/vox_words/decompression.wav new file mode 100755 index 0000000..a758f1b Binary files /dev/null and b/ttstoy/vox_words/decompression.wav differ diff --git a/ttstoy/vox_words/decontamination.wav b/ttstoy/vox_words/decontamination.wav new file mode 100755 index 0000000..9794de0 Binary files /dev/null and b/ttstoy/vox_words/decontamination.wav differ diff --git a/ttstoy/vox_words/deeoo.wav b/ttstoy/vox_words/deeoo.wav new file mode 100755 index 0000000..4f82a18 Binary files /dev/null and b/ttstoy/vox_words/deeoo.wav differ diff --git a/ttstoy/vox_words/defense.wav b/ttstoy/vox_words/defense.wav new file mode 100755 index 0000000..9da8de1 Binary files /dev/null and b/ttstoy/vox_words/defense.wav differ diff --git a/ttstoy/vox_words/degrees.wav b/ttstoy/vox_words/degrees.wav new file mode 100755 index 0000000..7e77561 Binary files /dev/null and b/ttstoy/vox_words/degrees.wav differ diff --git a/ttstoy/vox_words/delta.wav b/ttstoy/vox_words/delta.wav new file mode 100755 index 0000000..8605280 Binary files /dev/null and b/ttstoy/vox_words/delta.wav differ diff --git a/ttstoy/vox_words/denied.wav b/ttstoy/vox_words/denied.wav new file mode 100755 index 0000000..6b1167d Binary files /dev/null and b/ttstoy/vox_words/denied.wav differ diff --git a/ttstoy/vox_words/deploy.wav b/ttstoy/vox_words/deploy.wav new file mode 100755 index 0000000..2f9aacd Binary files /dev/null and b/ttstoy/vox_words/deploy.wav differ diff --git a/ttstoy/vox_words/deployed.wav b/ttstoy/vox_words/deployed.wav new file mode 100755 index 0000000..a85a076 Binary files /dev/null and b/ttstoy/vox_words/deployed.wav differ diff --git a/ttstoy/vox_words/destroy.wav b/ttstoy/vox_words/destroy.wav new file mode 100755 index 0000000..56cf437 Binary files /dev/null and b/ttstoy/vox_words/destroy.wav differ diff --git a/ttstoy/vox_words/destroyed.wav b/ttstoy/vox_words/destroyed.wav new file mode 100755 index 0000000..20308df Binary files /dev/null and b/ttstoy/vox_words/destroyed.wav differ diff --git a/ttstoy/vox_words/detain.wav b/ttstoy/vox_words/detain.wav new file mode 100755 index 0000000..7d5f4e0 Binary files /dev/null and b/ttstoy/vox_words/detain.wav differ diff --git a/ttstoy/vox_words/detected.wav b/ttstoy/vox_words/detected.wav new file mode 100755 index 0000000..101429a Binary files /dev/null and b/ttstoy/vox_words/detected.wav differ diff --git a/ttstoy/vox_words/detonation.wav b/ttstoy/vox_words/detonation.wav new file mode 100755 index 0000000..a696e97 Binary files /dev/null and b/ttstoy/vox_words/detonation.wav differ diff --git a/ttstoy/vox_words/device.wav b/ttstoy/vox_words/device.wav new file mode 100755 index 0000000..d5ecd2f Binary files /dev/null and b/ttstoy/vox_words/device.wav differ diff --git a/ttstoy/vox_words/did.wav b/ttstoy/vox_words/did.wav new file mode 100755 index 0000000..0c52e31 Binary files /dev/null and b/ttstoy/vox_words/did.wav differ diff --git a/ttstoy/vox_words/die.wav b/ttstoy/vox_words/die.wav new file mode 100755 index 0000000..b47622c Binary files /dev/null and b/ttstoy/vox_words/die.wav differ diff --git a/ttstoy/vox_words/dimensional.wav b/ttstoy/vox_words/dimensional.wav new file mode 100755 index 0000000..f67956d Binary files /dev/null and b/ttstoy/vox_words/dimensional.wav differ diff --git a/ttstoy/vox_words/dirt.wav b/ttstoy/vox_words/dirt.wav new file mode 100755 index 0000000..08990c4 Binary files /dev/null and b/ttstoy/vox_words/dirt.wav differ diff --git a/ttstoy/vox_words/disengaged.wav b/ttstoy/vox_words/disengaged.wav new file mode 100755 index 0000000..aa84ed2 Binary files /dev/null and b/ttstoy/vox_words/disengaged.wav differ diff --git a/ttstoy/vox_words/dish.wav b/ttstoy/vox_words/dish.wav new file mode 100755 index 0000000..aeff92d Binary files /dev/null and b/ttstoy/vox_words/dish.wav differ diff --git a/ttstoy/vox_words/disposal.wav b/ttstoy/vox_words/disposal.wav new file mode 100755 index 0000000..bfe38f6 Binary files /dev/null and b/ttstoy/vox_words/disposal.wav differ diff --git a/ttstoy/vox_words/distance.wav b/ttstoy/vox_words/distance.wav new file mode 100755 index 0000000..670eaca Binary files /dev/null and b/ttstoy/vox_words/distance.wav differ diff --git a/ttstoy/vox_words/distortion.wav b/ttstoy/vox_words/distortion.wav new file mode 100755 index 0000000..7fae33e Binary files /dev/null and b/ttstoy/vox_words/distortion.wav differ diff --git a/ttstoy/vox_words/do.wav b/ttstoy/vox_words/do.wav new file mode 100755 index 0000000..d82a9ab Binary files /dev/null and b/ttstoy/vox_words/do.wav differ diff --git a/ttstoy/vox_words/doctor.wav b/ttstoy/vox_words/doctor.wav new file mode 100755 index 0000000..f9cdc53 Binary files /dev/null and b/ttstoy/vox_words/doctor.wav differ diff --git a/ttstoy/vox_words/doop.wav b/ttstoy/vox_words/doop.wav new file mode 100755 index 0000000..14bce15 Binary files /dev/null and b/ttstoy/vox_words/doop.wav differ diff --git a/ttstoy/vox_words/door.wav b/ttstoy/vox_words/door.wav new file mode 100755 index 0000000..629dee3 Binary files /dev/null and b/ttstoy/vox_words/door.wav differ diff --git a/ttstoy/vox_words/down.wav b/ttstoy/vox_words/down.wav new file mode 100755 index 0000000..b677e03 Binary files /dev/null and b/ttstoy/vox_words/down.wav differ diff --git a/ttstoy/vox_words/dual.wav b/ttstoy/vox_words/dual.wav new file mode 100755 index 0000000..4693b31 Binary files /dev/null and b/ttstoy/vox_words/dual.wav differ diff --git a/ttstoy/vox_words/duct.wav b/ttstoy/vox_words/duct.wav new file mode 100755 index 0000000..d0e937f Binary files /dev/null and b/ttstoy/vox_words/duct.wav differ diff --git a/ttstoy/vox_words/e.wav b/ttstoy/vox_words/e.wav new file mode 100755 index 0000000..6f991d0 Binary files /dev/null and b/ttstoy/vox_words/e.wav differ diff --git a/ttstoy/vox_words/east.wav b/ttstoy/vox_words/east.wav new file mode 100755 index 0000000..296ce53 Binary files /dev/null and b/ttstoy/vox_words/east.wav differ diff --git a/ttstoy/vox_words/echo.wav b/ttstoy/vox_words/echo.wav new file mode 100755 index 0000000..3087ecb Binary files /dev/null and b/ttstoy/vox_words/echo.wav differ diff --git a/ttstoy/vox_words/ed.wav b/ttstoy/vox_words/ed.wav new file mode 100755 index 0000000..71303ff Binary files /dev/null and b/ttstoy/vox_words/ed.wav differ diff --git a/ttstoy/vox_words/effect.wav b/ttstoy/vox_words/effect.wav new file mode 100755 index 0000000..a0128cf Binary files /dev/null and b/ttstoy/vox_words/effect.wav differ diff --git a/ttstoy/vox_words/egress.wav b/ttstoy/vox_words/egress.wav new file mode 100755 index 0000000..47fa437 Binary files /dev/null and b/ttstoy/vox_words/egress.wav differ diff --git a/ttstoy/vox_words/eight.wav b/ttstoy/vox_words/eight.wav new file mode 100755 index 0000000..4fef5c2 Binary files /dev/null and b/ttstoy/vox_words/eight.wav differ diff --git a/ttstoy/vox_words/eighteen.wav b/ttstoy/vox_words/eighteen.wav new file mode 100755 index 0000000..1aff299 Binary files /dev/null and b/ttstoy/vox_words/eighteen.wav differ diff --git a/ttstoy/vox_words/eighty.wav b/ttstoy/vox_words/eighty.wav new file mode 100755 index 0000000..a1f5483 Binary files /dev/null and b/ttstoy/vox_words/eighty.wav differ diff --git a/ttstoy/vox_words/electric.wav b/ttstoy/vox_words/electric.wav new file mode 100755 index 0000000..48c4fe0 Binary files /dev/null and b/ttstoy/vox_words/electric.wav differ diff --git a/ttstoy/vox_words/electromagnetic.wav b/ttstoy/vox_words/electromagnetic.wav new file mode 100755 index 0000000..886c76e Binary files /dev/null and b/ttstoy/vox_words/electromagnetic.wav differ diff --git a/ttstoy/vox_words/elevator.wav b/ttstoy/vox_words/elevator.wav new file mode 100755 index 0000000..a4d4144 Binary files /dev/null and b/ttstoy/vox_words/elevator.wav differ diff --git a/ttstoy/vox_words/eleven.wav b/ttstoy/vox_words/eleven.wav new file mode 100755 index 0000000..5473a25 Binary files /dev/null and b/ttstoy/vox_words/eleven.wav differ diff --git a/ttstoy/vox_words/eliminate.wav b/ttstoy/vox_words/eliminate.wav new file mode 100755 index 0000000..d4e51af Binary files /dev/null and b/ttstoy/vox_words/eliminate.wav differ diff --git a/ttstoy/vox_words/emergency.wav b/ttstoy/vox_words/emergency.wav new file mode 100755 index 0000000..62bb34e Binary files /dev/null and b/ttstoy/vox_words/emergency.wav differ diff --git a/ttstoy/vox_words/energy.wav b/ttstoy/vox_words/energy.wav new file mode 100755 index 0000000..b66ed4d Binary files /dev/null and b/ttstoy/vox_words/energy.wav differ diff --git a/ttstoy/vox_words/engage.wav b/ttstoy/vox_words/engage.wav new file mode 100755 index 0000000..11a43c4 Binary files /dev/null and b/ttstoy/vox_words/engage.wav differ diff --git a/ttstoy/vox_words/engaged.wav b/ttstoy/vox_words/engaged.wav new file mode 100755 index 0000000..a47cbb5 Binary files /dev/null and b/ttstoy/vox_words/engaged.wav differ diff --git a/ttstoy/vox_words/engine.wav b/ttstoy/vox_words/engine.wav new file mode 100755 index 0000000..f6ab907 Binary files /dev/null and b/ttstoy/vox_words/engine.wav differ diff --git a/ttstoy/vox_words/enter.wav b/ttstoy/vox_words/enter.wav new file mode 100755 index 0000000..f3f53ea Binary files /dev/null and b/ttstoy/vox_words/enter.wav differ diff --git a/ttstoy/vox_words/entry.wav b/ttstoy/vox_words/entry.wav new file mode 100755 index 0000000..a027e1a Binary files /dev/null and b/ttstoy/vox_words/entry.wav differ diff --git a/ttstoy/vox_words/environment.wav b/ttstoy/vox_words/environment.wav new file mode 100755 index 0000000..6ab7d94 Binary files /dev/null and b/ttstoy/vox_words/environment.wav differ diff --git a/ttstoy/vox_words/error.wav b/ttstoy/vox_words/error.wav new file mode 100755 index 0000000..05b2beb Binary files /dev/null and b/ttstoy/vox_words/error.wav differ diff --git a/ttstoy/vox_words/escape.wav b/ttstoy/vox_words/escape.wav new file mode 100755 index 0000000..dbf3c95 Binary files /dev/null and b/ttstoy/vox_words/escape.wav differ diff --git a/ttstoy/vox_words/evacuate.wav b/ttstoy/vox_words/evacuate.wav new file mode 100755 index 0000000..2766662 Binary files /dev/null and b/ttstoy/vox_words/evacuate.wav differ diff --git a/ttstoy/vox_words/exchange.wav b/ttstoy/vox_words/exchange.wav new file mode 100755 index 0000000..73426ae Binary files /dev/null and b/ttstoy/vox_words/exchange.wav differ diff --git a/ttstoy/vox_words/exit.wav b/ttstoy/vox_words/exit.wav new file mode 100755 index 0000000..85680b3 Binary files /dev/null and b/ttstoy/vox_words/exit.wav differ diff --git a/ttstoy/vox_words/expect.wav b/ttstoy/vox_words/expect.wav new file mode 100755 index 0000000..a562e53 Binary files /dev/null and b/ttstoy/vox_words/expect.wav differ diff --git a/ttstoy/vox_words/experiment.wav b/ttstoy/vox_words/experiment.wav new file mode 100755 index 0000000..3f60c21 Binary files /dev/null and b/ttstoy/vox_words/experiment.wav differ diff --git a/ttstoy/vox_words/experimental.wav b/ttstoy/vox_words/experimental.wav new file mode 100755 index 0000000..fbf75a5 Binary files /dev/null and b/ttstoy/vox_words/experimental.wav differ diff --git a/ttstoy/vox_words/explode.wav b/ttstoy/vox_words/explode.wav new file mode 100755 index 0000000..847e645 Binary files /dev/null and b/ttstoy/vox_words/explode.wav differ diff --git a/ttstoy/vox_words/explosion.wav b/ttstoy/vox_words/explosion.wav new file mode 100755 index 0000000..a2e442e Binary files /dev/null and b/ttstoy/vox_words/explosion.wav differ diff --git a/ttstoy/vox_words/exposure.wav b/ttstoy/vox_words/exposure.wav new file mode 100755 index 0000000..37a933c Binary files /dev/null and b/ttstoy/vox_words/exposure.wav differ diff --git a/ttstoy/vox_words/exterminate.wav b/ttstoy/vox_words/exterminate.wav new file mode 100755 index 0000000..f1c3c23 Binary files /dev/null and b/ttstoy/vox_words/exterminate.wav differ diff --git a/ttstoy/vox_words/extinguish.wav b/ttstoy/vox_words/extinguish.wav new file mode 100755 index 0000000..1fc83d9 Binary files /dev/null and b/ttstoy/vox_words/extinguish.wav differ diff --git a/ttstoy/vox_words/extinguisher.wav b/ttstoy/vox_words/extinguisher.wav new file mode 100755 index 0000000..eed3164 Binary files /dev/null and b/ttstoy/vox_words/extinguisher.wav differ diff --git a/ttstoy/vox_words/extreme.wav b/ttstoy/vox_words/extreme.wav new file mode 100755 index 0000000..0fc3adf Binary files /dev/null and b/ttstoy/vox_words/extreme.wav differ diff --git a/ttstoy/vox_words/f.wav b/ttstoy/vox_words/f.wav new file mode 100755 index 0000000..0f06b33 Binary files /dev/null and b/ttstoy/vox_words/f.wav differ diff --git a/ttstoy/vox_words/facility.wav b/ttstoy/vox_words/facility.wav new file mode 100755 index 0000000..812445e Binary files /dev/null and b/ttstoy/vox_words/facility.wav differ diff --git a/ttstoy/vox_words/fahrenheit.wav b/ttstoy/vox_words/fahrenheit.wav new file mode 100755 index 0000000..96411aa Binary files /dev/null and b/ttstoy/vox_words/fahrenheit.wav differ diff --git a/ttstoy/vox_words/failed.wav b/ttstoy/vox_words/failed.wav new file mode 100755 index 0000000..f717b05 Binary files /dev/null and b/ttstoy/vox_words/failed.wav differ diff --git a/ttstoy/vox_words/failure.wav b/ttstoy/vox_words/failure.wav new file mode 100755 index 0000000..dd662fc Binary files /dev/null and b/ttstoy/vox_words/failure.wav differ diff --git a/ttstoy/vox_words/farthest.wav b/ttstoy/vox_words/farthest.wav new file mode 100755 index 0000000..fcca7a4 Binary files /dev/null and b/ttstoy/vox_words/farthest.wav differ diff --git a/ttstoy/vox_words/fast.wav b/ttstoy/vox_words/fast.wav new file mode 100755 index 0000000..821885a Binary files /dev/null and b/ttstoy/vox_words/fast.wav differ diff --git a/ttstoy/vox_words/feet.wav b/ttstoy/vox_words/feet.wav new file mode 100755 index 0000000..5adc5db Binary files /dev/null and b/ttstoy/vox_words/feet.wav differ diff --git a/ttstoy/vox_words/field.wav b/ttstoy/vox_words/field.wav new file mode 100755 index 0000000..6cc1ab4 Binary files /dev/null and b/ttstoy/vox_words/field.wav differ diff --git a/ttstoy/vox_words/fifteen.wav b/ttstoy/vox_words/fifteen.wav new file mode 100755 index 0000000..63ca382 Binary files /dev/null and b/ttstoy/vox_words/fifteen.wav differ diff --git a/ttstoy/vox_words/fifth.wav b/ttstoy/vox_words/fifth.wav new file mode 100755 index 0000000..3886e04 Binary files /dev/null and b/ttstoy/vox_words/fifth.wav differ diff --git a/ttstoy/vox_words/fifty.wav b/ttstoy/vox_words/fifty.wav new file mode 100755 index 0000000..80f7b11 Binary files /dev/null and b/ttstoy/vox_words/fifty.wav differ diff --git a/ttstoy/vox_words/final.wav b/ttstoy/vox_words/final.wav new file mode 100755 index 0000000..d4c807a Binary files /dev/null and b/ttstoy/vox_words/final.wav differ diff --git a/ttstoy/vox_words/fine.wav b/ttstoy/vox_words/fine.wav new file mode 100755 index 0000000..f74d8fb Binary files /dev/null and b/ttstoy/vox_words/fine.wav differ diff --git a/ttstoy/vox_words/fire.wav b/ttstoy/vox_words/fire.wav new file mode 100755 index 0000000..ce0a45d Binary files /dev/null and b/ttstoy/vox_words/fire.wav differ diff --git a/ttstoy/vox_words/first.wav b/ttstoy/vox_words/first.wav new file mode 100755 index 0000000..18a7fdf Binary files /dev/null and b/ttstoy/vox_words/first.wav differ diff --git a/ttstoy/vox_words/five.wav b/ttstoy/vox_words/five.wav new file mode 100755 index 0000000..aa23cbe Binary files /dev/null and b/ttstoy/vox_words/five.wav differ diff --git a/ttstoy/vox_words/flooding.wav b/ttstoy/vox_words/flooding.wav new file mode 100755 index 0000000..7b9f5bc Binary files /dev/null and b/ttstoy/vox_words/flooding.wav differ diff --git a/ttstoy/vox_words/floor.wav b/ttstoy/vox_words/floor.wav new file mode 100755 index 0000000..b086549 Binary files /dev/null and b/ttstoy/vox_words/floor.wav differ diff --git a/ttstoy/vox_words/fool.wav b/ttstoy/vox_words/fool.wav new file mode 100755 index 0000000..327b3f5 Binary files /dev/null and b/ttstoy/vox_words/fool.wav differ diff --git a/ttstoy/vox_words/for.wav b/ttstoy/vox_words/for.wav new file mode 100755 index 0000000..84f793d Binary files /dev/null and b/ttstoy/vox_words/for.wav differ diff --git a/ttstoy/vox_words/forbidden.wav b/ttstoy/vox_words/forbidden.wav new file mode 100755 index 0000000..f4d7425 Binary files /dev/null and b/ttstoy/vox_words/forbidden.wav differ diff --git a/ttstoy/vox_words/force.wav b/ttstoy/vox_words/force.wav new file mode 100755 index 0000000..3745c71 Binary files /dev/null and b/ttstoy/vox_words/force.wav differ diff --git a/ttstoy/vox_words/forms.wav b/ttstoy/vox_words/forms.wav new file mode 100755 index 0000000..642e201 Binary files /dev/null and b/ttstoy/vox_words/forms.wav differ diff --git a/ttstoy/vox_words/found.wav b/ttstoy/vox_words/found.wav new file mode 100755 index 0000000..59da403 Binary files /dev/null and b/ttstoy/vox_words/found.wav differ diff --git a/ttstoy/vox_words/four.wav b/ttstoy/vox_words/four.wav new file mode 100755 index 0000000..a614cbc Binary files /dev/null and b/ttstoy/vox_words/four.wav differ diff --git a/ttstoy/vox_words/fourteen.wav b/ttstoy/vox_words/fourteen.wav new file mode 100755 index 0000000..aa08468 Binary files /dev/null and b/ttstoy/vox_words/fourteen.wav differ diff --git a/ttstoy/vox_words/fourth.wav b/ttstoy/vox_words/fourth.wav new file mode 100755 index 0000000..5c9071d Binary files /dev/null and b/ttstoy/vox_words/fourth.wav differ diff --git a/ttstoy/vox_words/fourty.wav b/ttstoy/vox_words/fourty.wav new file mode 100755 index 0000000..6bb8fb3 Binary files /dev/null and b/ttstoy/vox_words/fourty.wav differ diff --git a/ttstoy/vox_words/foxtrot.wav b/ttstoy/vox_words/foxtrot.wav new file mode 100755 index 0000000..ece21be Binary files /dev/null and b/ttstoy/vox_words/foxtrot.wav differ diff --git a/ttstoy/vox_words/freeman.wav b/ttstoy/vox_words/freeman.wav new file mode 100755 index 0000000..26a9ae2 Binary files /dev/null and b/ttstoy/vox_words/freeman.wav differ diff --git a/ttstoy/vox_words/freezer.wav b/ttstoy/vox_words/freezer.wav new file mode 100755 index 0000000..32da861 Binary files /dev/null and b/ttstoy/vox_words/freezer.wav differ diff --git a/ttstoy/vox_words/from.wav b/ttstoy/vox_words/from.wav new file mode 100755 index 0000000..46541ab Binary files /dev/null and b/ttstoy/vox_words/from.wav differ diff --git a/ttstoy/vox_words/front.wav b/ttstoy/vox_words/front.wav new file mode 100755 index 0000000..46d6001 Binary files /dev/null and b/ttstoy/vox_words/front.wav differ diff --git a/ttstoy/vox_words/fuel.wav b/ttstoy/vox_words/fuel.wav new file mode 100755 index 0000000..1b2256b Binary files /dev/null and b/ttstoy/vox_words/fuel.wav differ diff --git a/ttstoy/vox_words/g.wav b/ttstoy/vox_words/g.wav new file mode 100755 index 0000000..37f5ce8 Binary files /dev/null and b/ttstoy/vox_words/g.wav differ diff --git a/ttstoy/vox_words/get.wav b/ttstoy/vox_words/get.wav new file mode 100755 index 0000000..9dd512d Binary files /dev/null and b/ttstoy/vox_words/get.wav differ diff --git a/ttstoy/vox_words/go.wav b/ttstoy/vox_words/go.wav new file mode 100755 index 0000000..665989a Binary files /dev/null and b/ttstoy/vox_words/go.wav differ diff --git a/ttstoy/vox_words/going.wav b/ttstoy/vox_words/going.wav new file mode 100755 index 0000000..308b1b6 Binary files /dev/null and b/ttstoy/vox_words/going.wav differ diff --git a/ttstoy/vox_words/good.wav b/ttstoy/vox_words/good.wav new file mode 100755 index 0000000..bf15c9c Binary files /dev/null and b/ttstoy/vox_words/good.wav differ diff --git a/ttstoy/vox_words/goodbye.wav b/ttstoy/vox_words/goodbye.wav new file mode 100755 index 0000000..2599ca5 Binary files /dev/null and b/ttstoy/vox_words/goodbye.wav differ diff --git a/ttstoy/vox_words/gordon.wav b/ttstoy/vox_words/gordon.wav new file mode 100755 index 0000000..51c7195 Binary files /dev/null and b/ttstoy/vox_words/gordon.wav differ diff --git a/ttstoy/vox_words/got.wav b/ttstoy/vox_words/got.wav new file mode 100755 index 0000000..b2b47c9 Binary files /dev/null and b/ttstoy/vox_words/got.wav differ diff --git a/ttstoy/vox_words/government.wav b/ttstoy/vox_words/government.wav new file mode 100755 index 0000000..17e7fa8 Binary files /dev/null and b/ttstoy/vox_words/government.wav differ diff --git a/ttstoy/vox_words/granted.wav b/ttstoy/vox_words/granted.wav new file mode 100755 index 0000000..661bf05 Binary files /dev/null and b/ttstoy/vox_words/granted.wav differ diff --git a/ttstoy/vox_words/great.wav b/ttstoy/vox_words/great.wav new file mode 100755 index 0000000..8640b88 Binary files /dev/null and b/ttstoy/vox_words/great.wav differ diff --git a/ttstoy/vox_words/green.wav b/ttstoy/vox_words/green.wav new file mode 100755 index 0000000..0eb5558 Binary files /dev/null and b/ttstoy/vox_words/green.wav differ diff --git a/ttstoy/vox_words/grenade.wav b/ttstoy/vox_words/grenade.wav new file mode 100755 index 0000000..7b2d80e Binary files /dev/null and b/ttstoy/vox_words/grenade.wav differ diff --git a/ttstoy/vox_words/guard.wav b/ttstoy/vox_words/guard.wav new file mode 100755 index 0000000..e715dc6 Binary files /dev/null and b/ttstoy/vox_words/guard.wav differ diff --git a/ttstoy/vox_words/gulf.wav b/ttstoy/vox_words/gulf.wav new file mode 100755 index 0000000..95b3e9f Binary files /dev/null and b/ttstoy/vox_words/gulf.wav differ diff --git a/ttstoy/vox_words/gun.wav b/ttstoy/vox_words/gun.wav new file mode 100755 index 0000000..86e3ac1 Binary files /dev/null and b/ttstoy/vox_words/gun.wav differ diff --git a/ttstoy/vox_words/guthrie.wav b/ttstoy/vox_words/guthrie.wav new file mode 100755 index 0000000..362ab46 Binary files /dev/null and b/ttstoy/vox_words/guthrie.wav differ diff --git a/ttstoy/vox_words/handling.wav b/ttstoy/vox_words/handling.wav new file mode 100755 index 0000000..b9c7c5f Binary files /dev/null and b/ttstoy/vox_words/handling.wav differ diff --git a/ttstoy/vox_words/hangar.wav b/ttstoy/vox_words/hangar.wav new file mode 100755 index 0000000..e937054 Binary files /dev/null and b/ttstoy/vox_words/hangar.wav differ diff --git a/ttstoy/vox_words/has.wav b/ttstoy/vox_words/has.wav new file mode 100755 index 0000000..625a110 Binary files /dev/null and b/ttstoy/vox_words/has.wav differ diff --git a/ttstoy/vox_words/have.wav b/ttstoy/vox_words/have.wav new file mode 100755 index 0000000..c5f0794 Binary files /dev/null and b/ttstoy/vox_words/have.wav differ diff --git a/ttstoy/vox_words/hazard.wav b/ttstoy/vox_words/hazard.wav new file mode 100755 index 0000000..b311552 Binary files /dev/null and b/ttstoy/vox_words/hazard.wav differ diff --git a/ttstoy/vox_words/head.wav b/ttstoy/vox_words/head.wav new file mode 100755 index 0000000..ed8e9d8 Binary files /dev/null and b/ttstoy/vox_words/head.wav differ diff --git a/ttstoy/vox_words/health.wav b/ttstoy/vox_words/health.wav new file mode 100755 index 0000000..e315831 Binary files /dev/null and b/ttstoy/vox_words/health.wav differ diff --git a/ttstoy/vox_words/heat.wav b/ttstoy/vox_words/heat.wav new file mode 100755 index 0000000..ca0b345 Binary files /dev/null and b/ttstoy/vox_words/heat.wav differ diff --git a/ttstoy/vox_words/helicopter.wav b/ttstoy/vox_words/helicopter.wav new file mode 100755 index 0000000..22e5449 Binary files /dev/null and b/ttstoy/vox_words/helicopter.wav differ diff --git a/ttstoy/vox_words/helium.wav b/ttstoy/vox_words/helium.wav new file mode 100755 index 0000000..fbb8197 Binary files /dev/null and b/ttstoy/vox_words/helium.wav differ diff --git a/ttstoy/vox_words/hello.wav b/ttstoy/vox_words/hello.wav new file mode 100755 index 0000000..7e61fec Binary files /dev/null and b/ttstoy/vox_words/hello.wav differ diff --git a/ttstoy/vox_words/help.wav b/ttstoy/vox_words/help.wav new file mode 100755 index 0000000..5bef919 Binary files /dev/null and b/ttstoy/vox_words/help.wav differ diff --git a/ttstoy/vox_words/here.wav b/ttstoy/vox_words/here.wav new file mode 100755 index 0000000..e6d74f4 Binary files /dev/null and b/ttstoy/vox_words/here.wav differ diff --git a/ttstoy/vox_words/hide.wav b/ttstoy/vox_words/hide.wav new file mode 100755 index 0000000..f7bb32b Binary files /dev/null and b/ttstoy/vox_words/hide.wav differ diff --git a/ttstoy/vox_words/high.wav b/ttstoy/vox_words/high.wav new file mode 100755 index 0000000..832bf16 Binary files /dev/null and b/ttstoy/vox_words/high.wav differ diff --git a/ttstoy/vox_words/highest.wav b/ttstoy/vox_words/highest.wav new file mode 100755 index 0000000..ef4b2ca Binary files /dev/null and b/ttstoy/vox_words/highest.wav differ diff --git a/ttstoy/vox_words/hit.wav b/ttstoy/vox_words/hit.wav new file mode 100755 index 0000000..d70619a Binary files /dev/null and b/ttstoy/vox_words/hit.wav differ diff --git a/ttstoy/vox_words/hole.wav b/ttstoy/vox_words/hole.wav new file mode 100755 index 0000000..afb46f5 Binary files /dev/null and b/ttstoy/vox_words/hole.wav differ diff --git a/ttstoy/vox_words/hostile.wav b/ttstoy/vox_words/hostile.wav new file mode 100755 index 0000000..4dc88f2 Binary files /dev/null and b/ttstoy/vox_words/hostile.wav differ diff --git a/ttstoy/vox_words/hot.wav b/ttstoy/vox_words/hot.wav new file mode 100755 index 0000000..7ced072 Binary files /dev/null and b/ttstoy/vox_words/hot.wav differ diff --git a/ttstoy/vox_words/hotel.wav b/ttstoy/vox_words/hotel.wav new file mode 100755 index 0000000..dde78cf Binary files /dev/null and b/ttstoy/vox_words/hotel.wav differ diff --git a/ttstoy/vox_words/hour.wav b/ttstoy/vox_words/hour.wav new file mode 100755 index 0000000..d428bd7 Binary files /dev/null and b/ttstoy/vox_words/hour.wav differ diff --git a/ttstoy/vox_words/hours.wav b/ttstoy/vox_words/hours.wav new file mode 100755 index 0000000..63a2470 Binary files /dev/null and b/ttstoy/vox_words/hours.wav differ diff --git a/ttstoy/vox_words/hundred.wav b/ttstoy/vox_words/hundred.wav new file mode 100755 index 0000000..8d3a8a0 Binary files /dev/null and b/ttstoy/vox_words/hundred.wav differ diff --git a/ttstoy/vox_words/hydro.wav b/ttstoy/vox_words/hydro.wav new file mode 100755 index 0000000..9653b7b Binary files /dev/null and b/ttstoy/vox_words/hydro.wav differ diff --git a/ttstoy/vox_words/i.wav b/ttstoy/vox_words/i.wav new file mode 100755 index 0000000..b98a48f Binary files /dev/null and b/ttstoy/vox_words/i.wav differ diff --git a/ttstoy/vox_words/idiot.wav b/ttstoy/vox_words/idiot.wav new file mode 100755 index 0000000..8c19f9d Binary files /dev/null and b/ttstoy/vox_words/idiot.wav differ diff --git a/ttstoy/vox_words/illegal.wav b/ttstoy/vox_words/illegal.wav new file mode 100755 index 0000000..6087e6b Binary files /dev/null and b/ttstoy/vox_words/illegal.wav differ diff --git a/ttstoy/vox_words/immediate.wav b/ttstoy/vox_words/immediate.wav new file mode 100755 index 0000000..19fab3c Binary files /dev/null and b/ttstoy/vox_words/immediate.wav differ diff --git a/ttstoy/vox_words/immediately.wav b/ttstoy/vox_words/immediately.wav new file mode 100755 index 0000000..f2716ed Binary files /dev/null and b/ttstoy/vox_words/immediately.wav differ diff --git a/ttstoy/vox_words/in.wav b/ttstoy/vox_words/in.wav new file mode 100755 index 0000000..e15bb9a Binary files /dev/null and b/ttstoy/vox_words/in.wav differ diff --git a/ttstoy/vox_words/inches.wav b/ttstoy/vox_words/inches.wav new file mode 100755 index 0000000..fc8e89c Binary files /dev/null and b/ttstoy/vox_words/inches.wav differ diff --git a/ttstoy/vox_words/india.wav b/ttstoy/vox_words/india.wav new file mode 100755 index 0000000..991f9af Binary files /dev/null and b/ttstoy/vox_words/india.wav differ diff --git a/ttstoy/vox_words/ing.wav b/ttstoy/vox_words/ing.wav new file mode 100755 index 0000000..0728905 Binary files /dev/null and b/ttstoy/vox_words/ing.wav differ diff --git a/ttstoy/vox_words/inoperative.wav b/ttstoy/vox_words/inoperative.wav new file mode 100755 index 0000000..6822cc5 Binary files /dev/null and b/ttstoy/vox_words/inoperative.wav differ diff --git a/ttstoy/vox_words/inside.wav b/ttstoy/vox_words/inside.wav new file mode 100755 index 0000000..03cb903 Binary files /dev/null and b/ttstoy/vox_words/inside.wav differ diff --git a/ttstoy/vox_words/inspection.wav b/ttstoy/vox_words/inspection.wav new file mode 100755 index 0000000..3aecb74 Binary files /dev/null and b/ttstoy/vox_words/inspection.wav differ diff --git a/ttstoy/vox_words/inspector.wav b/ttstoy/vox_words/inspector.wav new file mode 100755 index 0000000..06a54a3 Binary files /dev/null and b/ttstoy/vox_words/inspector.wav differ diff --git a/ttstoy/vox_words/interchange.wav b/ttstoy/vox_words/interchange.wav new file mode 100755 index 0000000..c781416 Binary files /dev/null and b/ttstoy/vox_words/interchange.wav differ diff --git a/ttstoy/vox_words/intruder.wav b/ttstoy/vox_words/intruder.wav new file mode 100755 index 0000000..a32958f Binary files /dev/null and b/ttstoy/vox_words/intruder.wav differ diff --git a/ttstoy/vox_words/invallid.wav b/ttstoy/vox_words/invallid.wav new file mode 100755 index 0000000..fbf3b4e Binary files /dev/null and b/ttstoy/vox_words/invallid.wav differ diff --git a/ttstoy/vox_words/invasion.wav b/ttstoy/vox_words/invasion.wav new file mode 100755 index 0000000..0024f33 Binary files /dev/null and b/ttstoy/vox_words/invasion.wav differ diff --git a/ttstoy/vox_words/is.wav b/ttstoy/vox_words/is.wav new file mode 100755 index 0000000..0eecb1a Binary files /dev/null and b/ttstoy/vox_words/is.wav differ diff --git a/ttstoy/vox_words/it.wav b/ttstoy/vox_words/it.wav new file mode 100755 index 0000000..1e96daf Binary files /dev/null and b/ttstoy/vox_words/it.wav differ diff --git a/ttstoy/vox_words/johnson.wav b/ttstoy/vox_words/johnson.wav new file mode 100755 index 0000000..1cef742 Binary files /dev/null and b/ttstoy/vox_words/johnson.wav differ diff --git a/ttstoy/vox_words/juliet.wav b/ttstoy/vox_words/juliet.wav new file mode 100755 index 0000000..88a7851 Binary files /dev/null and b/ttstoy/vox_words/juliet.wav differ diff --git a/ttstoy/vox_words/key.wav b/ttstoy/vox_words/key.wav new file mode 100755 index 0000000..ba42b39 Binary files /dev/null and b/ttstoy/vox_words/key.wav differ diff --git a/ttstoy/vox_words/kill.wav b/ttstoy/vox_words/kill.wav new file mode 100755 index 0000000..9ae66ac Binary files /dev/null and b/ttstoy/vox_words/kill.wav differ diff --git a/ttstoy/vox_words/kilo.wav b/ttstoy/vox_words/kilo.wav new file mode 100755 index 0000000..e30904c Binary files /dev/null and b/ttstoy/vox_words/kilo.wav differ diff --git a/ttstoy/vox_words/kit.wav b/ttstoy/vox_words/kit.wav new file mode 100755 index 0000000..b6cf53a Binary files /dev/null and b/ttstoy/vox_words/kit.wav differ diff --git a/ttstoy/vox_words/lab.wav b/ttstoy/vox_words/lab.wav new file mode 100755 index 0000000..8b2518f Binary files /dev/null and b/ttstoy/vox_words/lab.wav differ diff --git a/ttstoy/vox_words/lambda.wav b/ttstoy/vox_words/lambda.wav new file mode 100755 index 0000000..a238ac9 Binary files /dev/null and b/ttstoy/vox_words/lambda.wav differ diff --git a/ttstoy/vox_words/laser.wav b/ttstoy/vox_words/laser.wav new file mode 100755 index 0000000..7617aa6 Binary files /dev/null and b/ttstoy/vox_words/laser.wav differ diff --git a/ttstoy/vox_words/last.wav b/ttstoy/vox_words/last.wav new file mode 100755 index 0000000..458d13a Binary files /dev/null and b/ttstoy/vox_words/last.wav differ diff --git a/ttstoy/vox_words/launch.wav b/ttstoy/vox_words/launch.wav new file mode 100755 index 0000000..66fd222 Binary files /dev/null and b/ttstoy/vox_words/launch.wav differ diff --git a/ttstoy/vox_words/leak.wav b/ttstoy/vox_words/leak.wav new file mode 100755 index 0000000..0ec0fc7 Binary files /dev/null and b/ttstoy/vox_words/leak.wav differ diff --git a/ttstoy/vox_words/leave.wav b/ttstoy/vox_words/leave.wav new file mode 100755 index 0000000..ce45698 Binary files /dev/null and b/ttstoy/vox_words/leave.wav differ diff --git a/ttstoy/vox_words/left.wav b/ttstoy/vox_words/left.wav new file mode 100755 index 0000000..2621d07 Binary files /dev/null and b/ttstoy/vox_words/left.wav differ diff --git a/ttstoy/vox_words/legal.wav b/ttstoy/vox_words/legal.wav new file mode 100755 index 0000000..9280337 Binary files /dev/null and b/ttstoy/vox_words/legal.wav differ diff --git a/ttstoy/vox_words/level.wav b/ttstoy/vox_words/level.wav new file mode 100755 index 0000000..1f01010 Binary files /dev/null and b/ttstoy/vox_words/level.wav differ diff --git a/ttstoy/vox_words/lever.wav b/ttstoy/vox_words/lever.wav new file mode 100755 index 0000000..a863fdc Binary files /dev/null and b/ttstoy/vox_words/lever.wav differ diff --git a/ttstoy/vox_words/lie.wav b/ttstoy/vox_words/lie.wav new file mode 100755 index 0000000..01b8749 Binary files /dev/null and b/ttstoy/vox_words/lie.wav differ diff --git a/ttstoy/vox_words/lieutenant.wav b/ttstoy/vox_words/lieutenant.wav new file mode 100755 index 0000000..f749f27 Binary files /dev/null and b/ttstoy/vox_words/lieutenant.wav differ diff --git a/ttstoy/vox_words/life.wav b/ttstoy/vox_words/life.wav new file mode 100755 index 0000000..fd739b4 Binary files /dev/null and b/ttstoy/vox_words/life.wav differ diff --git a/ttstoy/vox_words/light.wav b/ttstoy/vox_words/light.wav new file mode 100755 index 0000000..69be049 Binary files /dev/null and b/ttstoy/vox_words/light.wav differ diff --git a/ttstoy/vox_words/lima.wav b/ttstoy/vox_words/lima.wav new file mode 100755 index 0000000..d0cf4d0 Binary files /dev/null and b/ttstoy/vox_words/lima.wav differ diff --git a/ttstoy/vox_words/liquid.wav b/ttstoy/vox_words/liquid.wav new file mode 100755 index 0000000..9f77a86 Binary files /dev/null and b/ttstoy/vox_words/liquid.wav differ diff --git a/ttstoy/vox_words/loading.wav b/ttstoy/vox_words/loading.wav new file mode 100755 index 0000000..f5404ef Binary files /dev/null and b/ttstoy/vox_words/loading.wav differ diff --git a/ttstoy/vox_words/locate.wav b/ttstoy/vox_words/locate.wav new file mode 100755 index 0000000..6b605a8 Binary files /dev/null and b/ttstoy/vox_words/locate.wav differ diff --git a/ttstoy/vox_words/located.wav b/ttstoy/vox_words/located.wav new file mode 100755 index 0000000..19fb575 Binary files /dev/null and b/ttstoy/vox_words/located.wav differ diff --git a/ttstoy/vox_words/location.wav b/ttstoy/vox_words/location.wav new file mode 100755 index 0000000..149b0ec Binary files /dev/null and b/ttstoy/vox_words/location.wav differ diff --git a/ttstoy/vox_words/lock.wav b/ttstoy/vox_words/lock.wav new file mode 100755 index 0000000..a62efba Binary files /dev/null and b/ttstoy/vox_words/lock.wav differ diff --git a/ttstoy/vox_words/locked.wav b/ttstoy/vox_words/locked.wav new file mode 100755 index 0000000..8d197f2 Binary files /dev/null and b/ttstoy/vox_words/locked.wav differ diff --git a/ttstoy/vox_words/locker.wav b/ttstoy/vox_words/locker.wav new file mode 100755 index 0000000..e6f9dfe Binary files /dev/null and b/ttstoy/vox_words/locker.wav differ diff --git a/ttstoy/vox_words/lockout.wav b/ttstoy/vox_words/lockout.wav new file mode 100755 index 0000000..b2cd0fb Binary files /dev/null and b/ttstoy/vox_words/lockout.wav differ diff --git a/ttstoy/vox_words/lower.wav b/ttstoy/vox_words/lower.wav new file mode 100755 index 0000000..a654c1d Binary files /dev/null and b/ttstoy/vox_words/lower.wav differ diff --git a/ttstoy/vox_words/lowest.wav b/ttstoy/vox_words/lowest.wav new file mode 100755 index 0000000..63fdaf9 Binary files /dev/null and b/ttstoy/vox_words/lowest.wav differ diff --git a/ttstoy/vox_words/magnetic.wav b/ttstoy/vox_words/magnetic.wav new file mode 100755 index 0000000..68deeea Binary files /dev/null and b/ttstoy/vox_words/magnetic.wav differ diff --git a/ttstoy/vox_words/main.wav b/ttstoy/vox_words/main.wav new file mode 100755 index 0000000..4657274 Binary files /dev/null and b/ttstoy/vox_words/main.wav differ diff --git a/ttstoy/vox_words/maintenance.wav b/ttstoy/vox_words/maintenance.wav new file mode 100755 index 0000000..ebca04c Binary files /dev/null and b/ttstoy/vox_words/maintenance.wav differ diff --git a/ttstoy/vox_words/malfunction.wav b/ttstoy/vox_words/malfunction.wav new file mode 100755 index 0000000..51d999b Binary files /dev/null and b/ttstoy/vox_words/malfunction.wav differ diff --git a/ttstoy/vox_words/man.wav b/ttstoy/vox_words/man.wav new file mode 100755 index 0000000..64525a4 Binary files /dev/null and b/ttstoy/vox_words/man.wav differ diff --git a/ttstoy/vox_words/mass.wav b/ttstoy/vox_words/mass.wav new file mode 100755 index 0000000..c782598 Binary files /dev/null and b/ttstoy/vox_words/mass.wav differ diff --git a/ttstoy/vox_words/materials.wav b/ttstoy/vox_words/materials.wav new file mode 100755 index 0000000..c983ae7 Binary files /dev/null and b/ttstoy/vox_words/materials.wav differ diff --git a/ttstoy/vox_words/maximum.wav b/ttstoy/vox_words/maximum.wav new file mode 100755 index 0000000..f1dbc80 Binary files /dev/null and b/ttstoy/vox_words/maximum.wav differ diff --git a/ttstoy/vox_words/may.wav b/ttstoy/vox_words/may.wav new file mode 100755 index 0000000..a238738 Binary files /dev/null and b/ttstoy/vox_words/may.wav differ diff --git a/ttstoy/vox_words/medical.wav b/ttstoy/vox_words/medical.wav new file mode 100755 index 0000000..ece7f0c Binary files /dev/null and b/ttstoy/vox_words/medical.wav differ diff --git a/ttstoy/vox_words/men.wav b/ttstoy/vox_words/men.wav new file mode 100755 index 0000000..594523d Binary files /dev/null and b/ttstoy/vox_words/men.wav differ diff --git a/ttstoy/vox_words/mercy.wav b/ttstoy/vox_words/mercy.wav new file mode 100755 index 0000000..8416b25 Binary files /dev/null and b/ttstoy/vox_words/mercy.wav differ diff --git a/ttstoy/vox_words/mesa.wav b/ttstoy/vox_words/mesa.wav new file mode 100755 index 0000000..fcb1d98 Binary files /dev/null and b/ttstoy/vox_words/mesa.wav differ diff --git a/ttstoy/vox_words/message.wav b/ttstoy/vox_words/message.wav new file mode 100755 index 0000000..b0b769d Binary files /dev/null and b/ttstoy/vox_words/message.wav differ diff --git a/ttstoy/vox_words/meter.wav b/ttstoy/vox_words/meter.wav new file mode 100755 index 0000000..ca063bd Binary files /dev/null and b/ttstoy/vox_words/meter.wav differ diff --git a/ttstoy/vox_words/micro.wav b/ttstoy/vox_words/micro.wav new file mode 100755 index 0000000..7869b97 Binary files /dev/null and b/ttstoy/vox_words/micro.wav differ diff --git a/ttstoy/vox_words/middle.wav b/ttstoy/vox_words/middle.wav new file mode 100755 index 0000000..b58a587 Binary files /dev/null and b/ttstoy/vox_words/middle.wav differ diff --git a/ttstoy/vox_words/mike.wav b/ttstoy/vox_words/mike.wav new file mode 100755 index 0000000..a008bde Binary files /dev/null and b/ttstoy/vox_words/mike.wav differ diff --git a/ttstoy/vox_words/miles.wav b/ttstoy/vox_words/miles.wav new file mode 100755 index 0000000..9b04a79 Binary files /dev/null and b/ttstoy/vox_words/miles.wav differ diff --git a/ttstoy/vox_words/military.wav b/ttstoy/vox_words/military.wav new file mode 100755 index 0000000..8387dc3 Binary files /dev/null and b/ttstoy/vox_words/military.wav differ diff --git a/ttstoy/vox_words/milli.wav b/ttstoy/vox_words/milli.wav new file mode 100755 index 0000000..cc0b3e4 Binary files /dev/null and b/ttstoy/vox_words/milli.wav differ diff --git a/ttstoy/vox_words/million.wav b/ttstoy/vox_words/million.wav new file mode 100755 index 0000000..6409eb0 Binary files /dev/null and b/ttstoy/vox_words/million.wav differ diff --git a/ttstoy/vox_words/minefield.wav b/ttstoy/vox_words/minefield.wav new file mode 100755 index 0000000..2af1c21 Binary files /dev/null and b/ttstoy/vox_words/minefield.wav differ diff --git a/ttstoy/vox_words/minimum.wav b/ttstoy/vox_words/minimum.wav new file mode 100755 index 0000000..ee184ce Binary files /dev/null and b/ttstoy/vox_words/minimum.wav differ diff --git a/ttstoy/vox_words/minutes.wav b/ttstoy/vox_words/minutes.wav new file mode 100755 index 0000000..2e9fb91 Binary files /dev/null and b/ttstoy/vox_words/minutes.wav differ diff --git a/ttstoy/vox_words/mister.wav b/ttstoy/vox_words/mister.wav new file mode 100755 index 0000000..27507cf Binary files /dev/null and b/ttstoy/vox_words/mister.wav differ diff --git a/ttstoy/vox_words/mode.wav b/ttstoy/vox_words/mode.wav new file mode 100755 index 0000000..e1060e9 Binary files /dev/null and b/ttstoy/vox_words/mode.wav differ diff --git a/ttstoy/vox_words/motor.wav b/ttstoy/vox_words/motor.wav new file mode 100755 index 0000000..7d1dbae Binary files /dev/null and b/ttstoy/vox_words/motor.wav differ diff --git a/ttstoy/vox_words/motorpool.wav b/ttstoy/vox_words/motorpool.wav new file mode 100755 index 0000000..ae5a6af Binary files /dev/null and b/ttstoy/vox_words/motorpool.wav differ diff --git a/ttstoy/vox_words/move.wav b/ttstoy/vox_words/move.wav new file mode 100755 index 0000000..1bc2172 Binary files /dev/null and b/ttstoy/vox_words/move.wav differ diff --git a/ttstoy/vox_words/must.wav b/ttstoy/vox_words/must.wav new file mode 100755 index 0000000..3e6e965 Binary files /dev/null and b/ttstoy/vox_words/must.wav differ diff --git a/ttstoy/vox_words/nearest.wav b/ttstoy/vox_words/nearest.wav new file mode 100755 index 0000000..3f84ee7 Binary files /dev/null and b/ttstoy/vox_words/nearest.wav differ diff --git a/ttstoy/vox_words/nice.wav b/ttstoy/vox_words/nice.wav new file mode 100755 index 0000000..8bd1406 Binary files /dev/null and b/ttstoy/vox_words/nice.wav differ diff --git a/ttstoy/vox_words/nine.wav b/ttstoy/vox_words/nine.wav new file mode 100755 index 0000000..b85745e Binary files /dev/null and b/ttstoy/vox_words/nine.wav differ diff --git a/ttstoy/vox_words/nineteen.wav b/ttstoy/vox_words/nineteen.wav new file mode 100755 index 0000000..c6da483 Binary files /dev/null and b/ttstoy/vox_words/nineteen.wav differ diff --git a/ttstoy/vox_words/ninety.wav b/ttstoy/vox_words/ninety.wav new file mode 100755 index 0000000..74e2e02 Binary files /dev/null and b/ttstoy/vox_words/ninety.wav differ diff --git a/ttstoy/vox_words/no.wav b/ttstoy/vox_words/no.wav new file mode 100755 index 0000000..ced1b66 Binary files /dev/null and b/ttstoy/vox_words/no.wav differ diff --git a/ttstoy/vox_words/nominal.wav b/ttstoy/vox_words/nominal.wav new file mode 100755 index 0000000..f065630 Binary files /dev/null and b/ttstoy/vox_words/nominal.wav differ diff --git a/ttstoy/vox_words/north.wav b/ttstoy/vox_words/north.wav new file mode 100755 index 0000000..4867fdc Binary files /dev/null and b/ttstoy/vox_words/north.wav differ diff --git a/ttstoy/vox_words/not.wav b/ttstoy/vox_words/not.wav new file mode 100755 index 0000000..626cd17 Binary files /dev/null and b/ttstoy/vox_words/not.wav differ diff --git a/ttstoy/vox_words/november.wav b/ttstoy/vox_words/november.wav new file mode 100755 index 0000000..212efe5 Binary files /dev/null and b/ttstoy/vox_words/november.wav differ diff --git a/ttstoy/vox_words/now.wav b/ttstoy/vox_words/now.wav new file mode 100755 index 0000000..0619087 Binary files /dev/null and b/ttstoy/vox_words/now.wav differ diff --git a/ttstoy/vox_words/number.wav b/ttstoy/vox_words/number.wav new file mode 100755 index 0000000..cfec10a Binary files /dev/null and b/ttstoy/vox_words/number.wav differ diff --git a/ttstoy/vox_words/objective.wav b/ttstoy/vox_words/objective.wav new file mode 100755 index 0000000..14ea571 Binary files /dev/null and b/ttstoy/vox_words/objective.wav differ diff --git a/ttstoy/vox_words/observation.wav b/ttstoy/vox_words/observation.wav new file mode 100755 index 0000000..9141789 Binary files /dev/null and b/ttstoy/vox_words/observation.wav differ diff --git a/ttstoy/vox_words/of.wav b/ttstoy/vox_words/of.wav new file mode 100755 index 0000000..7cf5427 Binary files /dev/null and b/ttstoy/vox_words/of.wav differ diff --git a/ttstoy/vox_words/officer.wav b/ttstoy/vox_words/officer.wav new file mode 100755 index 0000000..9017915 Binary files /dev/null and b/ttstoy/vox_words/officer.wav differ diff --git a/ttstoy/vox_words/ok.wav b/ttstoy/vox_words/ok.wav new file mode 100755 index 0000000..66011ae Binary files /dev/null and b/ttstoy/vox_words/ok.wav differ diff --git a/ttstoy/vox_words/on.wav b/ttstoy/vox_words/on.wav new file mode 100755 index 0000000..2b97a1f Binary files /dev/null and b/ttstoy/vox_words/on.wav differ diff --git a/ttstoy/vox_words/one.wav b/ttstoy/vox_words/one.wav new file mode 100755 index 0000000..57f2b3f Binary files /dev/null and b/ttstoy/vox_words/one.wav differ diff --git a/ttstoy/vox_words/open.wav b/ttstoy/vox_words/open.wav new file mode 100755 index 0000000..be98260 Binary files /dev/null and b/ttstoy/vox_words/open.wav differ diff --git a/ttstoy/vox_words/operating.wav b/ttstoy/vox_words/operating.wav new file mode 100755 index 0000000..b218069 Binary files /dev/null and b/ttstoy/vox_words/operating.wav differ diff --git a/ttstoy/vox_words/operations.wav b/ttstoy/vox_words/operations.wav new file mode 100755 index 0000000..37c9d75 Binary files /dev/null and b/ttstoy/vox_words/operations.wav differ diff --git a/ttstoy/vox_words/operative.wav b/ttstoy/vox_words/operative.wav new file mode 100755 index 0000000..70f9902 Binary files /dev/null and b/ttstoy/vox_words/operative.wav differ diff --git a/ttstoy/vox_words/option.wav b/ttstoy/vox_words/option.wav new file mode 100755 index 0000000..a5199b7 Binary files /dev/null and b/ttstoy/vox_words/option.wav differ diff --git a/ttstoy/vox_words/order.wav b/ttstoy/vox_words/order.wav new file mode 100755 index 0000000..c5a2548 Binary files /dev/null and b/ttstoy/vox_words/order.wav differ diff --git a/ttstoy/vox_words/organic.wav b/ttstoy/vox_words/organic.wav new file mode 100755 index 0000000..78dc879 Binary files /dev/null and b/ttstoy/vox_words/organic.wav differ diff --git a/ttstoy/vox_words/oscar.wav b/ttstoy/vox_words/oscar.wav new file mode 100755 index 0000000..32db239 Binary files /dev/null and b/ttstoy/vox_words/oscar.wav differ diff --git a/ttstoy/vox_words/out.wav b/ttstoy/vox_words/out.wav new file mode 100755 index 0000000..4aca4d7 Binary files /dev/null and b/ttstoy/vox_words/out.wav differ diff --git a/ttstoy/vox_words/outside.wav b/ttstoy/vox_words/outside.wav new file mode 100755 index 0000000..62993ea Binary files /dev/null and b/ttstoy/vox_words/outside.wav differ diff --git a/ttstoy/vox_words/over.wav b/ttstoy/vox_words/over.wav new file mode 100755 index 0000000..8068c74 Binary files /dev/null and b/ttstoy/vox_words/over.wav differ diff --git a/ttstoy/vox_words/overload.wav b/ttstoy/vox_words/overload.wav new file mode 100755 index 0000000..d932cb3 Binary files /dev/null and b/ttstoy/vox_words/overload.wav differ diff --git a/ttstoy/vox_words/override.wav b/ttstoy/vox_words/override.wav new file mode 100755 index 0000000..132667e Binary files /dev/null and b/ttstoy/vox_words/override.wav differ diff --git a/ttstoy/vox_words/pacify.wav b/ttstoy/vox_words/pacify.wav new file mode 100755 index 0000000..6a59619 Binary files /dev/null and b/ttstoy/vox_words/pacify.wav differ diff --git a/ttstoy/vox_words/pain.wav b/ttstoy/vox_words/pain.wav new file mode 100755 index 0000000..bdb30dd Binary files /dev/null and b/ttstoy/vox_words/pain.wav differ diff --git a/ttstoy/vox_words/pal.wav b/ttstoy/vox_words/pal.wav new file mode 100755 index 0000000..28eb17c Binary files /dev/null and b/ttstoy/vox_words/pal.wav differ diff --git a/ttstoy/vox_words/panel.wav b/ttstoy/vox_words/panel.wav new file mode 100755 index 0000000..ce1f22f Binary files /dev/null and b/ttstoy/vox_words/panel.wav differ diff --git a/ttstoy/vox_words/percent.wav b/ttstoy/vox_words/percent.wav new file mode 100755 index 0000000..9e77586 Binary files /dev/null and b/ttstoy/vox_words/percent.wav differ diff --git a/ttstoy/vox_words/perimeter.wav b/ttstoy/vox_words/perimeter.wav new file mode 100755 index 0000000..91a8d95 Binary files /dev/null and b/ttstoy/vox_words/perimeter.wav differ diff --git a/ttstoy/vox_words/period.wav b/ttstoy/vox_words/period.wav new file mode 100755 index 0000000..7eeb1eb Binary files /dev/null and b/ttstoy/vox_words/period.wav differ diff --git a/ttstoy/vox_words/permitted.wav b/ttstoy/vox_words/permitted.wav new file mode 100755 index 0000000..015d10e Binary files /dev/null and b/ttstoy/vox_words/permitted.wav differ diff --git a/ttstoy/vox_words/personnel.wav b/ttstoy/vox_words/personnel.wav new file mode 100755 index 0000000..665a321 Binary files /dev/null and b/ttstoy/vox_words/personnel.wav differ diff --git a/ttstoy/vox_words/pipe.wav b/ttstoy/vox_words/pipe.wav new file mode 100755 index 0000000..53abe7d Binary files /dev/null and b/ttstoy/vox_words/pipe.wav differ diff --git a/ttstoy/vox_words/plant.wav b/ttstoy/vox_words/plant.wav new file mode 100755 index 0000000..8fc84c6 Binary files /dev/null and b/ttstoy/vox_words/plant.wav differ diff --git a/ttstoy/vox_words/platform.wav b/ttstoy/vox_words/platform.wav new file mode 100755 index 0000000..72ca2c1 Binary files /dev/null and b/ttstoy/vox_words/platform.wav differ diff --git a/ttstoy/vox_words/please.wav b/ttstoy/vox_words/please.wav new file mode 100755 index 0000000..4dc346e Binary files /dev/null and b/ttstoy/vox_words/please.wav differ diff --git a/ttstoy/vox_words/point.wav b/ttstoy/vox_words/point.wav new file mode 100755 index 0000000..26e4ca5 Binary files /dev/null and b/ttstoy/vox_words/point.wav differ diff --git a/ttstoy/vox_words/portal.wav b/ttstoy/vox_words/portal.wav new file mode 100755 index 0000000..2f093e9 Binary files /dev/null and b/ttstoy/vox_words/portal.wav differ diff --git a/ttstoy/vox_words/power.wav b/ttstoy/vox_words/power.wav new file mode 100755 index 0000000..3256e3e Binary files /dev/null and b/ttstoy/vox_words/power.wav differ diff --git a/ttstoy/vox_words/presence.wav b/ttstoy/vox_words/presence.wav new file mode 100755 index 0000000..c7a3b8f Binary files /dev/null and b/ttstoy/vox_words/presence.wav differ diff --git a/ttstoy/vox_words/press.wav b/ttstoy/vox_words/press.wav new file mode 100755 index 0000000..138ef0f Binary files /dev/null and b/ttstoy/vox_words/press.wav differ diff --git a/ttstoy/vox_words/primary.wav b/ttstoy/vox_words/primary.wav new file mode 100755 index 0000000..ed107ab Binary files /dev/null and b/ttstoy/vox_words/primary.wav differ diff --git a/ttstoy/vox_words/proceed.wav b/ttstoy/vox_words/proceed.wav new file mode 100755 index 0000000..ec5ba2f Binary files /dev/null and b/ttstoy/vox_words/proceed.wav differ diff --git a/ttstoy/vox_words/processing.wav b/ttstoy/vox_words/processing.wav new file mode 100755 index 0000000..3470d8e Binary files /dev/null and b/ttstoy/vox_words/processing.wav differ diff --git a/ttstoy/vox_words/progress.wav b/ttstoy/vox_words/progress.wav new file mode 100755 index 0000000..ab2ef19 Binary files /dev/null and b/ttstoy/vox_words/progress.wav differ diff --git a/ttstoy/vox_words/proper.wav b/ttstoy/vox_words/proper.wav new file mode 100755 index 0000000..fb799ae Binary files /dev/null and b/ttstoy/vox_words/proper.wav differ diff --git a/ttstoy/vox_words/propulsion.wav b/ttstoy/vox_words/propulsion.wav new file mode 100755 index 0000000..cc60270 Binary files /dev/null and b/ttstoy/vox_words/propulsion.wav differ diff --git a/ttstoy/vox_words/prosecute.wav b/ttstoy/vox_words/prosecute.wav new file mode 100755 index 0000000..9d76be8 Binary files /dev/null and b/ttstoy/vox_words/prosecute.wav differ diff --git a/ttstoy/vox_words/protective.wav b/ttstoy/vox_words/protective.wav new file mode 100755 index 0000000..be17714 Binary files /dev/null and b/ttstoy/vox_words/protective.wav differ diff --git a/ttstoy/vox_words/push.wav b/ttstoy/vox_words/push.wav new file mode 100755 index 0000000..db91cb1 Binary files /dev/null and b/ttstoy/vox_words/push.wav differ diff --git a/ttstoy/vox_words/quantum.wav b/ttstoy/vox_words/quantum.wav new file mode 100755 index 0000000..3e5ee45 Binary files /dev/null and b/ttstoy/vox_words/quantum.wav differ diff --git a/ttstoy/vox_words/quebec.wav b/ttstoy/vox_words/quebec.wav new file mode 100755 index 0000000..6323a99 Binary files /dev/null and b/ttstoy/vox_words/quebec.wav differ diff --git a/ttstoy/vox_words/question.wav b/ttstoy/vox_words/question.wav new file mode 100755 index 0000000..cf6de8d Binary files /dev/null and b/ttstoy/vox_words/question.wav differ diff --git a/ttstoy/vox_words/questioning.wav b/ttstoy/vox_words/questioning.wav new file mode 100755 index 0000000..51f0848 Binary files /dev/null and b/ttstoy/vox_words/questioning.wav differ diff --git a/ttstoy/vox_words/quick.wav b/ttstoy/vox_words/quick.wav new file mode 100755 index 0000000..82c6c49 Binary files /dev/null and b/ttstoy/vox_words/quick.wav differ diff --git a/ttstoy/vox_words/quit.wav b/ttstoy/vox_words/quit.wav new file mode 100755 index 0000000..1ee00b8 Binary files /dev/null and b/ttstoy/vox_words/quit.wav differ diff --git a/ttstoy/vox_words/radiation.wav b/ttstoy/vox_words/radiation.wav new file mode 100755 index 0000000..5551f24 Binary files /dev/null and b/ttstoy/vox_words/radiation.wav differ diff --git a/ttstoy/vox_words/radioactive.wav b/ttstoy/vox_words/radioactive.wav new file mode 100755 index 0000000..84b3b4a Binary files /dev/null and b/ttstoy/vox_words/radioactive.wav differ diff --git a/ttstoy/vox_words/rads.wav b/ttstoy/vox_words/rads.wav new file mode 100755 index 0000000..13802ea Binary files /dev/null and b/ttstoy/vox_words/rads.wav differ diff --git a/ttstoy/vox_words/rapid.wav b/ttstoy/vox_words/rapid.wav new file mode 100755 index 0000000..fdfdd10 Binary files /dev/null and b/ttstoy/vox_words/rapid.wav differ diff --git a/ttstoy/vox_words/reach.wav b/ttstoy/vox_words/reach.wav new file mode 100755 index 0000000..5b2ec24 Binary files /dev/null and b/ttstoy/vox_words/reach.wav differ diff --git a/ttstoy/vox_words/reached.wav b/ttstoy/vox_words/reached.wav new file mode 100755 index 0000000..22bd766 Binary files /dev/null and b/ttstoy/vox_words/reached.wav differ diff --git a/ttstoy/vox_words/reactor.wav b/ttstoy/vox_words/reactor.wav new file mode 100755 index 0000000..c8fb909 Binary files /dev/null and b/ttstoy/vox_words/reactor.wav differ diff --git a/ttstoy/vox_words/red.wav b/ttstoy/vox_words/red.wav new file mode 100755 index 0000000..777e546 Binary files /dev/null and b/ttstoy/vox_words/red.wav differ diff --git a/ttstoy/vox_words/relay.wav b/ttstoy/vox_words/relay.wav new file mode 100755 index 0000000..bef9fb0 Binary files /dev/null and b/ttstoy/vox_words/relay.wav differ diff --git a/ttstoy/vox_words/released.wav b/ttstoy/vox_words/released.wav new file mode 100755 index 0000000..ba56d6d Binary files /dev/null and b/ttstoy/vox_words/released.wav differ diff --git a/ttstoy/vox_words/remaining.wav b/ttstoy/vox_words/remaining.wav new file mode 100755 index 0000000..0c393ee Binary files /dev/null and b/ttstoy/vox_words/remaining.wav differ diff --git a/ttstoy/vox_words/renegade.wav b/ttstoy/vox_words/renegade.wav new file mode 100755 index 0000000..0dc4518 Binary files /dev/null and b/ttstoy/vox_words/renegade.wav differ diff --git a/ttstoy/vox_words/repair.wav b/ttstoy/vox_words/repair.wav new file mode 100755 index 0000000..be2fe54 Binary files /dev/null and b/ttstoy/vox_words/repair.wav differ diff --git a/ttstoy/vox_words/report.wav b/ttstoy/vox_words/report.wav new file mode 100755 index 0000000..685c387 Binary files /dev/null and b/ttstoy/vox_words/report.wav differ diff --git a/ttstoy/vox_words/reports.wav b/ttstoy/vox_words/reports.wav new file mode 100755 index 0000000..19e8f04 Binary files /dev/null and b/ttstoy/vox_words/reports.wav differ diff --git a/ttstoy/vox_words/required.wav b/ttstoy/vox_words/required.wav new file mode 100755 index 0000000..c5bbf85 Binary files /dev/null and b/ttstoy/vox_words/required.wav differ diff --git a/ttstoy/vox_words/research.wav b/ttstoy/vox_words/research.wav new file mode 100755 index 0000000..d738586 Binary files /dev/null and b/ttstoy/vox_words/research.wav differ diff --git a/ttstoy/vox_words/resevoir.wav b/ttstoy/vox_words/resevoir.wav new file mode 100755 index 0000000..7b67635 Binary files /dev/null and b/ttstoy/vox_words/resevoir.wav differ diff --git a/ttstoy/vox_words/resistance.wav b/ttstoy/vox_words/resistance.wav new file mode 100755 index 0000000..1f3f90b Binary files /dev/null and b/ttstoy/vox_words/resistance.wav differ diff --git a/ttstoy/vox_words/right.wav b/ttstoy/vox_words/right.wav new file mode 100755 index 0000000..0c920c8 Binary files /dev/null and b/ttstoy/vox_words/right.wav differ diff --git a/ttstoy/vox_words/rocket.wav b/ttstoy/vox_words/rocket.wav new file mode 100755 index 0000000..57d7e6a Binary files /dev/null and b/ttstoy/vox_words/rocket.wav differ diff --git a/ttstoy/vox_words/roger.wav b/ttstoy/vox_words/roger.wav new file mode 100755 index 0000000..426014b Binary files /dev/null and b/ttstoy/vox_words/roger.wav differ diff --git a/ttstoy/vox_words/romeo.wav b/ttstoy/vox_words/romeo.wav new file mode 100755 index 0000000..4259092 Binary files /dev/null and b/ttstoy/vox_words/romeo.wav differ diff --git a/ttstoy/vox_words/room.wav b/ttstoy/vox_words/room.wav new file mode 100755 index 0000000..da12c55 Binary files /dev/null and b/ttstoy/vox_words/room.wav differ diff --git a/ttstoy/vox_words/round.wav b/ttstoy/vox_words/round.wav new file mode 100755 index 0000000..76d3038 Binary files /dev/null and b/ttstoy/vox_words/round.wav differ diff --git a/ttstoy/vox_words/run.wav b/ttstoy/vox_words/run.wav new file mode 100755 index 0000000..f2baec6 Binary files /dev/null and b/ttstoy/vox_words/run.wav differ diff --git a/ttstoy/vox_words/safe.wav b/ttstoy/vox_words/safe.wav new file mode 100755 index 0000000..65d52fb Binary files /dev/null and b/ttstoy/vox_words/safe.wav differ diff --git a/ttstoy/vox_words/safety.wav b/ttstoy/vox_words/safety.wav new file mode 100755 index 0000000..b24e895 Binary files /dev/null and b/ttstoy/vox_words/safety.wav differ diff --git a/ttstoy/vox_words/sargeant.wav b/ttstoy/vox_words/sargeant.wav new file mode 100755 index 0000000..e26c6a3 Binary files /dev/null and b/ttstoy/vox_words/sargeant.wav differ diff --git a/ttstoy/vox_words/satellite.wav b/ttstoy/vox_words/satellite.wav new file mode 100755 index 0000000..45187b8 Binary files /dev/null and b/ttstoy/vox_words/satellite.wav differ diff --git a/ttstoy/vox_words/save.wav b/ttstoy/vox_words/save.wav new file mode 100755 index 0000000..db09e27 Binary files /dev/null and b/ttstoy/vox_words/save.wav differ diff --git a/ttstoy/vox_words/science.wav b/ttstoy/vox_words/science.wav new file mode 100755 index 0000000..cb1f2d7 Binary files /dev/null and b/ttstoy/vox_words/science.wav differ diff --git a/ttstoy/vox_words/scream.wav b/ttstoy/vox_words/scream.wav new file mode 100755 index 0000000..66afd09 Binary files /dev/null and b/ttstoy/vox_words/scream.wav differ diff --git a/ttstoy/vox_words/screen.wav b/ttstoy/vox_words/screen.wav new file mode 100755 index 0000000..bb37526 Binary files /dev/null and b/ttstoy/vox_words/screen.wav differ diff --git a/ttstoy/vox_words/search.wav b/ttstoy/vox_words/search.wav new file mode 100755 index 0000000..023072d Binary files /dev/null and b/ttstoy/vox_words/search.wav differ diff --git a/ttstoy/vox_words/second.wav b/ttstoy/vox_words/second.wav new file mode 100755 index 0000000..dbed0ca Binary files /dev/null and b/ttstoy/vox_words/second.wav differ diff --git a/ttstoy/vox_words/secondary.wav b/ttstoy/vox_words/secondary.wav new file mode 100755 index 0000000..af29ac2 Binary files /dev/null and b/ttstoy/vox_words/secondary.wav differ diff --git a/ttstoy/vox_words/seconds.wav b/ttstoy/vox_words/seconds.wav new file mode 100755 index 0000000..a631ea1 Binary files /dev/null and b/ttstoy/vox_words/seconds.wav differ diff --git a/ttstoy/vox_words/sector.wav b/ttstoy/vox_words/sector.wav new file mode 100755 index 0000000..2318ccf Binary files /dev/null and b/ttstoy/vox_words/sector.wav differ diff --git a/ttstoy/vox_words/secure.wav b/ttstoy/vox_words/secure.wav new file mode 100755 index 0000000..17fe622 Binary files /dev/null and b/ttstoy/vox_words/secure.wav differ diff --git a/ttstoy/vox_words/secured.wav b/ttstoy/vox_words/secured.wav new file mode 100755 index 0000000..8007a48 Binary files /dev/null and b/ttstoy/vox_words/secured.wav differ diff --git a/ttstoy/vox_words/security.wav b/ttstoy/vox_words/security.wav new file mode 100755 index 0000000..16435fd Binary files /dev/null and b/ttstoy/vox_words/security.wav differ diff --git a/ttstoy/vox_words/select.wav b/ttstoy/vox_words/select.wav new file mode 100755 index 0000000..9b89d52 Binary files /dev/null and b/ttstoy/vox_words/select.wav differ diff --git a/ttstoy/vox_words/selected.wav b/ttstoy/vox_words/selected.wav new file mode 100755 index 0000000..d4124f5 Binary files /dev/null and b/ttstoy/vox_words/selected.wav differ diff --git a/ttstoy/vox_words/service.wav b/ttstoy/vox_words/service.wav new file mode 100755 index 0000000..3f6957d Binary files /dev/null and b/ttstoy/vox_words/service.wav differ diff --git a/ttstoy/vox_words/seven.wav b/ttstoy/vox_words/seven.wav new file mode 100755 index 0000000..eca99c1 Binary files /dev/null and b/ttstoy/vox_words/seven.wav differ diff --git a/ttstoy/vox_words/seventeen.wav b/ttstoy/vox_words/seventeen.wav new file mode 100755 index 0000000..9526b8a Binary files /dev/null and b/ttstoy/vox_words/seventeen.wav differ diff --git a/ttstoy/vox_words/seventy.wav b/ttstoy/vox_words/seventy.wav new file mode 100755 index 0000000..1b45ce6 Binary files /dev/null and b/ttstoy/vox_words/seventy.wav differ diff --git a/ttstoy/vox_words/severe.wav b/ttstoy/vox_words/severe.wav new file mode 100755 index 0000000..fed84ee Binary files /dev/null and b/ttstoy/vox_words/severe.wav differ diff --git a/ttstoy/vox_words/sewage.wav b/ttstoy/vox_words/sewage.wav new file mode 100755 index 0000000..6097896 Binary files /dev/null and b/ttstoy/vox_words/sewage.wav differ diff --git a/ttstoy/vox_words/sewer.wav b/ttstoy/vox_words/sewer.wav new file mode 100755 index 0000000..5071a1d Binary files /dev/null and b/ttstoy/vox_words/sewer.wav differ diff --git a/ttstoy/vox_words/shield.wav b/ttstoy/vox_words/shield.wav new file mode 100755 index 0000000..0ab01e0 Binary files /dev/null and b/ttstoy/vox_words/shield.wav differ diff --git a/ttstoy/vox_words/shipment.wav b/ttstoy/vox_words/shipment.wav new file mode 100755 index 0000000..9b9014c Binary files /dev/null and b/ttstoy/vox_words/shipment.wav differ diff --git a/ttstoy/vox_words/shock.wav b/ttstoy/vox_words/shock.wav new file mode 100755 index 0000000..d4451bd Binary files /dev/null and b/ttstoy/vox_words/shock.wav differ diff --git a/ttstoy/vox_words/shoot.wav b/ttstoy/vox_words/shoot.wav new file mode 100755 index 0000000..5661ceb Binary files /dev/null and b/ttstoy/vox_words/shoot.wav differ diff --git a/ttstoy/vox_words/shower.wav b/ttstoy/vox_words/shower.wav new file mode 100755 index 0000000..c32a9e4 Binary files /dev/null and b/ttstoy/vox_words/shower.wav differ diff --git a/ttstoy/vox_words/shut.wav b/ttstoy/vox_words/shut.wav new file mode 100755 index 0000000..6f68340 Binary files /dev/null and b/ttstoy/vox_words/shut.wav differ diff --git a/ttstoy/vox_words/side.wav b/ttstoy/vox_words/side.wav new file mode 100755 index 0000000..4f4091a Binary files /dev/null and b/ttstoy/vox_words/side.wav differ diff --git a/ttstoy/vox_words/sierra.wav b/ttstoy/vox_words/sierra.wav new file mode 100755 index 0000000..0e52a6e Binary files /dev/null and b/ttstoy/vox_words/sierra.wav differ diff --git a/ttstoy/vox_words/sight.wav b/ttstoy/vox_words/sight.wav new file mode 100755 index 0000000..5d479ca Binary files /dev/null and b/ttstoy/vox_words/sight.wav differ diff --git a/ttstoy/vox_words/silo.wav b/ttstoy/vox_words/silo.wav new file mode 100755 index 0000000..daf9f28 Binary files /dev/null and b/ttstoy/vox_words/silo.wav differ diff --git a/ttstoy/vox_words/six.wav b/ttstoy/vox_words/six.wav new file mode 100755 index 0000000..aa6bb63 Binary files /dev/null and b/ttstoy/vox_words/six.wav differ diff --git a/ttstoy/vox_words/sixteen.wav b/ttstoy/vox_words/sixteen.wav new file mode 100755 index 0000000..6f2db2a Binary files /dev/null and b/ttstoy/vox_words/sixteen.wav differ diff --git a/ttstoy/vox_words/sixty.wav b/ttstoy/vox_words/sixty.wav new file mode 100755 index 0000000..19610c7 Binary files /dev/null and b/ttstoy/vox_words/sixty.wav differ diff --git a/ttstoy/vox_words/slime.wav b/ttstoy/vox_words/slime.wav new file mode 100755 index 0000000..4d1d3cf Binary files /dev/null and b/ttstoy/vox_words/slime.wav differ diff --git a/ttstoy/vox_words/slow.wav b/ttstoy/vox_words/slow.wav new file mode 100755 index 0000000..75101b7 Binary files /dev/null and b/ttstoy/vox_words/slow.wav differ diff --git a/ttstoy/vox_words/soldier.wav b/ttstoy/vox_words/soldier.wav new file mode 100755 index 0000000..c84ce20 Binary files /dev/null and b/ttstoy/vox_words/soldier.wav differ diff --git a/ttstoy/vox_words/some.wav b/ttstoy/vox_words/some.wav new file mode 100755 index 0000000..c4ba34e Binary files /dev/null and b/ttstoy/vox_words/some.wav differ diff --git a/ttstoy/vox_words/someone.wav b/ttstoy/vox_words/someone.wav new file mode 100755 index 0000000..6f6183e Binary files /dev/null and b/ttstoy/vox_words/someone.wav differ diff --git a/ttstoy/vox_words/something.wav b/ttstoy/vox_words/something.wav new file mode 100755 index 0000000..4601140 Binary files /dev/null and b/ttstoy/vox_words/something.wav differ diff --git a/ttstoy/vox_words/son.wav b/ttstoy/vox_words/son.wav new file mode 100755 index 0000000..f234951 Binary files /dev/null and b/ttstoy/vox_words/son.wav differ diff --git a/ttstoy/vox_words/sorry.wav b/ttstoy/vox_words/sorry.wav new file mode 100755 index 0000000..4c0725b Binary files /dev/null and b/ttstoy/vox_words/sorry.wav differ diff --git a/ttstoy/vox_words/south.wav b/ttstoy/vox_words/south.wav new file mode 100755 index 0000000..3a5ba00 Binary files /dev/null and b/ttstoy/vox_words/south.wav differ diff --git a/ttstoy/vox_words/squad.wav b/ttstoy/vox_words/squad.wav new file mode 100755 index 0000000..71739d1 Binary files /dev/null and b/ttstoy/vox_words/squad.wav differ diff --git a/ttstoy/vox_words/square.wav b/ttstoy/vox_words/square.wav new file mode 100755 index 0000000..f08f73a Binary files /dev/null and b/ttstoy/vox_words/square.wav differ diff --git a/ttstoy/vox_words/stairway.wav b/ttstoy/vox_words/stairway.wav new file mode 100755 index 0000000..ab84571 Binary files /dev/null and b/ttstoy/vox_words/stairway.wav differ diff --git a/ttstoy/vox_words/status.wav b/ttstoy/vox_words/status.wav new file mode 100755 index 0000000..42fe8b7 Binary files /dev/null and b/ttstoy/vox_words/status.wav differ diff --git a/ttstoy/vox_words/sterile.wav b/ttstoy/vox_words/sterile.wav new file mode 100755 index 0000000..d957532 Binary files /dev/null and b/ttstoy/vox_words/sterile.wav differ diff --git a/ttstoy/vox_words/sterilization.wav b/ttstoy/vox_words/sterilization.wav new file mode 100755 index 0000000..f532403 Binary files /dev/null and b/ttstoy/vox_words/sterilization.wav differ diff --git a/ttstoy/vox_words/storage.wav b/ttstoy/vox_words/storage.wav new file mode 100755 index 0000000..18954a4 Binary files /dev/null and b/ttstoy/vox_words/storage.wav differ diff --git a/ttstoy/vox_words/sub.wav b/ttstoy/vox_words/sub.wav new file mode 100755 index 0000000..d25510d Binary files /dev/null and b/ttstoy/vox_words/sub.wav differ diff --git a/ttstoy/vox_words/subsurface.wav b/ttstoy/vox_words/subsurface.wav new file mode 100755 index 0000000..cb19be3 Binary files /dev/null and b/ttstoy/vox_words/subsurface.wav differ diff --git a/ttstoy/vox_words/sudden.wav b/ttstoy/vox_words/sudden.wav new file mode 100755 index 0000000..b5eb408 Binary files /dev/null and b/ttstoy/vox_words/sudden.wav differ diff --git a/ttstoy/vox_words/suit.wav b/ttstoy/vox_words/suit.wav new file mode 100755 index 0000000..d099c95 Binary files /dev/null and b/ttstoy/vox_words/suit.wav differ diff --git a/ttstoy/vox_words/superconducting.wav b/ttstoy/vox_words/superconducting.wav new file mode 100755 index 0000000..ad4fcc8 Binary files /dev/null and b/ttstoy/vox_words/superconducting.wav differ diff --git a/ttstoy/vox_words/supercooled.wav b/ttstoy/vox_words/supercooled.wav new file mode 100755 index 0000000..936562e Binary files /dev/null and b/ttstoy/vox_words/supercooled.wav differ diff --git a/ttstoy/vox_words/supply.wav b/ttstoy/vox_words/supply.wav new file mode 100755 index 0000000..4b33c8e Binary files /dev/null and b/ttstoy/vox_words/supply.wav differ diff --git a/ttstoy/vox_words/surface.wav b/ttstoy/vox_words/surface.wav new file mode 100755 index 0000000..0b02d73 Binary files /dev/null and b/ttstoy/vox_words/surface.wav differ diff --git a/ttstoy/vox_words/surrender.wav b/ttstoy/vox_words/surrender.wav new file mode 100755 index 0000000..f0a17b3 Binary files /dev/null and b/ttstoy/vox_words/surrender.wav differ diff --git a/ttstoy/vox_words/surround.wav b/ttstoy/vox_words/surround.wav new file mode 100755 index 0000000..c9afd86 Binary files /dev/null and b/ttstoy/vox_words/surround.wav differ diff --git a/ttstoy/vox_words/surrounded.wav b/ttstoy/vox_words/surrounded.wav new file mode 100755 index 0000000..3cbf513 Binary files /dev/null and b/ttstoy/vox_words/surrounded.wav differ diff --git a/ttstoy/vox_words/switch.wav b/ttstoy/vox_words/switch.wav new file mode 100755 index 0000000..f5ffcc7 Binary files /dev/null and b/ttstoy/vox_words/switch.wav differ diff --git a/ttstoy/vox_words/system.wav b/ttstoy/vox_words/system.wav new file mode 100755 index 0000000..078727a Binary files /dev/null and b/ttstoy/vox_words/system.wav differ diff --git a/ttstoy/vox_words/systems.wav b/ttstoy/vox_words/systems.wav new file mode 100755 index 0000000..c880250 Binary files /dev/null and b/ttstoy/vox_words/systems.wav differ diff --git a/ttstoy/vox_words/tactical.wav b/ttstoy/vox_words/tactical.wav new file mode 100755 index 0000000..6b1aca6 Binary files /dev/null and b/ttstoy/vox_words/tactical.wav differ diff --git a/ttstoy/vox_words/take.wav b/ttstoy/vox_words/take.wav new file mode 100755 index 0000000..70644f5 Binary files /dev/null and b/ttstoy/vox_words/take.wav differ diff --git a/ttstoy/vox_words/talk.wav b/ttstoy/vox_words/talk.wav new file mode 100755 index 0000000..3ff83c4 Binary files /dev/null and b/ttstoy/vox_words/talk.wav differ diff --git a/ttstoy/vox_words/tango.wav b/ttstoy/vox_words/tango.wav new file mode 100755 index 0000000..37b6f4f Binary files /dev/null and b/ttstoy/vox_words/tango.wav differ diff --git a/ttstoy/vox_words/tank.wav b/ttstoy/vox_words/tank.wav new file mode 100755 index 0000000..9608b0d Binary files /dev/null and b/ttstoy/vox_words/tank.wav differ diff --git a/ttstoy/vox_words/target.wav b/ttstoy/vox_words/target.wav new file mode 100755 index 0000000..bd00414 Binary files /dev/null and b/ttstoy/vox_words/target.wav differ diff --git a/ttstoy/vox_words/team.wav b/ttstoy/vox_words/team.wav new file mode 100755 index 0000000..d0e47d4 Binary files /dev/null and b/ttstoy/vox_words/team.wav differ diff --git a/ttstoy/vox_words/temperature.wav b/ttstoy/vox_words/temperature.wav new file mode 100755 index 0000000..e9b51da Binary files /dev/null and b/ttstoy/vox_words/temperature.wav differ diff --git a/ttstoy/vox_words/temporal.wav b/ttstoy/vox_words/temporal.wav new file mode 100755 index 0000000..d0398a0 Binary files /dev/null and b/ttstoy/vox_words/temporal.wav differ diff --git a/ttstoy/vox_words/ten.wav b/ttstoy/vox_words/ten.wav new file mode 100755 index 0000000..fd0f353 Binary files /dev/null and b/ttstoy/vox_words/ten.wav differ diff --git a/ttstoy/vox_words/terminal.wav b/ttstoy/vox_words/terminal.wav new file mode 100755 index 0000000..5ffe391 Binary files /dev/null and b/ttstoy/vox_words/terminal.wav differ diff --git a/ttstoy/vox_words/terminated.wav b/ttstoy/vox_words/terminated.wav new file mode 100755 index 0000000..1c972f0 Binary files /dev/null and b/ttstoy/vox_words/terminated.wav differ diff --git a/ttstoy/vox_words/termination.wav b/ttstoy/vox_words/termination.wav new file mode 100755 index 0000000..f093cd3 Binary files /dev/null and b/ttstoy/vox_words/termination.wav differ diff --git a/ttstoy/vox_words/test.wav b/ttstoy/vox_words/test.wav new file mode 100755 index 0000000..f7c9b46 Binary files /dev/null and b/ttstoy/vox_words/test.wav differ diff --git a/ttstoy/vox_words/that.wav b/ttstoy/vox_words/that.wav new file mode 100755 index 0000000..f9e8c92 Binary files /dev/null and b/ttstoy/vox_words/that.wav differ diff --git a/ttstoy/vox_words/the.wav b/ttstoy/vox_words/the.wav new file mode 100755 index 0000000..3a07ea3 Binary files /dev/null and b/ttstoy/vox_words/the.wav differ diff --git a/ttstoy/vox_words/then.wav b/ttstoy/vox_words/then.wav new file mode 100755 index 0000000..1c2ec53 Binary files /dev/null and b/ttstoy/vox_words/then.wav differ diff --git a/ttstoy/vox_words/there.wav b/ttstoy/vox_words/there.wav new file mode 100755 index 0000000..0e25880 Binary files /dev/null and b/ttstoy/vox_words/there.wav differ diff --git a/ttstoy/vox_words/third.wav b/ttstoy/vox_words/third.wav new file mode 100755 index 0000000..962f2f9 Binary files /dev/null and b/ttstoy/vox_words/third.wav differ diff --git a/ttstoy/vox_words/thirteen.wav b/ttstoy/vox_words/thirteen.wav new file mode 100755 index 0000000..1c392cf Binary files /dev/null and b/ttstoy/vox_words/thirteen.wav differ diff --git a/ttstoy/vox_words/thirty.wav b/ttstoy/vox_words/thirty.wav new file mode 100755 index 0000000..d7a34d4 Binary files /dev/null and b/ttstoy/vox_words/thirty.wav differ diff --git a/ttstoy/vox_words/this.wav b/ttstoy/vox_words/this.wav new file mode 100755 index 0000000..b8f51ea Binary files /dev/null and b/ttstoy/vox_words/this.wav differ diff --git a/ttstoy/vox_words/those.wav b/ttstoy/vox_words/those.wav new file mode 100755 index 0000000..ef7db90 Binary files /dev/null and b/ttstoy/vox_words/those.wav differ diff --git a/ttstoy/vox_words/thousand.wav b/ttstoy/vox_words/thousand.wav new file mode 100755 index 0000000..b1f9df6 Binary files /dev/null and b/ttstoy/vox_words/thousand.wav differ diff --git a/ttstoy/vox_words/threat.wav b/ttstoy/vox_words/threat.wav new file mode 100755 index 0000000..75cefae Binary files /dev/null and b/ttstoy/vox_words/threat.wav differ diff --git a/ttstoy/vox_words/three.wav b/ttstoy/vox_words/three.wav new file mode 100755 index 0000000..097504d Binary files /dev/null and b/ttstoy/vox_words/three.wav differ diff --git a/ttstoy/vox_words/through.wav b/ttstoy/vox_words/through.wav new file mode 100755 index 0000000..78053bd Binary files /dev/null and b/ttstoy/vox_words/through.wav differ diff --git a/ttstoy/vox_words/time.wav b/ttstoy/vox_words/time.wav new file mode 100755 index 0000000..856529d Binary files /dev/null and b/ttstoy/vox_words/time.wav differ diff --git a/ttstoy/vox_words/to.wav b/ttstoy/vox_words/to.wav new file mode 100755 index 0000000..5189cf2 Binary files /dev/null and b/ttstoy/vox_words/to.wav differ diff --git a/ttstoy/vox_words/top.wav b/ttstoy/vox_words/top.wav new file mode 100755 index 0000000..0747e89 Binary files /dev/null and b/ttstoy/vox_words/top.wav differ diff --git a/ttstoy/vox_words/topside.wav b/ttstoy/vox_words/topside.wav new file mode 100755 index 0000000..6f6f54b Binary files /dev/null and b/ttstoy/vox_words/topside.wav differ diff --git a/ttstoy/vox_words/touch.wav b/ttstoy/vox_words/touch.wav new file mode 100755 index 0000000..6712dc1 Binary files /dev/null and b/ttstoy/vox_words/touch.wav differ diff --git a/ttstoy/vox_words/towards.wav b/ttstoy/vox_words/towards.wav new file mode 100755 index 0000000..ff04147 Binary files /dev/null and b/ttstoy/vox_words/towards.wav differ diff --git a/ttstoy/vox_words/track.wav b/ttstoy/vox_words/track.wav new file mode 100755 index 0000000..5247aca Binary files /dev/null and b/ttstoy/vox_words/track.wav differ diff --git a/ttstoy/vox_words/train.wav b/ttstoy/vox_words/train.wav new file mode 100755 index 0000000..ced1a98 Binary files /dev/null and b/ttstoy/vox_words/train.wav differ diff --git a/ttstoy/vox_words/transportation.wav b/ttstoy/vox_words/transportation.wav new file mode 100755 index 0000000..302241a Binary files /dev/null and b/ttstoy/vox_words/transportation.wav differ diff --git a/ttstoy/vox_words/truck.wav b/ttstoy/vox_words/truck.wav new file mode 100755 index 0000000..1692a9c Binary files /dev/null and b/ttstoy/vox_words/truck.wav differ diff --git a/ttstoy/vox_words/tunnel.wav b/ttstoy/vox_words/tunnel.wav new file mode 100755 index 0000000..f158042 Binary files /dev/null and b/ttstoy/vox_words/tunnel.wav differ diff --git a/ttstoy/vox_words/turn.wav b/ttstoy/vox_words/turn.wav new file mode 100755 index 0000000..407d2cd Binary files /dev/null and b/ttstoy/vox_words/turn.wav differ diff --git a/ttstoy/vox_words/turret.wav b/ttstoy/vox_words/turret.wav new file mode 100755 index 0000000..ec42293 Binary files /dev/null and b/ttstoy/vox_words/turret.wav differ diff --git a/ttstoy/vox_words/twelve.wav b/ttstoy/vox_words/twelve.wav new file mode 100755 index 0000000..d9fbcc2 Binary files /dev/null and b/ttstoy/vox_words/twelve.wav differ diff --git a/ttstoy/vox_words/twenty.wav b/ttstoy/vox_words/twenty.wav new file mode 100755 index 0000000..e6d7d80 Binary files /dev/null and b/ttstoy/vox_words/twenty.wav differ diff --git a/ttstoy/vox_words/two.wav b/ttstoy/vox_words/two.wav new file mode 100755 index 0000000..14f432f Binary files /dev/null and b/ttstoy/vox_words/two.wav differ diff --git a/ttstoy/vox_words/unauthorized.wav b/ttstoy/vox_words/unauthorized.wav new file mode 100755 index 0000000..7d3236f Binary files /dev/null and b/ttstoy/vox_words/unauthorized.wav differ diff --git a/ttstoy/vox_words/under.wav b/ttstoy/vox_words/under.wav new file mode 100755 index 0000000..dc30e47 Binary files /dev/null and b/ttstoy/vox_words/under.wav differ diff --git a/ttstoy/vox_words/uniform.wav b/ttstoy/vox_words/uniform.wav new file mode 100755 index 0000000..899c7f0 Binary files /dev/null and b/ttstoy/vox_words/uniform.wav differ diff --git a/ttstoy/vox_words/unlocked.wav b/ttstoy/vox_words/unlocked.wav new file mode 100755 index 0000000..0e48489 Binary files /dev/null and b/ttstoy/vox_words/unlocked.wav differ diff --git a/ttstoy/vox_words/until.wav b/ttstoy/vox_words/until.wav new file mode 100755 index 0000000..68f5ea4 Binary files /dev/null and b/ttstoy/vox_words/until.wav differ diff --git a/ttstoy/vox_words/up.wav b/ttstoy/vox_words/up.wav new file mode 100755 index 0000000..1816546 Binary files /dev/null and b/ttstoy/vox_words/up.wav differ diff --git a/ttstoy/vox_words/upper.wav b/ttstoy/vox_words/upper.wav new file mode 100755 index 0000000..8b59156 Binary files /dev/null and b/ttstoy/vox_words/upper.wav differ diff --git a/ttstoy/vox_words/uranium.wav b/ttstoy/vox_words/uranium.wav new file mode 100755 index 0000000..2ab47d6 Binary files /dev/null and b/ttstoy/vox_words/uranium.wav differ diff --git a/ttstoy/vox_words/us.wav b/ttstoy/vox_words/us.wav new file mode 100755 index 0000000..23b56e9 Binary files /dev/null and b/ttstoy/vox_words/us.wav differ diff --git a/ttstoy/vox_words/usa.wav b/ttstoy/vox_words/usa.wav new file mode 100755 index 0000000..591233e Binary files /dev/null and b/ttstoy/vox_words/usa.wav differ diff --git a/ttstoy/vox_words/use.wav b/ttstoy/vox_words/use.wav new file mode 100755 index 0000000..35be617 Binary files /dev/null and b/ttstoy/vox_words/use.wav differ diff --git a/ttstoy/vox_words/used.wav b/ttstoy/vox_words/used.wav new file mode 100755 index 0000000..3385240 Binary files /dev/null and b/ttstoy/vox_words/used.wav differ diff --git a/ttstoy/vox_words/user.wav b/ttstoy/vox_words/user.wav new file mode 100755 index 0000000..91ccb15 Binary files /dev/null and b/ttstoy/vox_words/user.wav differ diff --git a/ttstoy/vox_words/vacate.wav b/ttstoy/vox_words/vacate.wav new file mode 100755 index 0000000..bed5d70 Binary files /dev/null and b/ttstoy/vox_words/vacate.wav differ diff --git a/ttstoy/vox_words/valid.wav b/ttstoy/vox_words/valid.wav new file mode 100755 index 0000000..40c3f1e Binary files /dev/null and b/ttstoy/vox_words/valid.wav differ diff --git a/ttstoy/vox_words/vapor.wav b/ttstoy/vox_words/vapor.wav new file mode 100755 index 0000000..d067f69 Binary files /dev/null and b/ttstoy/vox_words/vapor.wav differ diff --git a/ttstoy/vox_words/vent.wav b/ttstoy/vox_words/vent.wav new file mode 100755 index 0000000..b4efa86 Binary files /dev/null and b/ttstoy/vox_words/vent.wav differ diff --git a/ttstoy/vox_words/ventillation.wav b/ttstoy/vox_words/ventillation.wav new file mode 100755 index 0000000..d56679d Binary files /dev/null and b/ttstoy/vox_words/ventillation.wav differ diff --git a/ttstoy/vox_words/victor.wav b/ttstoy/vox_words/victor.wav new file mode 100755 index 0000000..8245a2a Binary files /dev/null and b/ttstoy/vox_words/victor.wav differ diff --git a/ttstoy/vox_words/violated.wav b/ttstoy/vox_words/violated.wav new file mode 100755 index 0000000..c597d1d Binary files /dev/null and b/ttstoy/vox_words/violated.wav differ diff --git a/ttstoy/vox_words/violation.wav b/ttstoy/vox_words/violation.wav new file mode 100755 index 0000000..98ed5dd Binary files /dev/null and b/ttstoy/vox_words/violation.wav differ diff --git a/ttstoy/vox_words/voltage.wav b/ttstoy/vox_words/voltage.wav new file mode 100755 index 0000000..140e1dd Binary files /dev/null and b/ttstoy/vox_words/voltage.wav differ diff --git a/ttstoy/vox_words/vox_login.wav b/ttstoy/vox_words/vox_login.wav new file mode 100755 index 0000000..5679acf Binary files /dev/null and b/ttstoy/vox_words/vox_login.wav differ diff --git a/ttstoy/vox_words/walk.wav b/ttstoy/vox_words/walk.wav new file mode 100755 index 0000000..fd8d85f Binary files /dev/null and b/ttstoy/vox_words/walk.wav differ diff --git a/ttstoy/vox_words/wall.wav b/ttstoy/vox_words/wall.wav new file mode 100755 index 0000000..e8e9a5f Binary files /dev/null and b/ttstoy/vox_words/wall.wav differ diff --git a/ttstoy/vox_words/want.wav b/ttstoy/vox_words/want.wav new file mode 100755 index 0000000..3d130ce Binary files /dev/null and b/ttstoy/vox_words/want.wav differ diff --git a/ttstoy/vox_words/wanted.wav b/ttstoy/vox_words/wanted.wav new file mode 100755 index 0000000..a292d2d Binary files /dev/null and b/ttstoy/vox_words/wanted.wav differ diff --git a/ttstoy/vox_words/warm.wav b/ttstoy/vox_words/warm.wav new file mode 100755 index 0000000..518f290 Binary files /dev/null and b/ttstoy/vox_words/warm.wav differ diff --git a/ttstoy/vox_words/warn.wav b/ttstoy/vox_words/warn.wav new file mode 100755 index 0000000..7580146 Binary files /dev/null and b/ttstoy/vox_words/warn.wav differ diff --git a/ttstoy/vox_words/warning.wav b/ttstoy/vox_words/warning.wav new file mode 100755 index 0000000..87db6c1 Binary files /dev/null and b/ttstoy/vox_words/warning.wav differ diff --git a/ttstoy/vox_words/waste.wav b/ttstoy/vox_words/waste.wav new file mode 100755 index 0000000..245c44d Binary files /dev/null and b/ttstoy/vox_words/waste.wav differ diff --git a/ttstoy/vox_words/water.wav b/ttstoy/vox_words/water.wav new file mode 100755 index 0000000..c29d7ac Binary files /dev/null and b/ttstoy/vox_words/water.wav differ diff --git a/ttstoy/vox_words/we.wav b/ttstoy/vox_words/we.wav new file mode 100755 index 0000000..d7ba934 Binary files /dev/null and b/ttstoy/vox_words/we.wav differ diff --git a/ttstoy/vox_words/weapon.wav b/ttstoy/vox_words/weapon.wav new file mode 100755 index 0000000..3e95e9e Binary files /dev/null and b/ttstoy/vox_words/weapon.wav differ diff --git a/ttstoy/vox_words/west.wav b/ttstoy/vox_words/west.wav new file mode 100755 index 0000000..09f5808 Binary files /dev/null and b/ttstoy/vox_words/west.wav differ diff --git a/ttstoy/vox_words/whiskey.wav b/ttstoy/vox_words/whiskey.wav new file mode 100755 index 0000000..11ef7b3 Binary files /dev/null and b/ttstoy/vox_words/whiskey.wav differ diff --git a/ttstoy/vox_words/white.wav b/ttstoy/vox_words/white.wav new file mode 100755 index 0000000..6d4ec3e Binary files /dev/null and b/ttstoy/vox_words/white.wav differ diff --git a/ttstoy/vox_words/wilco.wav b/ttstoy/vox_words/wilco.wav new file mode 100755 index 0000000..862aa8d Binary files /dev/null and b/ttstoy/vox_words/wilco.wav differ diff --git a/ttstoy/vox_words/will.wav b/ttstoy/vox_words/will.wav new file mode 100755 index 0000000..327d81c Binary files /dev/null and b/ttstoy/vox_words/will.wav differ diff --git a/ttstoy/vox_words/with.wav b/ttstoy/vox_words/with.wav new file mode 100755 index 0000000..f1db548 Binary files /dev/null and b/ttstoy/vox_words/with.wav differ diff --git a/ttstoy/vox_words/without.wav b/ttstoy/vox_words/without.wav new file mode 100755 index 0000000..7f3096d Binary files /dev/null and b/ttstoy/vox_words/without.wav differ diff --git a/ttstoy/vox_words/woop.wav b/ttstoy/vox_words/woop.wav new file mode 100755 index 0000000..62fa99c Binary files /dev/null and b/ttstoy/vox_words/woop.wav differ diff --git a/ttstoy/vox_words/xeno.wav b/ttstoy/vox_words/xeno.wav new file mode 100755 index 0000000..6b77a20 Binary files /dev/null and b/ttstoy/vox_words/xeno.wav differ diff --git a/ttstoy/vox_words/yankee.wav b/ttstoy/vox_words/yankee.wav new file mode 100755 index 0000000..62bf15a Binary files /dev/null and b/ttstoy/vox_words/yankee.wav differ diff --git a/ttstoy/vox_words/yards.wav b/ttstoy/vox_words/yards.wav new file mode 100755 index 0000000..33c3c4c Binary files /dev/null and b/ttstoy/vox_words/yards.wav differ diff --git a/ttstoy/vox_words/year.wav b/ttstoy/vox_words/year.wav new file mode 100755 index 0000000..12dca8e Binary files /dev/null and b/ttstoy/vox_words/year.wav differ diff --git a/ttstoy/vox_words/yellow.wav b/ttstoy/vox_words/yellow.wav new file mode 100755 index 0000000..bed92f5 Binary files /dev/null and b/ttstoy/vox_words/yellow.wav differ diff --git a/ttstoy/vox_words/yes.wav b/ttstoy/vox_words/yes.wav new file mode 100755 index 0000000..799bb2b Binary files /dev/null and b/ttstoy/vox_words/yes.wav differ diff --git a/ttstoy/vox_words/you.wav b/ttstoy/vox_words/you.wav new file mode 100755 index 0000000..7ba9be7 Binary files /dev/null and b/ttstoy/vox_words/you.wav differ diff --git a/ttstoy/vox_words/your.wav b/ttstoy/vox_words/your.wav new file mode 100755 index 0000000..9e29c24 Binary files /dev/null and b/ttstoy/vox_words/your.wav differ diff --git a/ttstoy/vox_words/yourself.wav b/ttstoy/vox_words/yourself.wav new file mode 100755 index 0000000..582c3b7 Binary files /dev/null and b/ttstoy/vox_words/yourself.wav differ diff --git a/ttstoy/vox_words/zero.wav b/ttstoy/vox_words/zero.wav new file mode 100755 index 0000000..9b6c397 Binary files /dev/null and b/ttstoy/vox_words/zero.wav differ diff --git a/ttstoy/vox_words/zone.wav b/ttstoy/vox_words/zone.wav new file mode 100755 index 0000000..68f9b89 Binary files /dev/null and b/ttstoy/vox_words/zone.wav differ diff --git a/ttstoy/vox_words/zulu.wav b/ttstoy/vox_words/zulu.wav new file mode 100755 index 0000000..32d6605 Binary files /dev/null and b/ttstoy/vox_words/zulu.wav differ diff --git a/ttstoy/webui/__pycache__/app.cpython-310.pyc b/ttstoy/webui/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..74f6105 Binary files /dev/null and b/ttstoy/webui/__pycache__/app.cpython-310.pyc differ diff --git a/ttstoy/webui/__pycache__/app.cpython-311.pyc b/ttstoy/webui/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..f9f0ee9 Binary files /dev/null and b/ttstoy/webui/__pycache__/app.cpython-311.pyc differ diff --git a/ttstoy/webui/__pycache__/ttstoy_constants.cpython-311.pyc b/ttstoy/webui/__pycache__/ttstoy_constants.cpython-311.pyc new file mode 100644 index 0000000..14b778a Binary files /dev/null and b/ttstoy/webui/__pycache__/ttstoy_constants.cpython-311.pyc differ diff --git a/ttstoy/webui/app.py b/ttstoy/webui/app.py new file mode 100644 index 0000000..6ee2b9a --- /dev/null +++ b/ttstoy/webui/app.py @@ -0,0 +1,901 @@ +""" +TtsToy Web UI โ€” Flask app +Reads RedBot config directly from disk. No RedBot dependency at runtime. +Run: python app.py (or gunicorn app:app) +""" +import os +import json +import logging +import secrets +import tempfile +import threading +import time +import uuid +from pathlib import Path +from functools import wraps + +import requests +from flask import ( + Flask, render_template, request, redirect, url_for, + session, jsonify, send_file, abort, flash +) + +log = logging.getLogger("ttstoy.webui") + +app = Flask(__name__) + +# โ”€โ”€ Load stack config for shared session โ”€โ”€ +_STACK_FILE = Path(__file__).parent / "stack.json" +def _load_stack(): + try: + with open(_STACK_FILE) as f: + return json.load(f) + except Exception: + return {} + +_stack = _load_stack() +app.secret_key = _stack.get("session_secret", os.environ.get("WEBUI_SECRET", secrets.token_hex(32))) +app.config["SESSION_COOKIE_DOMAIN"] = _stack.get("cookie_domain", None) +app.config["SESSION_COOKIE_NAME"] = "scrapyard_session" +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" +app.config["SESSION_COOKIE_SECURE"] = True + +# --------------------------------------------------------------------------- +# Config โ€” edit these or set via environment variables +# --------------------------------------------------------------------------- +REDBOT_DATA_PATH = Path(os.environ.get( + "REDBOT_DATA_PATH", + Path.home() / ".local/share/Red-DiscordBot/data" +)) +BOT_INSTANCE = os.environ.get("BOT_INSTANCE", "redbot") +WEBUI_PORT = int(os.environ.get("WEBUI_PORT", 8098)) +# Hardcoded public URL used for login links (Discord DM). +# This web UI is typically reached via Cloudflared at this hostname. +WEBUI_URL = "https://ttstoy.kingstons-scrapyard.net" + +# Path where RedBot stores cog config +COG_CONFIG_PATH = REDBOT_DATA_PATH / BOT_INSTANCE / "cogs" / "TtsToy" +# Path where RedBot stores per-user config +CORE_CONFIG_PATH = REDBOT_DATA_PATH / BOT_INSTANCE / "core" + +# --------------------------------------------------------------------------- +# Shared state init โ€” runs at import time so gunicorn workers pick it up +# --------------------------------------------------------------------------- +_internal_secret = os.environ.get("TTSTOY_INTERNAL_SECRET") or secrets.token_hex(16) +app.config["INTERNAL_SECRET"] = _internal_secret +SHARED_STATE_FILE = Path(os.environ.get("TTSTOY_SHARED_STATE", "/tmp/ttstoy_webui_state.json")) + +# --------------------------------------------------------------------------- +# Helpers โ€” RedBot config reading (defined early; needed by _init_shared_state) +# --------------------------------------------------------------------------- + +def _read_json(path: Path) -> dict: + try: + with open(path) as f: + return json.load(f) + except FileNotFoundError: + log.debug(f"Config file not found: {path}") + return {} + except Exception as e: + log.warning(f"Failed to read {path}: {e}") + return {} + + +def _init_shared_state(): + try: + state = _read_json(SHARED_STATE_FILE) if SHARED_STATE_FILE.exists() else {} + state["internal_secret"] = _internal_secret + state["webui_url"] = WEBUI_URL + SHARED_STATE_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(SHARED_STATE_FILE, "w") as f: + json.dump(state, f) + except Exception as e: + log.warning(f"Could not init shared state: {e}") + # Also ensure shared_auth.json exists with the secret + try: + auth = _read_auth_file() + auth["secret"] = _internal_secret + _write_auth_file(auth) + except Exception: + pass + +_init_shared_state() + +# --------------------------------------------------------------------------- +# --------------------------------------------------------------------------- +# File-based login token store: ttstoy/shared_auth.json +# All apps in the stack read/write this file via stack.json paths. +# --------------------------------------------------------------------------- + +def _get_auth_file() -> Path: + """Resolve shared_auth.json location from stack.json, fallback to relative.""" + ttstoy_path = _stack.get("ttstoy", "") + if ttstoy_path: + return Path(ttstoy_path) / "shared_auth.json" + return Path(__file__).parent.parent / "shared_auth.json" + +def _read_auth_file() -> dict: + p = _get_auth_file() + try: + with open(p) as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return {"tokens": {}, "secret": _internal_secret} + +def _write_auth_file(data: dict): + p = _get_auth_file() + p.parent.mkdir(parents=True, exist_ok=True) + with open(p, "w") as f: + json.dump(data, f, indent=2) + +_token_lock = threading.Lock() +TOKEN_TTL = 300 # 5 minutes to use the token + +# In-memory TTS output store: {job_id: {status, path, text, user, engine}} +_tts_jobs: dict = {} + + +# RedBot stores TtsToy config under the cog's numeric identifier (0x0A0A0A0A) +COG_IDENTIFIER = "168430090" + + +def get_global_config() -> dict: + """Read TtsToy global config from RedBot data dir.""" + p = COG_CONFIG_PATH / "settings.json" + data = _read_json(p) + if not data: + return {} + cfg = data.get(COG_IDENTIFIER, {}).get("GLOBAL", {}) + log.debug(f"Global config: mode={cfg.get('tts_mode')} chatterbox={cfg.get('chatterbox_api_url')}") + return cfg + + +def get_user_config(user_id: str) -> dict: + """Read TtsToy per-user config.""" + p = COG_CONFIG_PATH / "settings.json" + data = _read_json(p) + if not data: + return {} + cfg = data.get(COG_IDENTIFIER, {}).get("USER", {}).get(str(user_id), {}) + log.debug(f"User config for {user_id}: voice={cfg.get('minimax_voice')} voices={cfg.get('chatterbox_voices')}") + return cfg + + +def get_shared_state() -> dict: + return _read_json(SHARED_STATE_FILE) + + +def get_chatterbox_url() -> str: + cfg = get_global_config() + return cfg.get("chatterbox_api_url", "http://127.0.0.1:8099") + + +def get_dectalk_url() -> str: + cfg = get_global_config() + return cfg.get("dectalk_api_url", "http://127.0.0.1:3001") + + +# --------------------------------------------------------------------------- +# Auth helpers +# --------------------------------------------------------------------------- + +def create_login_token(user_id: str, discord_name: str, avatar_url: str = "", is_owner: bool = False, guild_id: str = "", guild_name: str = "") -> str: + token = secrets.token_urlsafe(24) + with _token_lock: + auth = _read_auth_file() + auth.setdefault("tokens", {})[token] = { + "user_id": user_id, + "discord_name": discord_name, + "avatar_url": avatar_url, + "is_owner": is_owner, + "guild_id": guild_id, + "guild_name": guild_name, + "expires": time.time() + TOKEN_TTL, + } + auth["secret"] = _internal_secret + _write_auth_file(auth) + log.debug(f"Login token created for user {user_id} ({discord_name}) guild={guild_id}, expires in {TOKEN_TTL}s") + return token + + +def consume_login_token(token: str) -> dict | None: + """Validate a token. Does NOT delete it โ€” the session cookie handles auth + across all apps via the shared cookie domain.""" + with _token_lock: + auth = _read_auth_file() + tokens = auth.get("tokens", {}) + entry = tokens.get(token) + if not entry: + log.debug("consume_login_token: token not found") + return None + if time.time() > entry["expires"]: + log.debug(f"consume_login_token: token expired for user {entry['user_id']}") + del tokens[token] + _write_auth_file(auth) + return None + log.debug(f"consume_login_token: success for user {entry['user_id']} ({entry['discord_name']})") + return entry + + +def login_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if "user_id" not in session: + return redirect(url_for("login_page")) + return f(*args, **kwargs) + return decorated + + +# --------------------------------------------------------------------------- +# Routes โ€” Auth +# --------------------------------------------------------------------------- + +@app.route("/") +def index(): + if "user_id" in session: + return redirect(url_for("dashboard")) + return redirect(url_for("login_page")) + + +@app.route("/login") +def login_page(): + return render_template("login.html", webui_url=WEBUI_URL) + + +@app.route("/login", methods=["POST"]) +def login_submit(): + token = request.form.get("token", "").strip() + log.debug(f"Login attempt with token length={len(token)}") + entry = consume_login_token(token) + if not entry: + log.warning("Login failed: invalid or expired token") + flash("Invalid or expired token. Run [p]ttstoy login in Discord to get a new one.", "error") + return redirect(url_for("login_page")) + session["user_id"] = entry["user_id"] + session["discord_name"] = entry["discord_name"] + session["avatar_url"] = entry["avatar_url"] + session["is_owner"] = entry.get("is_owner", False) + session["guild_id"] = entry.get("guild_id", "") + session["guild_name"] = entry.get("guild_name", "") + # Persist userโ†’guild mapping server-side so it survives session overwrites + try: + state = _read_json(SHARED_STATE_FILE) if SHARED_STATE_FILE.exists() else {} + ugm = state.setdefault("user_guild_map", {}) + if entry.get("guild_id"): + ugm[entry["user_id"]] = entry["guild_id"] + with open(SHARED_STATE_FILE, "w") as f: + json.dump(state, f) + except Exception: + pass + log.info(f"User logged in: {entry['discord_name']} ({entry['user_id']}) owner={session['is_owner']} guild={session['guild_id']}") + return redirect(url_for("dashboard")) + + +@app.route("/logout") +def logout(): + session.clear() + return redirect(url_for("login_page")) + + +# --------------------------------------------------------------------------- +# Routes โ€” Dashboard +# --------------------------------------------------------------------------- + +@app.route("/dashboard") +@login_required +def dashboard(): + user_id = session["user_id"] + gcfg = get_global_config() + ucfg = get_user_config(user_id) + global_mode = gcfg.get("tts_mode", "minimax") + # Per-user mode override stored in shared state + state = get_shared_state() + user_mode_overrides = state.get("user_mode_overrides", {}) + mode = user_mode_overrides.get(str(user_id), global_mode) + user_voice = ucfg.get("minimax_voice") + # Build display name for active voice + if user_voice and "_" in user_voice: + active_voice_display = user_voice.split("_", 1)[1].rsplit(".", 1)[0] + else: + active_voice_display = user_voice or "Global default" + return render_template("dashboard.html", + mode=mode, + global_mode=global_mode, + user_voice=user_voice, + active_voice_display=active_voice_display, + gcfg=gcfg, + ucfg=ucfg, + ) + + +# --------------------------------------------------------------------------- +# Routes โ€” TTS Submit +# --------------------------------------------------------------------------- + +@app.route("/api/tts", methods=["POST"]) +@login_required +def api_tts(): + """Queue a TTS request. If generate_only is true, generate directly without VC.""" + data = request.get_json(force=True) + text = (data.get("text") or "").strip() + if not text: + return jsonify({"error": "No text provided"}), 400 + + # If generate_only, redirect to the direct generation endpoint + if data.get("generate_only"): + return api_tts_generate() + + user_id = session["user_id"] + discord_name = session.get("discord_name", "Unknown") + gcfg = get_global_config() + + # Allow per-request mode/voice override from the web UI + state = get_shared_state() + user_mode_overrides = state.get("user_mode_overrides", {}) + global_mode = gcfg.get("tts_mode", "minimax") + mode = data.get("mode") or user_mode_overrides.get(str(user_id), global_mode) + voice = data.get("voice") or None # None = use user's saved voice + + job_id = str(uuid.uuid4()) + log.info(f"TTS job {job_id}: user={discord_name} ({user_id}) mode={mode} voice={voice} text={text[:80]!r}") + _tts_jobs[job_id] = {"status": "pending", "text": text, "user": discord_name, "engine": mode} + + _write_bot_command(user_id, "speak_in_vc", { + "job_id": job_id, + "text": text, + "user": discord_name, + "mode_override": mode, + "voice_override": voice, + "guild_id": _resolve_guild_id(), + }) + + return jsonify({"job_id": job_id}) + + +@app.route("/api/tts//status") +@login_required +def tts_job_status(job_id): + job = _tts_jobs.get(job_id) + if not job: + return jsonify({"status": "not_found"}), 404 + return jsonify({k: v for k, v in job.items() if k != "path"}) + + +@app.route("/api/tts//audio") +@login_required +def tts_job_audio(job_id): + job = _tts_jobs.get(job_id) + if not job or job.get("status") != "done": + abort(404) + return send_file(job["path"], mimetype="audio/mpeg", as_attachment=False) + + +@app.route("/api/tts//update", methods=["POST"]) +def tts_job_update(job_id): + """Called by the bot to update a job's status (done/error) and provide the audio path.""" + secret = request.headers.get("X-Internal-Secret", "") + if secret != app.config.get("INTERNAL_SECRET", ""): + abort(403) + data = request.get_json(force=True) + if job_id in _tts_jobs: + _tts_jobs[job_id].update(data) + log.debug(f"Job {job_id} updated by bot: {data}") + return jsonify({"ok": True}) + + +@app.route("/api/tts/generate", methods=["POST"]) +@login_required +def api_tts_generate(): + """Generate TTS via the bot (no VC playback). Chatterbox only.""" + data = request.get_json(force=True) + text = (data.get("text") or "").strip() + if not text: + return jsonify({"error": "No text provided"}), 400 + + user_id = session["user_id"] + discord_name = session.get("discord_name", "Unknown") + gcfg = get_global_config() + state = get_shared_state() + user_mode_overrides = state.get("user_mode_overrides", {}) + global_mode = gcfg.get("tts_mode", "minimax") + mode = data.get("mode") or user_mode_overrides.get(str(user_id), global_mode) + + if mode != "chatterbox": + return jsonify({"error": "Generate Only is available for Chatterbox mode only. Switch to Chatterbox or use Speak in VC."}), 400 + + voice = data.get("voice") or None + job_id = str(uuid.uuid4()) + _tts_jobs[job_id] = {"status": "pending", "text": text, "user": discord_name, "engine": mode} + + _write_bot_command(user_id, "speak_in_vc", { + "job_id": job_id, + "text": text, + "user": discord_name, + "mode_override": mode, + "voice_override": voice, + "skip_vc": True, + "guild_id": _resolve_guild_id(), + }) + + return jsonify({"job_id": job_id}) + + +def _notify_bot_post(job_id, path, text, user, engine): + """Write a pending post request to the shared state file for the bot to pick up.""" + try: + state = _read_json(SHARED_STATE_FILE) if SHARED_STATE_FILE.exists() else {} + pending = state.get("pending_posts", []) + pending.append({"job_id": job_id, "path": path, "text": text, "user": user, "engine": engine}) + state["pending_posts"] = pending + with open(SHARED_STATE_FILE, "w") as f: + json.dump(state, f) + log.debug(f"Queued Discord post for job {job_id} (user={user} engine={engine})") + except Exception as e: + log.error(f"Failed to queue Discord post for job {job_id}: {e}") + + +# --------------------------------------------------------------------------- +# Routes โ€” Settings API +# --------------------------------------------------------------------------- + +@app.route("/api/settings", methods=["GET"]) +@login_required +def api_get_settings(): + user_id = session["user_id"] + gcfg = get_global_config() + ucfg = get_user_config(user_id) + from ttstoy_constants import MINIMAX_VOICES + + # Merge in any locally-cached param changes (written immediately on save, + # before the bot has had a chance to process the command and update RedBot config) + state = get_shared_state() + param_cache = state.get("chatterbox_params_cache", {}).get(str(user_id), {}) + + # Cache keys match the params keys: temperature, exaggeration, volume_db, speed + # RedBot config keys: chatterbox_temperature_offsets, etc. + def _merged(redbot_key, cache_key): + from_disk = ucfg.get(redbot_key, {}) + from_cache = param_cache.get(cache_key, {}) + return {**from_disk, **from_cache} + + return jsonify({ + "mode": gcfg.get("tts_mode", "minimax"), + "user_voice": ucfg.get("minimax_voice"), + "chatterbox_voices": ucfg.get("chatterbox_voices", []), + "chatterbox_temperature_offsets": _merged("chatterbox_temperature_offsets", "temperature"), + "chatterbox_exaggeration_offsets": _merged("chatterbox_exaggeration_offsets", "exaggeration"), + "chatterbox_volume_offsets": _merged("chatterbox_volume_offsets", "volume_db"), + "chatterbox_speed_offsets": _merged("chatterbox_speed_offsets", "speed"), + "global_voice": gcfg.get("minimax_voice"), + "vox_pack": gcfg.get("vox_pack", "vox"), + "sfx_volume": gcfg.get("sfx_volume", 100), + "minimax_voices": MINIMAX_VOICES, + }) + + +@app.route("/api/settings/voice", methods=["POST"]) +@login_required +def api_set_voice(): + """Tell the bot to update the user's voice via shared state.""" + data = request.get_json(force=True) + voice = data.get("voice", "").strip() + if not voice: + return jsonify({"error": "No voice provided"}), 400 + _write_bot_command(session["user_id"], "set_voice", {"voice": voice}) + return jsonify({"ok": True}) + + +@app.route("/api/settings/mode", methods=["POST"]) +@login_required +def api_set_mode(): + data = request.get_json(force=True) + mode = data.get("mode", "").strip() + if mode not in ("minimax", "chatterbox", "dectalk", "morshu", "vox"): + return jsonify({"error": "Invalid mode"}), 400 + # Store per-user mode override in shared state (doesn't change global bot mode) + state = _read_json(SHARED_STATE_FILE) if SHARED_STATE_FILE.exists() else {} + overrides = state.get("user_mode_overrides", {}) + overrides[str(session["user_id"])] = mode + state["user_mode_overrides"] = overrides + with open(SHARED_STATE_FILE, "w") as f: + json.dump(state, f) + log.info(f"User {session['user_id']} set mode override โ†’ {mode}") + return jsonify({"ok": True}) + + +@app.route("/api/settings/chatterbox_params", methods=["POST"]) +@login_required +def api_set_chatterbox_params(): + data = request.get_json(force=True) + voice = data.get("voice", "").strip() + raw = data.get("params") or {} + params = {k: raw[k] for k in ("temperature", "exaggeration", "volume_db", "speed") if raw.get(k) is not None} + + # Write to shared state immediately so /api/settings reflects the change right away, + # without waiting for the bot to process the command and write back to RedBot config. + try: + state = _read_json(SHARED_STATE_FILE) if SHARED_STATE_FILE.exists() else {} + user_id = str(session["user_id"]) + cache = state.setdefault("chatterbox_params_cache", {}).setdefault(user_id, {}) + for k, v in params.items(): + # params keys: temperature, exaggeration, volume_db, speed + # store flat: {voice: value} under each key + cache.setdefault(k, {})[voice] = v + with open(SHARED_STATE_FILE, "w") as f: + json.dump(state, f) + except Exception as e: + log.warning(f"Could not cache chatterbox params: {e}") + + _write_bot_command(session["user_id"], "set_chatterbox_params", {"voice": voice, "params": params}) + return jsonify({"ok": True}) + + +def _write_bot_command(user_id, cmd, payload): + """Queue a command for the bot cog to execute.""" + try: + state = _read_json(SHARED_STATE_FILE) if SHARED_STATE_FILE.exists() else {} + cmds = state.get("pending_commands", []) + cmds.append({"user_id": str(user_id), "cmd": cmd, "payload": payload, "ts": time.time()}) + state["pending_commands"] = cmds + with open(SHARED_STATE_FILE, "w") as f: + json.dump(state, f) + log.debug(f"Queued bot command: user={user_id} cmd={cmd} payload={payload}") + except Exception as e: + log.error(f"Failed to write bot command {cmd} for user {user_id}: {e}") + + +def _resolve_guild_id() -> str: + """Get the guild_id for the current user: session first, then server-side map.""" + gid = session.get("guild_id", "") + if gid: + return gid + try: + state = _read_json(SHARED_STATE_FILE) if SHARED_STATE_FILE.exists() else {} + return state.get("user_guild_map", {}).get(session.get("user_id", ""), "") + except Exception: + return "" + + +# --------------------------------------------------------------------------- +# Routes โ€” Chatterbox voice management +# --------------------------------------------------------------------------- + +@app.route("/api/chatterbox/voices") +@login_required +def api_chatterbox_voices(): + user_id = session["user_id"] + ucfg = get_user_config(user_id) + voices = ucfg.get("chatterbox_voices", []) + result = [] + for v in voices: + parts = v.split("_", 1) + display = parts[1].rsplit(".", 1)[0] if len(parts) > 1 else v.rsplit(".", 1)[0] + result.append({"filename": v, "display": display}) + return jsonify(result) + + +@app.route("/api/chatterbox/voices/upload", methods=["POST"]) +@login_required +def api_chatterbox_upload(): + if "file" not in request.files: + log.warning("Voice upload: no file in request") + return jsonify({"error": "No file"}), 400 + f = request.files["file"] + name = request.form.get("name", "").strip() + if not name: + name = f.filename.rsplit(".", 1)[0] + import re + safe_name = re.sub(r'[^a-zA-Z0-9_]', '', name) or "voice" + ext = ".wav" if f.filename.lower().endswith(".wav") else ".mp3" + filename = f"{session['user_id']}_{safe_name}{ext}" + cb_url = get_chatterbox_url() + log.debug(f"Voice upload: filename={filename} cb_url={cb_url}") + audio_bytes = f.read() + log.debug(f"Voice upload: raw audio size={len(audio_bytes)} bytes") + try: + from pydub import AudioSegment + import io as _io + seg = AudioSegment.from_file(_io.BytesIO(audio_bytes)) + log.debug(f"Voice upload: clip duration={len(seg)}ms") + if len(seg) < 6000: + loops = (6000 // len(seg)) + 1 + seg = seg * loops + log.debug(f"Voice upload: looped clip to {len(seg)}ms") + buf = _io.BytesIO() + seg.export(buf, format="wav") + audio_bytes = buf.getvalue() + filename = f"{session['user_id']}_{safe_name}.wav" + log.debug(f"Voice upload: processed wav size={len(audio_bytes)} bytes") + except Exception as e: + log.warning(f"Voice upload: pydub processing failed ({e}), uploading raw") + try: + r = requests.post(f"{cb_url.rstrip('/')}/upload_predefined_voice", + files={"files": (filename, audio_bytes, "audio/wav")}, timeout=30) + log.debug(f"Voice upload: Chatterbox response {r.status_code}") + r.raise_for_status() + except Exception as ex: + log.error(f"Voice upload failed: {ex}") + return jsonify({"error": str(ex)}), 500 + _write_bot_command(session["user_id"], "add_voice", {"filename": filename}) + log.info(f"Voice uploaded: {filename} for user {session['user_id']}") + return jsonify({"ok": True, "filename": filename}) + + +@app.route("/api/chatterbox/voices/", methods=["DELETE"]) +@login_required +def api_chatterbox_delete_voice(filename): + user_id = session["user_id"] + if not filename.startswith(f"{user_id}_"): + return jsonify({"error": "Not your voice"}), 403 + cb_url = get_chatterbox_url() + try: + r = requests.delete(f"{cb_url.rstrip('/')}/delete_predefined_voice/{filename}", timeout=10) + if r.status_code not in (200, 204, 404): + return jsonify({"error": f"Server returned {r.status_code}"}), 500 + except Exception as ex: + return jsonify({"error": str(ex)}), 500 + _write_bot_command(user_id, "remove_voice", {"filename": filename}) + return jsonify({"ok": True}) + + +# --------------------------------------------------------------------------- +# Routes โ€” Pages +# --------------------------------------------------------------------------- + +@app.route("/api/sfx_emojis") +@login_required +def api_sfx_emojis(): + """Return the emojiโ†’sfx folder mapping for the SFX picker.""" + mapping = _load_emoji_map() + return jsonify([{"emoji": e, "name": n} for e, n in mapping.items()]) + + +# --------------------------------------------------------------------------- +# SFX helpers +# --------------------------------------------------------------------------- +def _sfx_root() -> Path: + return Path(__file__).resolve().parent.parent / "sfx" + + +def _emoji_map_path() -> Path: + return _sfx_root() / "emoji_map.json" + + +def _load_emoji_map() -> dict: + """Load emojiโ†’folder mapping from the shared JSON file.""" + p = _emoji_map_path() + if p.exists(): + try: + with open(p) as f: + return json.load(f) + except Exception: + pass + # Fallback: try importing the live dict from the cog + try: + import sys + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from ttstoy import EMOJI_SFX_FOLDERS + return dict(EMOJI_SFX_FOLDERS) + except Exception: + return {} + + +def _save_emoji_map(mapping: dict): + """Save emojiโ†’folder mapping to the shared JSON file and reload in the cog.""" + p = _emoji_map_path() + p.parent.mkdir(parents=True, exist_ok=True) + with open(p, "w") as f: + json.dump(mapping, f, ensure_ascii=False, indent=2) + # Try to reload the live dict in the cog (same process won't help for + # the bot, but the bot's state processor or next read will pick it up) + try: + import sys + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from ttstoy import _reload_emoji_sfx_map + _reload_emoji_sfx_map() + except Exception: + pass + + +def _owner_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not session.get("is_owner"): + return jsonify({"error": "Owner only"}), 403 + return f(*args, **kwargs) + return decorated + + +@app.route("/api/sfx", methods=["GET"]) +@login_required +@_owner_required +def api_sfx_list(): + """List all SFX entries: one emoji + one file per folder.""" + mapping = _load_emoji_map() + + root = _sfx_root() + # Reverse map: folder โ†’ emoji + folder_emoji = {} + for emoji, folder in mapping.items(): + folder_emoji[folder] = emoji # last one wins (1:1 model) + + result = [] + if root.exists(): + for d in sorted(root.iterdir()): + if d.is_dir(): + files = sorted(f.name for f in d.iterdir() if f.is_file() and f.suffix in (".mp3", ".wav", ".ogg")) + result.append({ + "name": d.name, + "emoji": folder_emoji.get(d.name, ""), + "file": files[0] if files else "", + }) + return jsonify(result) + + +@app.route("/api/sfx//upload", methods=["POST"]) +@login_required +@_owner_required +def api_sfx_upload(folder_name): + """Upload an audio file to an SFX folder. Creates the folder if needed.""" + import re + if not re.match(r'^[a-zA-Z0-9_-]+$', folder_name): + return jsonify({"error": "Invalid folder name"}), 400 + if "file" not in request.files: + return jsonify({"error": "No file"}), 400 + f = request.files["file"] + if not f.filename: + return jsonify({"error": "No filename"}), 400 + ext = Path(f.filename).suffix.lower() + if ext not in (".mp3", ".wav", ".ogg"): + return jsonify({"error": "Only .mp3, .wav, .ogg allowed"}), 400 + + safe_name = re.sub(r'[^a-zA-Z0-9_.-]', '_', f.filename) + folder = _sfx_root() / folder_name + folder.mkdir(parents=True, exist_ok=True) + dest = folder / safe_name + f.save(str(dest)) + log.info(f"SFX uploaded: {dest}") + return jsonify({"ok": True, "path": str(dest.relative_to(_sfx_root()))}) + + +@app.route("/api/sfx/", methods=["DELETE"]) +@login_required +@_owner_required +def api_sfx_delete_folder(folder_name): + """Delete an SFX and its emoji mapping.""" + import shutil + folder = _sfx_root() / folder_name + if not folder.exists(): + return jsonify({"error": "Folder not found"}), 404 + shutil.rmtree(folder) + log.info(f"SFX folder deleted: {folder}") + # Also remove any emoji mappings pointing to this folder + mapping = _load_emoji_map() + changed = {e: f for e, f in mapping.items() if f != folder_name} + if len(changed) != len(mapping): + _save_emoji_map(changed) + return jsonify({"ok": True}) + + +@app.route("/api/sfx/emoji_map", methods=["POST"]) +@login_required +@_owner_required +def api_emoji_map_set(): + """Set or clear the emoji for an SFX folder. Enforces 1 emoji per folder.""" + data = request.get_json(force=True) + emoji = (data.get("emoji") or "").strip() + folder = (data.get("folder") or "").strip() + name = (data.get("name") or folder or "").strip() + + mapping = _load_emoji_map() + + # Remove any existing emoji that pointed to this folder (1:1) + if name: + mapping = {e: f for e, f in mapping.items() if f != name} + + if emoji and folder: + # Also remove this emoji from any other folder + mapping = {e: f for e, f in mapping.items() if e != emoji} + mapping[emoji] = folder + # If emoji is empty, we already removed the folder's old mapping above + + _save_emoji_map(mapping) + log.info(f"Emoji map updated: {emoji!r} โ†’ {folder!r}") + return jsonify({"ok": True}) + + +@app.route("/sfx") +@login_required +def page_sfx(): + if not session.get("is_owner"): + return redirect(url_for("dashboard")) + return render_template("sfx.html") + + +@app.route("/chatterbox") +@login_required +def page_chatterbox(): + return render_template("chatterbox.html") + + +@app.route("/minimax") +@login_required +def page_minimax(): + gcfg = get_global_config() + from ttstoy_constants import MINIMAX_VOICES + return render_template("minimax.html", voices=MINIMAX_VOICES, gcfg=gcfg) + + +@app.route("/dectalk") +@login_required +def page_dectalk(): + gcfg = get_global_config() + return render_template("dectalk.html", gcfg=gcfg) + + +@app.route("/vox") +@login_required +def page_vox(): + gcfg = get_global_config() + try: + import sys + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from vox_engine import get_available_words, get_available_packs + pack = gcfg.get("vox_pack", "vox") + words = get_available_words(pack) + packs = get_available_packs() + except Exception: + words, packs, pack = [], ["vox"], "vox" + return render_template("vox.html", words=words, packs=packs, current_pack=pack) + + +@app.route("/profile") +@login_required +def page_profile(): + user_id = session["user_id"] + gcfg = get_global_config() + ucfg = get_user_config(user_id) + state = get_shared_state() + user_info = state.get("users", {}).get(str(user_id), {}) + return render_template("profile.html", gcfg=gcfg, ucfg=ucfg, user_info=user_info) + + +# --------------------------------------------------------------------------- +# Token endpoint โ€” called by the bot cog to register a login token +# --------------------------------------------------------------------------- + +@app.route("/api/register_token", methods=["POST"]) +def api_register_token(): + """Called by the bot (locally) to register a login token for a user.""" + secret = request.headers.get("X-Internal-Secret", "") + if secret != app.config.get("INTERNAL_SECRET", ""): + log.warning("api_register_token: rejected request with wrong internal secret") + abort(403) + data = request.get_json(force=True) + log.debug(f"api_register_token: creating token for user {data.get('user_id')} ({data.get('discord_name')})") + token = create_login_token( + str(data["user_id"]), + data.get("discord_name", "Unknown"), + data.get("avatar_url", ""), + data.get("is_owner", False), + data.get("guild_id", ""), + data.get("guild_name", ""), + ) + return jsonify({"token": token}) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + import sys + import logging as _logging + _logging.basicConfig( + stream=sys.stdout, + level=_logging.DEBUG, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + ) + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + log.info(f"TtsToy WebUI starting โ€” port={WEBUI_PORT} url={WEBUI_URL}") + app.run(host="0.0.0.0", port=WEBUI_PORT, debug=False) diff --git a/ttstoy/webui/requirements.txt b/ttstoy/webui/requirements.txt new file mode 100644 index 0000000..3b0b408 --- /dev/null +++ b/ttstoy/webui/requirements.txt @@ -0,0 +1,4 @@ +flask>=3.0 +requests>=2.31 +pydub>=0.25 +gunicorn diff --git a/ttstoy/webui/stack.json b/ttstoy/webui/stack.json new file mode 100644 index 0000000..ff667a0 --- /dev/null +++ b/ttstoy/webui/stack.json @@ -0,0 +1,10 @@ +{ + "homepage": "/home/owen/Desktop/Homepage", + "webtable": "/home/owen/Desktop/WebTable", + "porgupicker": "/home/owen/Desktop/PorguPicker", + "galaxy": "/home/owen/Desktop/Something", + "ttstoy": "/home/owen/Desktop/ttstoy", + "ttstoy_webui": "/home/owen/Desktop/ttstoy/webui", + "cookie_domain": ".kingstons-scrapyard.net", + "session_secret": "scrapyard-shared-session-k8x7m2p4q9w1" +} diff --git a/ttstoy/webui/start.sh b/ttstoy/webui/start.sh new file mode 100755 index 0000000..e87bd87 --- /dev/null +++ b/ttstoy/webui/start.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Start the TtsToy Web UI +# Edit these or export them before running + +# Path to your RedBot data directory +export REDBOT_DATA_PATH="${REDBOT_DATA_PATH:-$HOME/.local/share/Red-DiscordBot/data}" + +# Your RedBot instance name (the name you used when creating it) +export BOT_INSTANCE="${BOT_INSTANCE:-redbot}" + +# Port the web UI listens on +export WEBUI_PORT="${WEBUI_PORT:-8098}" + +# Public URL users will visit. +# Kept in sync with the hardcoded value in webui/app.py for the Discord login links. +export WEBUI_URL="https://ttstoy.kingstons-scrapyard.net" + +# Discord channel ID to post TTS messages to (get it by right-clicking a channel) +export TTSTOY_WEBUI_CHANNEL_ID="${TTSTOY_WEBUI_CHANNEL_ID:-0}" + +# Shared state file path (must match TTSTOY_SHARED_STATE in the bot env if changed) +export TTSTOY_SHARED_STATE="${TTSTOY_SHARED_STATE:-/tmp/ttstoy_webui_state.json}" + +cd "$(dirname "$0")" + +# โ”€โ”€ Setup venv โ”€โ”€ +if [ ! -d "venv" ]; then + echo "[setup] Creating ttstoy webui venv..." + python3 -m venv venv +fi +source venv/bin/activate +pip install -q -r requirements.txt + +exec gunicorn "app:app" \ + --workers 1 \ + --bind "0.0.0.0:${WEBUI_PORT}" \ + --log-level info diff --git a/ttstoy/webui/templates/base.html b/ttstoy/webui/templates/base.html new file mode 100644 index 0000000..b4f64b5 --- /dev/null +++ b/ttstoy/webui/templates/base.html @@ -0,0 +1,247 @@ + + + + + + {% block title %}TtsToy{% endblock %} + + {% block head %}{% endblock %} + + + + + + +
+{% if session.get('user_id') %} + +{% endif %} +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% for cat, msg in messages %} +
{{ msg }}
+ {% endfor %} + {% endwith %} + {% block content %}{% endblock %} +
+ + +{% block scripts %}{% endblock %} + + diff --git a/ttstoy/webui/templates/chatterbox.html b/ttstoy/webui/templates/chatterbox.html new file mode 100644 index 0000000..0b8b621 --- /dev/null +++ b/ttstoy/webui/templates/chatterbox.html @@ -0,0 +1,194 @@ +{% extends "base.html" %} +{% block title %}Chatterbox โ€” TtsToy{% endblock %} +{% block content %} +
+

Chatterbox Voices

+
+ +
+

Upload New Voice

+
Upload a 5โ€“15 second .wav or .mp3 clip of a single speaker. Short clips are auto-looped.
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+

Your Voices

+
  • Loading...
+
+ + +{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/ttstoy/webui/templates/dashboard.html b/ttstoy/webui/templates/dashboard.html new file mode 100644 index 0000000..0f03e52 --- /dev/null +++ b/ttstoy/webui/templates/dashboard.html @@ -0,0 +1,399 @@ +{% extends "base.html" %} +{% block title %}Dashboard โ€” TtsToy{% endblock %} +{% block content %} +
+

Dashboard

+ {{ session.get('discord_name','') }} +
+ +
+
+
+

Active Mode

+
+ {{ mode.upper() }} +
+
Global bot mode: {{ global_mode }}
+
+
+
+
+

Active Voice

+
{{ active_voice_display }}
+
+ {% if mode == "chatterbox" %}Manage voices + {% elif mode == "minimax" %}Browse voices + {% elif mode == "dectalk" %}Voice commands + {% elif mode == "vox" %}Word list + {% endif %} +
+
+
+
+ +
+

Submit TTS Message

+
Speak in VC plays in your voice channel. Generate Only creates the audio for playback here โ€” no VC needed.
+ +
+
+ + +
+
+ + +
+
+ + + + +
+ +
+ + +
+ +
+
+ + + +
+ +
+ +
+

Mode Preference

+
Your personal mode override โ€” does not affect other users.
+
+ + +
+
+ +
+

TTS Tips

+ + + + + + + + + + + + + + + + + + + + + +
Voice switchingUse [mode|voice] tags to change voice mid-message. Everything after the tag uses that voice until the next tag. The voice part is optional for modes that don't need it.
+ Example: [minimax|robotnik] I am the eggman [dectalk] [:nh]now I'm Harry +
Available modes + minimax ยท chatterbox ยท dectalk ยท morshu ยท vox +
Voice names + MiniMax: [minimax|Robotnik], [minimax|Gnome], etc.
+ Chatterbox: [chatterbox|YourVoiceName] (uploaded filename)
+ DECTalk: just [dectalk] โ€” control voices in the text with [:np] Paul, [:nb] Betty, [:nh] Harry, etc.
+ Morshu / VOX: [morshu] or [vox] โ€” no voice selection needed +
Sound effectsInsert SFX emoji anywhere in your text โ€” click the ๐Ÿ”Š button on the message box to browse available sounds.
Chatterbox tokens[laugh] [chuckle] [sigh] [gasp] [cough] [clear throat] [sniff] [groan] [shush]
+
+{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/ttstoy/webui/templates/dectalk.html b/ttstoy/webui/templates/dectalk.html new file mode 100644 index 0000000..84bb918 --- /dev/null +++ b/ttstoy/webui/templates/dectalk.html @@ -0,0 +1,50 @@ +{% extends "base.html" %} +{% block title %}DECTalk โ€” TtsToy{% endblock %} +{% block content %} +
+

DECTalk

+
+ +
+

About

+
Classic 1980s robotic speech synthesizer. Voices are controlled inline using commands embedded in your text.
+
Server URL: {{ gcfg.get('dectalk_api_url', 'http://127.0.0.1:3001') }}
+
+ +
+

Voice Commands

+
Embed these in your TTS text to switch voices mid-sentence.
+ + + + {% for cmd, name, desc in [ + ("[:np]","Paul","Perfect Paul โ€” default male"), + ("[:nb]","Betty","Beautiful Betty โ€” female"), + ("[:nh]","Harry","Huge Harry โ€” deep male"), + ("[:nf]","Frank","Frail Frank โ€” male"), + ("[:nd]","Dennis","Doctor Dennis โ€” male"), + ("[:nk]","Kit","Kit the Kid โ€” child"), + ("[:nu]","Ursula","Uppity Ursula โ€” female"), + ("[:nr]","Rita","Rough Rita โ€” female"), + ("[:nw]","Wendy","Whispering Wendy โ€” female"), + ] %} + + + + + + {% endfor %} + +
CommandVoiceDescription
{{ cmd }}{{ name }}{{ desc }}
+
+ +
+

Example

+ + [:np]Hello, I am Paul. [:nb]And I am Betty. [:nh]I am Harry, the big one. + +
+ See the DECTalk manual for advanced rate, pitch, and phoneme control. +
+
+{% endblock %} diff --git a/ttstoy/webui/templates/login.html b/ttstoy/webui/templates/login.html new file mode 100644 index 0000000..ea63f86 --- /dev/null +++ b/ttstoy/webui/templates/login.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}Login โ€” TtsToy{% endblock %} +{% block content %} +
+
+
+
TTSTOY
+
Web Interface
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% for cat, msg in messages %} +
{{ msg }}
+ {% endfor %} + {% endwith %} + +
+ Run [p]login in Discord.
+ The bot will DM you a one-time key.
+ Paste it below and click Login. +
+ +
+
+ + +
+ +
+
+
+{% endblock %} diff --git a/ttstoy/webui/templates/minimax.html b/ttstoy/webui/templates/minimax.html new file mode 100644 index 0000000..4388f6b --- /dev/null +++ b/ttstoy/webui/templates/minimax.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% block title %}MiniMax โ€” TtsToy{% endblock %} +{% block content %} +
+

MiniMax

+
+ +
+

About

+
MiniMax is a cloud TTS API with natural-sounding voices. Requires an API key set by the bot owner.
+
Current model: {{ gcfg.get('minimax_model', 'speech-01-turbo') }}
+
+ +
+

Available Voices

+
Click a voice to set it as your personal voice.
+ + + + {% for name, vid in voices.items() %} + + + + + + {% endfor %} + +
NameVoice ID
{{ name }}{{ vid }}
+
+
+ +
+

Models

+ + + + {% for m, desc in [("speech-01-turbo","Fast and cheap"),("speech-01-hd","Higher quality, slower"),("speech-02-turbo","Newer fast model"),("speech-02-hd","Newer high quality model")] %} + + {% endfor %} + +
ModelDescription
{{ m }}{{ desc }}
+
Model can only be changed by the bot owner via [p]ttstoy model.
+
+{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/ttstoy/webui/templates/profile.html b/ttstoy/webui/templates/profile.html new file mode 100644 index 0000000..abbd52f --- /dev/null +++ b/ttstoy/webui/templates/profile.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} +{% block title %}Profile โ€” TtsToy{% endblock %} +{% block content %} +
+

Profile

+
+ +
+

Discord

+
+ {% if session.get('avatar_url') %} + + {% endif %} +
+
{{ session.get('discord_name', 'Unknown') }}
+
ID: {{ session.get('user_id') }}
+
+
+ {% if user_info %} + + {% for k, v in user_info.items() %} + + {% endfor %} +
{{ k }}{{ v }}
+ {% else %} +
No additional info available from the bot yet.
+ {% endif %} +
+ +
+

TtsToy Settings

+ + + + + + + + + + + + + + + + + + + + + +
Active Voice + {% if ucfg.get('minimax_voice') %} + {% set v = ucfg.minimax_voice %} + {{ v.split('_',1)[1].rsplit('.',1)[0] if '_' in v else v }} + {% else %} + Global default + {% endif %} +
Uploaded Voices{{ ucfg.get('chatterbox_voices', []) | length }}
Global TTS Mode{{ gcfg.get('tts_mode','minimax').upper() }}
SFX Volume{{ gcfg.get('sfx_volume', 100) }}%
Accessibility Mode{{ 'On' if gcfg.get('accessibility_mode') else 'Off' }}
+
+ +{% if ucfg.get('chatterbox_voices') %} +
+

Per-Voice Settings

+ + + + {% for v in ucfg.get('chatterbox_voices', []) %} + {% set display = v.split('_',1)[1].rsplit('.',1)[0] if '_' in v else v %} + + + + + + + + {% endfor %} + +
VoiceTempExagVol dBSpeed
{{ display }}{{ ucfg.get('chatterbox_temperature_offsets',{}).get(v, 'โ€”') }}{{ ucfg.get('chatterbox_exaggeration_offsets',{}).get(v, 'โ€”') }}{{ ucfg.get('chatterbox_volume_offsets',{}).get(v, 'โ€”') }}{{ ucfg.get('chatterbox_speed_offsets',{}).get(v, 'โ€”') }}
+
+{% endif %} +{% endblock %} diff --git a/ttstoy/webui/templates/sfx.html b/ttstoy/webui/templates/sfx.html new file mode 100644 index 0000000..5a71506 --- /dev/null +++ b/ttstoy/webui/templates/sfx.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} +{% block title %}SFX Manager โ€” TtsToy{% endblock %} +{% block content %} +
+

Sound Effects Manager

+
Each SFX has one emoji trigger and one audio file. Users insert the emoji in their TTS text to play the sound.
+
+ +
+

Add New SFX

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+

SFX Library

+ + + +
EmojiNameFile
Loading...
+
+{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/ttstoy/webui/templates/vox.html b/ttstoy/webui/templates/vox.html new file mode 100644 index 0000000..bfeccd5 --- /dev/null +++ b/ttstoy/webui/templates/vox.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block title %}VOX โ€” TtsToy{% endblock %} +{% block content %} +
+

VOX Announcer

+
+ +
+

About

+
Black Mesa / Half-Life VOX announcer. Only words in the dictionary are spoken โ€” unknown words are silently skipped.
+
Current pack: {{ current_pack }}  |  Available: {{ packs | join(', ') }}
+
+ +
+

Available Words ({{ words | length }})

+
+ +
+
+ {% for w in words %} + {{ w }} + {% endfor %} +
+
+{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/ttstoy/webui/ttstoy_constants.py b/ttstoy/webui/ttstoy_constants.py new file mode 100644 index 0000000..a93b702 --- /dev/null +++ b/ttstoy/webui/ttstoy_constants.py @@ -0,0 +1,25 @@ +"""Shared constants mirrored from ttstoy.py โ€” no RedBot import needed.""" + +MINIMAX_VOICES = { + "BigMan": "moss_audio_54f969bf-12c1-11f1-93de-a6f4120d2cc7", + "BlueGnome": "moss_audio_166dfe7c-1390-11f1-b6f2-729162d0a8d2", + "Dracafow": "moss_audio_13cb66d2-12c7-11f1-b6f2-729162d0a8d2", + "Gaben": "moss_audio_5239ca93-115b-11f1-b9c4-4ea5324904c7", + "Gregori": "moss_audio_63a397aa-24c9-11f1-918f-5a2de67f838a", + "Gnome": "moss_audio_b304011b-11b2-11f1-90fe-36953c023630", + "King": "moss_audio_39c16b2c-1158-11f1-995d-b25e2f30d3fa", + "Peppa": "moss_audio_d656489f-114b-11f1-995d-b25e2f30d3fa", + "Robotnik": "moss_audio_123443a8-11b4-11f1-bfa6-763108879732", +} + +DECTALK_VOICES = { + "paul": ("[:np]", "Perfect Paul โ€” default male"), + "betty": ("[:nb]", "Beautiful Betty โ€” female"), + "harry": ("[:nh]", "Huge Harry โ€” deep male"), + "frank": ("[:nf]", "Frail Frank โ€” male"), + "dennis": ("[:nd]", "Doctor Dennis โ€” male"), + "kit": ("[:nk]", "Kit the Kid โ€” child"), + "ursula": ("[:nu]", "Uppity Ursula โ€” female"), + "rita": ("[:nr]", "Rough Rita โ€” female"), + "wendy": ("[:nw]", "Whispering Wendy โ€” female"), +}