Enter Text
+ +Voice Settings
+ +Select a voice to test it with a sample sentence
+Recent Generations
+No recent generations yet
+diff --git a/.gitignore b/.gitignore index 8e093ea..acc657c 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,4 @@ cython_debug/ *.mp3 *.srt /.idea/ +.DS_Store diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/main.py b/main.py new file mode 100644 index 0000000..ab75f6b --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from edge-tts!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b1f39c8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "edge-tts" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..0976e14 --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,53 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Documentation +README.md +QUICKSTART.md +*.md + +# Build scripts +build.sh +deploy.sh +create_icons.py +create_icons.sh + +# Test files +test_*.py +*_test.py + +# Logs +*.log + +# Temporary files +*.tmp +*.mp3 +*.wav +*.srt diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..2ac4ced --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,26 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.mp3 +*.wav +*.srt +create_icons.py +create_icons.sh diff --git a/web/DEPLOYMENT.md b/web/DEPLOYMENT.md new file mode 100644 index 0000000..56daf24 --- /dev/null +++ b/web/DEPLOYMENT.md @@ -0,0 +1,435 @@ +# Edge TTS Web UI - Docker Deployment Guide + +Complete guide for deploying Edge TTS Web UI to a remote server using Docker. + +## 📋 Prerequisites + +### Local Machine +- Docker installed +- Docker Compose installed +- SSH access to remote server +- rsync (for deployment script) + +### Remote Server +- Linux server (Ubuntu 20.04+ recommended) +- Docker installed +- Docker Compose installed +- Port 8000 open (or configure different port) +- Minimum 512MB RAM +- SSH access configured + +## 🚀 Quick Start + +### Option 1: Local Testing + +```bash +cd web + +# Build the image +./build.sh + +# Start with docker-compose +docker-compose up -d + +# Check logs +docker-compose logs -f + +# Access at http://localhost:8000 +``` + +### Option 2: Remote Deployment + +```bash +cd web + +# Deploy to remote server +REMOTE_HOST=192.168.1.100 ./deploy.sh + +# Or with custom user and path +REMOTE_HOST=myserver.com REMOTE_USER=deployer REMOTE_PATH=/opt/edge-tts ./deploy.sh +``` + +## 📦 Building the Docker Image + +### Build Locally + +```bash +# Build with default tag (latest) +./build.sh + +# Build with custom tag +./build.sh v1.0.0 +``` + +### Manual Build + +```bash +docker build -t edge-tts-web:latest . +``` + +## 🏃 Running the Container + +### Using Docker Compose (Recommended) + +```bash +# Start in background +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop +docker-compose down + +# Restart +docker-compose restart + +# View status +docker-compose ps +``` + +### Using Docker CLI + +```bash +# Run container +docker run -d \ + --name edge-tts-web \ + -p 8000:8000 \ + --restart unless-stopped \ + edge-tts-web:latest + +# View logs +docker logs -f edge-tts-web + +# Stop container +docker stop edge-tts-web + +# Remove container +docker rm edge-tts-web +``` + +## 🌐 Remote Server Deployment + +### Step-by-Step Manual Deployment + +#### 1. Install Docker on Remote Server + +```bash +# SSH into server +ssh user@your-server.com + +# Install Docker +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# Install Docker Compose +sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +sudo chmod +x /usr/local/bin/docker-compose + +# Add user to docker group (optional) +sudo usermod -aG docker $USER +``` + +#### 2. Copy Files to Server + +```bash +# Create deployment directory +ssh user@your-server.com 'mkdir -p /opt/edge-tts' + +# Copy files (from local machine) +cd web +rsync -avz --exclude='venv' --exclude='.git' \ + ./ user@your-server.com:/opt/edge-tts/ +``` + +#### 3. Build and Start on Server + +```bash +# SSH into server +ssh user@your-server.com + +# Navigate to deployment directory +cd /opt/edge-tts + +# Build and start +docker-compose up -d + +# Check status +docker-compose ps +docker-compose logs -f +``` + +### Automated Deployment Script + +The `deploy.sh` script automates the entire deployment process: + +```bash +# Basic usage +REMOTE_HOST=192.168.1.100 ./deploy.sh + +# With custom configuration +REMOTE_HOST=myserver.com \ +REMOTE_USER=deployer \ +REMOTE_PATH=/home/deployer/edge-tts \ +./deploy.sh +``` + +**What the script does:** +1. ✅ Checks SSH connectivity +2. ✅ Creates remote directory +3. ✅ Copies all files to server +4. ✅ Stops existing containers +5. ✅ Builds new Docker image +6. ✅ Starts containers +7. ✅ Shows deployment status + +## 🔧 Configuration + +### Environment Variables + +Create a `.env` file for custom configuration: + +```bash +# .env +PYTHONUNBUFFERED=1 +# Add other environment variables as needed +``` + +### Custom Port + +To use a different port, edit `docker-compose.yml`: + +```yaml +ports: + - "3000:8000" # Host:Container +``` + +### Resource Limits + +Add resource limits in `docker-compose.yml`: + +```yaml +services: + edge-tts-web: + # ... other config + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M +``` + +## 🔒 Security Best Practices + +### 1. Use Non-Root User +The Dockerfile already creates a non-root user (`appuser`) + +### 2. Reverse Proxy with SSL + +Use Nginx or Traefik as reverse proxy: + +**Nginx example:** +```nginx +server { + listen 80; + server_name tts.yourdomain.com; + + location / { + proxy_pass http://localhost:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +**With Let's Encrypt SSL:** +```bash +sudo apt install certbot python3-certbot-nginx +sudo certbot --nginx -d tts.yourdomain.com +``` + +### 3. Firewall Configuration + +```bash +# Allow only necessary ports +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw enable +``` + +### 4. Docker Socket Security + +Don't expose Docker socket unnecessarily. The current setup doesn't require it. + +## 📊 Monitoring + +### Check Container Status + +```bash +# Using docker-compose +docker-compose ps + +# Using docker CLI +docker ps +``` + +### View Logs + +```bash +# All logs +docker-compose logs + +# Follow logs (real-time) +docker-compose logs -f + +# Last 100 lines +docker-compose logs --tail=100 + +# Specific service +docker-compose logs edge-tts-web +``` + +### Health Checks + +The container includes a health check that runs every 30 seconds: + +```bash +# Check health status +docker inspect edge-tts-web --format='{{.State.Health.Status}}' + +# View health check logs +docker inspect edge-tts-web --format='{{json .State.Health}}' | jq +``` + +## 🔄 Updates and Maintenance + +### Update Application + +```bash +# Pull latest changes +git pull + +# Rebuild and restart +docker-compose up -d --build + +# Or use deployment script +REMOTE_HOST=your-server.com ./deploy.sh +``` + +### Backup and Restore + +```bash +# Backup (no persistent data currently) +# If you add persistent data, use Docker volumes + +# Create volume backup +docker run --rm -v edge-tts-data:/data -v $(pwd):/backup \ + alpine tar czf /backup/edge-tts-backup.tar.gz /data + +# Restore volume +docker run --rm -v edge-tts-data:/data -v $(pwd):/backup \ + alpine tar xzf /backup/edge-tts-backup.tar.gz -C / +``` + +### Clean Up + +```bash +# Stop and remove containers +docker-compose down + +# Remove images +docker rmi edge-tts-web:latest + +# Clean up unused resources +docker system prune -a +``` + +## 🐛 Troubleshooting + +### Container Won't Start + +```bash +# Check logs +docker-compose logs + +# Check container status +docker-compose ps + +# Rebuild from scratch +docker-compose down +docker-compose build --no-cache +docker-compose up -d +``` + +### Port Already in Use + +```bash +# Check what's using the port +sudo lsof -i :8000 + +# Kill the process or change port in docker-compose.yml +``` + +### Permission Denied Errors + +```bash +# On remote server, add user to docker group +sudo usermod -aG docker $USER +newgrp docker +``` + +### Health Check Failing + +```bash +# Check if app is responding +curl http://localhost:8000/api/health + +# Check health status +docker inspect edge-tts-web --format='{{json .State.Health}}' | jq +``` + +## 📁 File Structure + +``` +web/ +├── Dockerfile # Docker image definition +├── docker-compose.yml # Docker Compose configuration +├── .dockerignore # Files to exclude from image +├── build.sh # Build script +├── deploy.sh # Deployment script +├── server.py # FastAPI server +├── index.html # Web UI +├── app.js # Frontend logic +├── styles.css # Styling +├── manifest.json # PWA manifest +├── sw.js # Service worker +└── requirements.txt # Python dependencies +``` + +## 🌟 Production Recommendations + +1. **Use a Reverse Proxy**: Nginx or Traefik with SSL/TLS +2. **Set Up Monitoring**: Prometheus + Grafana +3. **Configure Logging**: Centralized logging with ELK or Loki +4. **Auto-Restart**: Use `restart: unless-stopped` in docker-compose +5. **Resource Limits**: Set appropriate CPU and memory limits +6. **Regular Backups**: If you add persistent data +7. **Security Updates**: Keep Docker and base images updated +8. **Domain Name**: Use a proper domain with DNS +9. **CDN**: Consider using a CDN for static assets +10. **Rate Limiting**: Implement rate limiting for API endpoints + +## 📞 Support + +For issues or questions: +- Check logs: `docker-compose logs -f` +- GitHub Issues: https://github.com/rany2/edge-tts/issues +- Review health checks: `docker inspect edge-tts-web` + +## 📝 License + +This deployment configuration is part of the Edge TTS project. diff --git a/web/DOCKER_QUICKSTART.md b/web/DOCKER_QUICKSTART.md new file mode 100644 index 0000000..121c577 --- /dev/null +++ b/web/DOCKER_QUICKSTART.md @@ -0,0 +1,195 @@ +# Docker Quick Start Guide + +## 🚀 Deploy in 3 Steps + +### Step 1: Build +```bash +cd web +./build.sh +``` + +### Step 2: Deploy to Remote Server +```bash +# Replace with your server IP/hostname +REMOTE_HOST=192.168.1.100 ./deploy.sh +``` + +### Step 3: Access +``` +http://YOUR_SERVER_IP:8000 +``` + +## 📋 Common Commands + +### Local Development +```bash +# Build and start +docker-compose up -d + +# View logs +docker-compose logs -f + +# Stop +docker-compose down + +# Restart +docker-compose restart +``` + +### Remote Deployment +```bash +# Basic deployment +REMOTE_HOST=192.168.1.100 ./deploy.sh + +# Custom user and path +REMOTE_HOST=myserver.com \ +REMOTE_USER=deployer \ +REMOTE_PATH=/opt/edge-tts \ +./deploy.sh +``` + +### Monitoring +```bash +# Container status +docker-compose ps + +# Health check +docker inspect edge-tts-web --format='{{.State.Health.Status}}' + +# Resource usage +docker stats edge-tts-web +``` + +### Troubleshooting +```bash +# View logs +docker-compose logs --tail=100 + +# Restart container +docker-compose restart + +# Rebuild from scratch +docker-compose down +docker-compose build --no-cache +docker-compose up -d +``` + +## 🔧 Configuration + +### Change Port +Edit `docker-compose.yml`: +```yaml +ports: + - "3000:8000" # Change 3000 to your desired port +``` + +### Environment Variables +Create `.env` file: +```bash +PYTHONUNBUFFERED=1 +# Add your variables here +``` + +## 🌐 Production Setup + +### 1. Use Reverse Proxy (Recommended) + +**Nginx:** +```nginx +server { + listen 80; + server_name tts.yourdomain.com; + + location / { + proxy_pass http://localhost:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +**Install SSL:** +```bash +sudo certbot --nginx -d tts.yourdomain.com +``` + +### 2. Firewall Setup +```bash +sudo ufw allow 22/tcp # SSH +sudo ufw allow 80/tcp # HTTP +sudo ufw allow 443/tcp # HTTPS +sudo ufw enable +``` + +### 3. Auto-Updates (Optional) +```bash +# Add to crontab +0 2 * * * cd /opt/edge-tts && docker-compose pull && docker-compose up -d +``` + +## 📊 Monitoring + +### Check Health +```bash +curl http://localhost:8000/api/health +``` + +### View Metrics +```bash +docker stats edge-tts-web +``` + +## 🆘 Quick Fixes + +### Port Already in Use +```bash +# Find process using port +sudo lsof -i :8000 + +# Or change port in docker-compose.yml +``` + +### Permission Denied +```bash +sudo usermod -aG docker $USER +newgrp docker +``` + +### Container Won't Start +```bash +# Check logs +docker-compose logs + +# Rebuild +docker-compose build --no-cache +docker-compose up -d +``` + +## 📁 File Structure +``` +web/ +├── Dockerfile # Container definition +├── docker-compose.yml # Orchestration +├── build.sh # Build script +├── deploy.sh # Deploy script +├── server.py # Backend +└── [web files] # Frontend +``` + +## 🔗 Useful Links + +- Full Documentation: [DEPLOYMENT.md](DEPLOYMENT.md) +- Edge TTS Project: https://github.com/rany2/edge-tts +- Docker Docs: https://docs.docker.com + +## 💡 Tips + +1. **Always use reverse proxy in production** +2. **Enable SSL/TLS with Let's Encrypt** +3. **Set up monitoring and logging** +4. **Regular backups if you add persistent data** +5. **Keep Docker and images updated** + +--- + +**Need Help?** Check [DEPLOYMENT.md](DEPLOYMENT.md) for detailed instructions! diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..9480929 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,42 @@ +# Edge TTS Web UI - Production Dockerfile +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application files +COPY *.py . +COPY *.html . +COPY *.css . +COPY *.js . +COPY *.json . +COPY *.png . 2>/dev/null || true +COPY *.svg . 2>/dev/null || true + +# Create non-root user for security +RUN useradd -m -u 1000 appuser && \ + chown -R appuser:appuser /app + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" + +# Run the application +CMD ["python", "server.py", "--host", "0.0.0.0", "--port", "8000"] diff --git a/web/QUICKSTART.md b/web/QUICKSTART.md new file mode 100644 index 0000000..723f1da --- /dev/null +++ b/web/QUICKSTART.md @@ -0,0 +1,212 @@ +# Quick Start Guide + +## 🚀 Get Started in 3 Steps + +### 1. Install Dependencies + +```bash +cd web +pip install -r requirements.txt +``` + +### 2. Start the Server + +```bash +./start.sh +``` + +Or manually: +```bash +python3 server.py +``` + +### 3. Open Your Browser + +Visit: **http://localhost:8000** + +--- + +## ✨ Features at a Glance + +### Text to Speech +- Enter any text (up to 5000 characters) +- Select from 100+ voices in multiple languages +- Adjust speed, volume, and pitch +- Generate natural-sounding speech + +### Voice Selection +- Filter by language and gender +- Preview voice names and locales +- Save your favorite settings + +### Audio Controls +- Play audio directly in browser +- Download as MP3 files +- View generation history +- Quick reload from history + +### PWA Features +- Install as standalone app +- Offline support with service worker +- Works on desktop and mobile +- Responsive design + +--- + +## 📱 Install as App + +### On Desktop (Chrome/Edge) +1. Click the install icon in the address bar +2. Or look for "Install App" button in the UI +3. App will be added to your applications + +### On Mobile (Android) +1. Open in Chrome +2. Tap the menu (⋮) +3. Select "Add to Home screen" +4. App icon will appear on home screen + +### On iOS (Safari) +1. Tap the share button +2. Select "Add to Home Screen" +3. Name the app and add to home screen + +--- + +## 🎯 Quick Usage Tips + +### Generate Speech +1. Enter or paste text +2. Select a voice (default: English) +3. Adjust speed/volume/pitch if needed +4. Click "Generate Speech" +5. Audio player appears with playback controls + +### Download Audio +- Click "Download MP3" button +- File saves with timestamp and text snippet + +### Use History +- Recent generations saved automatically +- Click "Load" to restore settings +- Click "Delete" to remove from history + +### Filter Voices +- Use language dropdown for specific locales +- Use gender filter for Male/Female voices +- Voice list updates automatically + +--- + +## 🔧 Configuration + +### Change Port +```bash +python3 server.py --port 8080 +``` + +### Enable Hot Reload (Development) +```bash +python3 server.py --reload +``` + +### Bind to Specific Host +```bash +python3 server.py --host 127.0.0.1 +``` + +--- + +## ⚡ API Usage + +### Test with cURL + +Get voices: +```bash +curl http://localhost:8000/api/voices +``` + +Generate speech: +```bash +curl -X POST http://localhost:8000/api/synthesize \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Hello, world!", + "voice": "en-US-EmmaMultilingualNeural", + "rate": "+0%", + "volume": "+0%", + "pitch": "+0Hz" + }' \ + --output speech.mp3 +``` + +--- + +## 🎨 Customization + +### Update Theme Color + +Edit `styles.css`: +```css +:root { + --primary-color: #2563eb; /* Your color here */ +} +``` + +Update `manifest.json`: +```json +{ + "theme_color": "#2563eb" +} +``` + +### Replace Icons + +Create PNG icons: +- `icon-192.png` - 192x192 pixels +- `icon-512.png` - 512x512 pixels + +Use any image editing tool or online icon generator. + +--- + +## 🐛 Troubleshooting + +### Port Already in Use +```bash +python3 server.py --port 8080 +``` + +### Dependencies Not Found +```bash +pip3 install -r requirements.txt +``` + +### Voices Not Loading +- Check internet connection +- Check server logs for errors +- Try refreshing the page + +### Service Worker Issues +- Clear browser cache +- Hard refresh (Ctrl+Shift+R or Cmd+Shift+R) +- Check browser console for errors + +--- + +## 📚 More Information + +See [README.md](README.md) for detailed documentation including: +- Full API reference +- Deployment guide +- Docker setup +- Production considerations +- Contributing guidelines + +--- + +## 🎉 You're All Set! + +Enjoy using Edge TTS Web UI! + +For issues or questions, visit: https://github.com/rany2/edge-tts diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..b83837b --- /dev/null +++ b/web/README.md @@ -0,0 +1,297 @@ +# Edge TTS Web UI + +A Progressive Web App (PWA) for converting text to speech using Microsoft Edge's online TTS service. + +## Features + +- 🎙️ **Text to Speech**: Convert any text to natural-sounding speech +- 🌍 **Multiple Languages**: Support for 100+ voices in various languages +- 🎛️ **Voice Customization**: Adjust speed, volume, and pitch +- 📱 **PWA Support**: Install as an app on any device +- 💾 **Offline Support**: Service worker caching for offline usage +- 📝 **History**: Keep track of recent generations +- ⬇️ **Download**: Save generated audio as MP3 files + +## Installation + +### Prerequisites + +- Python 3.8 or higher +- pip (Python package manager) + +### Setup + +1. Navigate to the web directory: +```bash +cd web +``` + +2. Install dependencies: +```bash +pip install -r requirements.txt +``` + +## Usage + +### Start the Server + +```bash +python server.py +``` + +Or with custom options: +```bash +python server.py --host 0.0.0.0 --port 8000 +``` + +Options: +- `--host`: Host to bind to (default: 0.0.0.0) +- `--port`: Port to bind to (default: 8000) +- `--reload`: Enable auto-reload for development + +### Access the Web UI + +Open your browser and navigate to: +``` +http://localhost:8000 +``` + +### Install as PWA + +1. Open the web UI in a modern browser (Chrome, Edge, Safari, Firefox) +2. Look for the install prompt or click "Install App" button +3. The app will be added to your home screen/app drawer + +## API Endpoints + +The server provides the following REST API endpoints: + +### GET /api/health +Health check endpoint + +**Response:** +```json +{ + "status": "healthy", + "service": "edge-tts-api" +} +``` + +### GET /api/voices +Get list of all available voices + +**Response:** +```json +[ + { + "Name": "en-US-EmmaMultilingualNeural", + "ShortName": "en-US-EmmaMultilingualNeural", + "Gender": "Female", + "Locale": "en-US", + "LocaleName": "English (United States)", + ... + } +] +``` + +### POST /api/synthesize +Synthesize speech from text + +**Request Body:** +```json +{ + "text": "Hello, world!", + "voice": "en-US-EmmaMultilingualNeural", + "rate": "+0%", + "volume": "+0%", + "pitch": "+0Hz" +} +``` + +**Response:** +Returns MP3 audio file + +**Parameters:** +- `text` (required): Text to convert (max 5000 characters) +- `voice` (optional): Voice name (default: "en-US-EmmaMultilingualNeural") +- `rate` (optional): Speech rate from -100% to +100% (default: "+0%") +- `volume` (optional): Volume from -100% to +100% (default: "+0%") +- `pitch` (optional): Pitch from -500Hz to +500Hz (default: "+0Hz") + +### POST /api/synthesize-with-subtitles +Synthesize speech with subtitle generation + +**Request Body:** +Same as /api/synthesize + +**Response:** +```json +{ + "audio": "base64_encoded_audio_data", + "subtitles": "SRT formatted subtitles", + "format": "mp3" +} +``` + +## File Structure + +``` +web/ +├── index.html # Main HTML page +├── styles.css # Styles and theme +├── app.js # Client-side JavaScript +├── manifest.json # PWA manifest +├── sw.js # Service worker +├── server.py # FastAPI backend server +├── requirements.txt # Python dependencies +├── icon-192.png # App icon (192x192) +├── icon-512.png # App icon (512x512) +└── README.md # This file +``` + +## Development + +### Running in Development Mode + +```bash +python server.py --reload +``` + +This enables auto-reload when you modify the code. + +### Testing + +Test the API endpoints using curl: + +```bash +# Get voices +curl http://localhost:8000/api/voices + +# Synthesize speech +curl -X POST http://localhost:8000/api/synthesize \ + -H "Content-Type: application/json" \ + -d '{"text":"Hello world","voice":"en-US-EmmaMultilingualNeural"}' \ + --output speech.mp3 +``` + +### Customization + +#### Update Icons + +Replace `icon-192.png` and `icon-512.png` with your own icons. + +For best results, create: +- 192x192 PNG for mobile devices +- 512x512 PNG for high-resolution displays + +#### Update Theme Color + +Edit the `--primary-color` variable in [styles.css](styles.css): + +```css +:root { + --primary-color: #2563eb; /* Change this color */ +} +``` + +Also update `theme_color` in [manifest.json](manifest.json). + +## Browser Support + +### PWA Features +- ✅ Chrome/Edge (Desktop & Mobile) +- ✅ Safari (iOS 11.3+) +- ✅ Firefox (Desktop & Android) +- ✅ Samsung Internet + +### Service Worker +- ✅ All modern browsers +- ❌ IE11 (not supported) + +## Troubleshooting + +### Port Already in Use + +If port 8000 is already in use: +```bash +python server.py --port 8080 +``` + +### Icons Not Showing + +Make sure `icon-192.png` and `icon-512.png` exist in the web directory. + +### Voices Not Loading + +Check the server logs for errors. The server needs internet connection to fetch voices from Microsoft's API. + +### CORS Issues + +The server is configured to allow all origins for development. For production, update the CORS settings in [server.py](server.py): + +```python +app.add_middleware( + CORSMiddleware, + allow_origins=["https://yourdomain.com"], # Update this + ... +) +``` + +## Deployment + +### Production Considerations + +1. **Use a production ASGI server**: Uvicorn with multiple workers + ```bash + uvicorn server:app --host 0.0.0.0 --port 8000 --workers 4 + ``` + +2. **Use a reverse proxy**: nginx or Apache for SSL/TLS + +3. **Set environment variables**: + ```bash + export EDGE_TTS_HOST=0.0.0.0 + export EDGE_TTS_PORT=8000 + ``` + +4. **Update CORS settings**: Restrict to your domain + +5. **Enable HTTPS**: Required for PWA installation + +### Docker Deployment + +Create a `Dockerfile`: + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["python", "server.py", "--host", "0.0.0.0", "--port", "8000"] +``` + +Build and run: +```bash +docker build -t edge-tts-web . +docker run -p 8000:8000 edge-tts-web +``` + +## License + +This web UI is built on top of [edge-tts](https://github.com/rany2/edge-tts). + +## Contributing + +Contributions are welcome! Please feel free to submit issues or pull requests. + +## Credits + +- **edge-tts**: The underlying TTS library by [@rany2](https://github.com/rany2) +- **Microsoft Edge TTS**: The text-to-speech service diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..0c5f1bf --- /dev/null +++ b/web/app.js @@ -0,0 +1,594 @@ +// Configuration +const API_BASE_URL = window.location.hostname === 'localhost' + ? 'http://localhost:8000/api' + : '/api'; + +// Sample sentences for different languages +const SAMPLE_SENTENCES = { + 'ar': ['مرحبا، كيف حالك اليوم؟', 'الطقس جميل اليوم.', 'أتمنى لك يوما سعيدا.'], + 'bg': ['Здравейте, как сте днес?', 'Времето е хубаво днес.', 'Желая ви приятен ден.'], + 'ca': ['Hola, com estàs avui?', 'El temps és agradable avui.', 'Que tinguis un bon dia.'], + 'cs': ['Ahoj, jak se máš dnes?', 'Počasí je dnes pěkné.', 'Přeji ti hezký den.'], + 'da': ['Hej, hvordan har du det i dag?', 'Vejret er dejligt i dag.', 'Hav en god dag.'], + 'de': ['Hallo, wie geht es dir heute?', 'Das Wetter ist heute schön.', 'Ich wünsche dir einen schönen Tag.'], + 'el': ['Γεια σου, πώς είσαι σήμερα;', 'Ο καιρός είναι ωραίος σήμερα.', 'Σου εύχομαι μια όμορφη μέρα.'], + 'en': ['Hello, how are you today?', 'The weather is nice today.', 'Have a wonderful day!'], + 'es': ['Hola, ¿cómo estás hoy?', 'El clima está agradable hoy.', '¡Que tengas un buen día!'], + 'fi': ['Hei, mitä kuuluu tänään?', 'Sää on kaunis tänään.', 'Mukavaa päivää!'], + 'fr': ['Bonjour, comment allez-vous aujourd\'hui?', 'Le temps est agréable aujourd\'hui.', 'Passez une bonne journée!'], + 'hi': ['नमस्ते, आज आप कैसे हैं?', 'आज मौसम अच्छा है।', 'आपका दिन शुभ हो।'], + 'hr': ['Bok, kako si danas?', 'Vrijeme je lijepo danas.', 'Želim ti lijep dan.'], + 'hu': ['Szia, hogy vagy ma?', 'Az idő szép ma.', 'Szép napot kívánok!'], + 'id': ['Halo, apa kabar hari ini?', 'Cuacanya bagus hari ini.', 'Semoga harimu menyenangkan!'], + 'it': ['Ciao, come stai oggi?', 'Il tempo è bello oggi.', 'Ti auguro una buona giornata!'], + 'ja': ['こんにちは、今日はお元気ですか?', '今日は天気がいいですね。', '良い一日をお過ごしください。'], + 'ko': ['안녕하세요, 오늘은 어떠세요?', '오늘 날씨가 좋네요.', '좋은 하루 보내세요!'], + 'nl': ['Hallo, hoe gaat het vandaag?', 'Het weer is mooi vandaag.', 'Fijne dag gewenst!'], + 'no': ['Hei, hvordan har du det i dag?', 'Været er fint i dag.', 'Ha en fin dag!'], + 'pl': ['Cześć, jak się masz dzisiaj?', 'Pogoda jest ładna dzisiaj.', 'Miłego dnia!'], + 'pt': ['Olá, como você está hoje?', 'O tempo está agradável hoje.', 'Tenha um ótimo dia!'], + 'ro': ['Bună, ce mai faci astăzi?', 'Vremea este frumoasă astăzi.', 'O zi bună!'], + 'ru': ['Привет, как дела сегодня?', 'Погода сегодня хорошая.', 'Хорошего дня!'], + 'sk': ['Ahoj, ako sa máš dnes?', 'Počasie je dnes pekné.', 'Prajem ti pekný deň.'], + 'sv': ['Hej, hur mår du idag?', 'Vädret är fint idag.', 'Ha en trevlig dag!'], + 'th': ['สวัสดี วันนี้เป็นอย่างไรบ้าง?', 'อากาศดีวันนี้.', 'ขอให้มีความสุขตลอดวัน!'], + 'tr': ['Merhaba, bugün nasılsın?', 'Hava bugün güzel.', 'İyi günler dilerim!'], + 'uk': ['Привіт, як справи сьогодні?', 'Погода сьогодні гарна.', 'Гарного дня!'], + 'vi': ['Xin chào, hôm nay bạn thế nào?', 'Thời tiết hôm nay đẹp.', 'Chúc bạn một ngày tốt lành!'], + 'zh': ['你好,今天过得怎么样?', '今天天气真好。', '祝你有美好的一天!'], + // Cantonese (yue-CN) + 'yue': ['你好,今日點呀?', '今日天氣好好。', '祝你有美好嘅一天!'], + // Wu Chinese (wuu-CN) - uses Simplified Chinese + 'wuu': ['侬好,今朝好伐?', '今朝天气老好额。', '祝侬开心!'], +}; + +// State +let voices = []; +let filteredVoices = []; +let currentAudioUrl = null; +let currentTestAudioUrl = null; +let history = []; +let deferredPrompt = null; + +// DOM Elements +const textInput = document.getElementById('textInput'); +const charCount = document.getElementById('charCount'); +const voiceSelect = document.getElementById('voiceSelect'); +const languageSelect = document.getElementById('languageSelect'); +const genderFilter = document.getElementById('genderFilter'); +const rateSlider = document.getElementById('rateSlider'); +const rateValue = document.getElementById('rateValue'); +const volumeSlider = document.getElementById('volumeSlider'); +const volumeValue = document.getElementById('volumeValue'); +const pitchSlider = document.getElementById('pitchSlider'); +const pitchValue = document.getElementById('pitchValue'); +const generateBtn = document.getElementById('generateBtn'); +const clearBtn = document.getElementById('clearBtn'); +const testVoiceBtn = document.getElementById('testVoiceBtn'); +const progressBar = document.getElementById('progressBar'); +const statusMessage = document.getElementById('statusMessage'); +const audioSection = document.getElementById('audioSection'); +const audioPlayer = document.getElementById('audioPlayer'); +const downloadBtn = document.getElementById('downloadBtn'); +const historyList = document.getElementById('historyList'); +const onlineStatus = document.getElementById('onlineStatus'); +const installPrompt = document.getElementById('installPrompt'); +const installBtn = document.getElementById('installBtn'); + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + loadVoices(); + loadHistory(); + setupEventListeners(); + setupPWA(); + updateOnlineStatus(); +}); + +// Event Listeners +function setupEventListeners() { + textInput.addEventListener('input', updateCharCount); + + languageSelect.addEventListener('change', () => { + filterVoices(); + updateTestVoiceButton(); + }); + genderFilter.addEventListener('change', filterVoices); + voiceSelect.addEventListener('change', updateTestVoiceButton); + + rateSlider.addEventListener('input', (e) => { + const value = e.target.value; + rateValue.textContent = `${value >= 0 ? '+' : ''}${value}%`; + }); + + volumeSlider.addEventListener('input', (e) => { + const value = e.target.value; + volumeValue.textContent = `${value >= 0 ? '+' : ''}${value}%`; + }); + + pitchSlider.addEventListener('input', (e) => { + const value = e.target.value; + pitchValue.textContent = `${value >= 0 ? '+' : ''}${value}Hz`; + }); + + generateBtn.addEventListener('click', generateSpeech); + clearBtn.addEventListener('click', clearForm); + testVoiceBtn.addEventListener('click', testVoice); + downloadBtn.addEventListener('click', downloadAudio); + + window.addEventListener('online', updateOnlineStatus); + window.addEventListener('offline', updateOnlineStatus); +} + +// Character count +function updateCharCount() { + const count = textInput.value.length; + charCount.textContent = count; + + if (count > 4500) { + charCount.style.color = 'var(--error-color)'; + } else if (count > 4000) { + charCount.style.color = 'var(--primary-color)'; + } else { + charCount.style.color = ''; + } +} + +// Load voices from API +async function loadVoices() { + try { + // Add cache busting to ensure fresh data + const response = await fetch(`${API_BASE_URL}/voices?_=${Date.now()}`); + if (!response.ok) throw new Error('Failed to load voices'); + + voices = await response.json(); + populateLanguageSelect(); + + showStatus('Voices loaded successfully', 'success'); + console.log(`Loaded ${voices.length} voices from API`); + } catch (error) { + console.error('Error loading voices:', error); + showStatus('Failed to load voices. Please check the server connection.', 'error'); + } +} + +// Populate language select +function populateLanguageSelect() { + const languages = [...new Set(voices.map(v => v.Locale))].sort(); + + languageSelect.innerHTML = ''; + languages.forEach(lang => { + const option = document.createElement('option'); + option.value = lang; + option.textContent = getLanguageName(lang); + languageSelect.appendChild(option); + }); +} + +// Get language name from locale +function getLanguageName(locale) { + const names = voices.filter(v => v.Locale === locale); + return names.length > 0 ? names[0].LocaleName : locale; +} + +// Filter voices based on selected language and gender +function filterVoices() { + const selectedLanguage = languageSelect.value; + const selectedGender = genderFilter.value; + + // If no language is selected, clear voice dropdown + if (!selectedLanguage) { + voiceSelect.innerHTML = ''; + filteredVoices = []; + return; + } + + // Filter voices by language and gender + filteredVoices = voices.filter(voice => { + const languageMatch = voice.Locale === selectedLanguage; + const genderMatch = !selectedGender || voice.Gender === selectedGender; + return languageMatch && genderMatch; + }); + + populateVoiceSelect(); +} + +// Populate voice select +function populateVoiceSelect() { + voiceSelect.innerHTML = ''; + + if (filteredVoices.length === 0) { + voiceSelect.innerHTML = ''; + return; + } + + // Sort voices alphabetically by LocalName + const sortedVoices = [...filteredVoices].sort((a, b) => { + return a.LocalName.localeCompare(b.LocalName); + }); + + sortedVoices.forEach(voice => { + const option = document.createElement('option'); + option.value = voice.Name; + option.textContent = `${voice.LocalName} (${voice.Gender})`; + voiceSelect.appendChild(option); + }); +} + +// Generate speech +async function generateSpeech() { + const text = textInput.value.trim(); + + if (!text) { + showStatus('Please enter some text', 'error'); + return; + } + + if (text.length > 5000) { + showStatus('Text exceeds maximum length of 5000 characters', 'error'); + return; + } + + const voice = voiceSelect.value; + if (!voice) { + showStatus('Please select a voice', 'error'); + return; + } + + const params = { + text: text, + voice: voice, + rate: `${rateSlider.value >= 0 ? '+' : ''}${rateSlider.value}%`, + volume: `${volumeSlider.value >= 0 ? '+' : ''}${volumeSlider.value}%`, + pitch: `${pitchSlider.value >= 0 ? '+' : ''}${pitchSlider.value}Hz` + }; + + try { + generateBtn.disabled = true; + generateBtn.innerHTML = ' Generating...'; + progressBar.style.display = 'block'; + hideStatus(); + + const response = await fetch(`${API_BASE_URL}/synthesize`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: 'Failed to generate speech' })); + const errorMsg = error.detail || 'Failed to generate speech'; + + // Check if it's an invalid voice error + if (errorMsg.includes('No audio') || errorMsg.includes('voice')) { + throw new Error(`Voice error: ${errorMsg}. Try selecting a different voice or refresh the page.`); + } + throw new Error(errorMsg); + } + + const blob = await response.blob(); + + // Clean up previous audio URL + if (currentAudioUrl) { + URL.revokeObjectURL(currentAudioUrl); + } + + currentAudioUrl = URL.createObjectURL(blob); + audioPlayer.src = currentAudioUrl; + audioSection.style.display = 'block'; + + // Add to history + const selectedVoice = voices.find(v => v.Name === voice); + addToHistory({ + text: text, + voice: voice, + voiceName: voiceSelect.options[voiceSelect.selectedIndex].text, + locale: selectedVoice ? selectedVoice.Locale : '', + localeName: selectedVoice ? selectedVoice.LocaleName : '', + params: params, + timestamp: new Date().toISOString() + }); + + showStatus('Speech generated successfully!', 'success'); + + } catch (error) { + console.error('Error generating speech:', error); + showStatus(error.message, 'error'); + } finally { + generateBtn.disabled = false; + generateBtn.innerHTML = ' Generate Speech'; + progressBar.style.display = 'none'; + } +} + +// Download audio +function downloadAudio() { + if (!currentAudioUrl) return; + + const text = textInput.value.substring(0, 30).replace(/[^a-z0-9]/gi, '_'); + const filename = `edge-tts-${text}-${Date.now()}.mp3`; + + const a = document.createElement('a'); + a.href = currentAudioUrl; + a.download = filename; + a.click(); +} + +// Update test voice button state +function updateTestVoiceButton() { + const hasVoice = voiceSelect.value && voiceSelect.value !== ''; + testVoiceBtn.disabled = !hasVoice; +} + +// Get sample sentence for language +function getSampleSentence(locale) { + // Extract language code (e.g., 'en' from 'en-US') + const langCode = locale.split('-')[0]; + + // Check for exact locale match first (for special cases like yue-CN, wuu-CN) + if (SAMPLE_SENTENCES[locale]) { + const sentences = SAMPLE_SENTENCES[locale]; + return sentences[Math.floor(Math.random() * sentences.length)]; + } + + // Then check for language code match + if (SAMPLE_SENTENCES[langCode]) { + const sentences = SAMPLE_SENTENCES[langCode]; + return sentences[Math.floor(Math.random() * sentences.length)]; + } + + // Default to English + const sentences = SAMPLE_SENTENCES['en']; + return sentences[Math.floor(Math.random() * sentences.length)]; +} + +// Test voice with sample sentence +async function testVoice() { + const voice = voiceSelect.value; + const selectedLanguage = languageSelect.value; + + if (!voice || !selectedLanguage) { + showStatus('Please select a voice first', 'error'); + return; + } + + // Get sample sentence + const sampleText = getSampleSentence(selectedLanguage); + + const params = { + text: sampleText, + voice: voice, + rate: `${rateSlider.value >= 0 ? '+' : ''}${rateSlider.value}%`, + volume: `${volumeSlider.value >= 0 ? '+' : ''}${volumeSlider.value}%`, + pitch: `${pitchSlider.value >= 0 ? '+' : ''}${pitchSlider.value}Hz` + }; + + try { + testVoiceBtn.disabled = true; + testVoiceBtn.innerHTML = ' Testing...'; + + const response = await fetch(`${API_BASE_URL}/synthesize`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(params) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: 'Failed to generate test speech' })); + const errorMsg = error.detail || 'Failed to generate test speech'; + + // Check if it's an invalid voice error + if (errorMsg.includes('No audio') || errorMsg.includes('voice')) { + throw new Error(`Voice error: ${errorMsg}. This voice may not be available. Try refreshing the page.`); + } + throw new Error(errorMsg); + } + + const blob = await response.blob(); + + // Clean up previous test audio URL + if (currentTestAudioUrl) { + URL.revokeObjectURL(currentTestAudioUrl); + } + + currentTestAudioUrl = URL.createObjectURL(blob); + + // Play audio automatically + const testAudio = new Audio(currentTestAudioUrl); + testAudio.play(); + + showStatus(`Testing voice: "${sampleText}"`, 'info'); + + } catch (error) { + console.error('Error testing voice:', error); + showStatus(error.message, 'error'); + } finally { + testVoiceBtn.disabled = false; + testVoiceBtn.innerHTML = ' Test Voice'; + } +} + +// Clear form +function clearForm() { + textInput.value = ''; + updateCharCount(); + rateSlider.value = 0; + volumeSlider.value = 0; + pitchSlider.value = 0; + rateValue.textContent = '+0%'; + volumeValue.textContent = '+0%'; + pitchValue.textContent = '+0Hz'; + hideStatus(); +} + +// History management +function loadHistory() { + const saved = localStorage.getItem('tts_history'); + if (saved) { + history = JSON.parse(saved); + renderHistory(); + } +} + +function saveHistory() { + // Keep only last 10 items + history = history.slice(0, 10); + localStorage.setItem('tts_history', JSON.stringify(history)); +} + +function addToHistory(item) { + history.unshift(item); + saveHistory(); + renderHistory(); +} + +function renderHistory() { + if (history.length === 0) { + historyList.innerHTML = '
No recent generations yet
'; + return; + } + + historyList.innerHTML = ''; + + history.forEach((item, index) => { + const div = document.createElement('div'); + div.className = 'history-item'; + + const date = new Date(item.timestamp); + const timeAgo = getTimeAgo(date); + + const languageInfo = item.localeName ? ` - ${item.localeName}` : ''; + + div.innerHTML = ` +Text to Speech Converter
+Select a voice to test it with a sample sentence
+No recent generations yet
+