temporarily delete web_live directory to merge PR
This commit is contained in:
@@ -1,220 +0,0 @@
|
|||||||
# WhisperLive Remote Setup Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This setup allows you to run WhisperLive on your intranet machine and expose it through a public cloud instance via SSH remote port forwarding.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
```
|
|
||||||
Browser → Nginx (Cloud) → SSH Tunnel → WhisperLive Server (Intranet)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup Steps
|
|
||||||
|
|
||||||
### 1. WhisperLive Server Setup (Intranet Machine)
|
|
||||||
|
|
||||||
First, install and run WhisperLive on your intranet machine:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install WhisperLive
|
|
||||||
pip install whisper-live
|
|
||||||
# OR
|
|
||||||
git clone https://github.com/collabora/WhisperLive
|
|
||||||
# OR use this repo till final merge:
|
|
||||||
git clone https://github.com/klonikar/WhisperLive
|
|
||||||
|
|
||||||
# Start the server (default port 9090)
|
|
||||||
python run_server.py -fw deepdml/faster-whisper-large-v3-turbo-ct2
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. SSH Remote Port Forwarding
|
|
||||||
|
|
||||||
From your intranet machine, create an SSH tunnel to your cloud instance:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic SSH tunnel - forwards local port 9090 to cloud instance port 9090
|
|
||||||
ssh -R 9090:localhost:9090 user@your-cloud-instance.com
|
|
||||||
|
|
||||||
# Keep the tunnel alive with auto-reconnect
|
|
||||||
ssh -R 9090:localhost:9090 -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@your-cloud-instance.com
|
|
||||||
|
|
||||||
# Run in background with autossh (install autossh first)
|
|
||||||
autossh -M 0 -R 9090:localhost:9090 -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@your-cloud-instance.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Nginx Configuration (Cloud Instance)
|
|
||||||
|
|
||||||
Apply the nginx configuration provided in the artifacts:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Edit your nginx configuration
|
|
||||||
sudo nano /etc/nginx/sites-available/your-site
|
|
||||||
|
|
||||||
# Test the configuration
|
|
||||||
sudo nginx -t
|
|
||||||
|
|
||||||
# Reload nginx
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. SSL Certificate (Recommended)
|
|
||||||
|
|
||||||
For WebSocket connections over HTTPS, you'll need an SSL certificate:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Using Let's Encrypt with certbot
|
|
||||||
sudo apt install certbot python3-certbot-nginx
|
|
||||||
sudo certbot --nginx -d yourserver.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Firewall Configuration
|
|
||||||
|
|
||||||
Ensure your cloud instance firewall allows the necessary ports:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Allow HTTP and HTTPS
|
|
||||||
sudo ufw allow 80
|
|
||||||
sudo ufw allow 443
|
|
||||||
|
|
||||||
# If using a specific port for the tunnel
|
|
||||||
sudo ufw allow 9090
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing the Setup
|
|
||||||
|
|
||||||
### 0. Test client from filesystem
|
|
||||||
Simply open the file whisperlive_client.html from the file explorer and connect it to a whisperlive server on the localhost
|
|
||||||
|
|
||||||
### 1. Test WhisperLive Server
|
|
||||||
```bash
|
|
||||||
# On your intranet machine
|
|
||||||
curl http://localhost:9090/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Test SSH Tunnel
|
|
||||||
```bash
|
|
||||||
# On your cloud instance
|
|
||||||
curl http://localhost:9090/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Test Nginx Proxy
|
|
||||||
```bash
|
|
||||||
# From outside
|
|
||||||
curl http://yourserver.com/whisper-ws
|
|
||||||
```
|
|
||||||
|
|
||||||
## Browser Client Usage
|
|
||||||
|
|
||||||
1. Open the HTML page in your browser
|
|
||||||
2. Update the WebSocket URL to: `wss://yourserver.com/whisper-ws` (or `ws://` for HTTP)
|
|
||||||
3. Configure sample rate and language
|
|
||||||
4. Click "Start Recording" to begin transcription
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **WebSocket Connection Failed**
|
|
||||||
- Check if SSH tunnel is active
|
|
||||||
- Verify nginx configuration
|
|
||||||
- Check firewall settings
|
|
||||||
|
|
||||||
2. **Audio Not Streaming**
|
|
||||||
- Ensure microphone permissions are granted
|
|
||||||
- Check browser console for errors
|
|
||||||
- Verify audio format compatibility
|
|
||||||
|
|
||||||
3. **SSH Tunnel Disconnects**
|
|
||||||
- Use `autossh` for auto-reconnection
|
|
||||||
- Increase `ServerAliveInterval` settings
|
|
||||||
- Check network stability
|
|
||||||
|
|
||||||
### Debug Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if WhisperLive is running
|
|
||||||
ps aux | grep whisper
|
|
||||||
|
|
||||||
# Check SSH tunnel status
|
|
||||||
ps aux | grep ssh
|
|
||||||
|
|
||||||
# Check nginx logs
|
|
||||||
sudo tail -f /var/log/nginx/access.log
|
|
||||||
sudo tail -f /var/log/nginx/error.log
|
|
||||||
|
|
||||||
# Test WebSocket connection
|
|
||||||
wscat -c ws://localhost:9090 # Install wscat: npm install -g wscat
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Use SSH Key Authentication**
|
|
||||||
```bash
|
|
||||||
# Generate SSH key if not exists
|
|
||||||
ssh-keygen -t rsa -b 4096
|
|
||||||
|
|
||||||
# Copy to cloud instance
|
|
||||||
ssh-copy-id user@your-cloud-instance.com
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Restrict SSH Access**
|
|
||||||
```bash
|
|
||||||
# In /etc/ssh/sshd_config on cloud instance
|
|
||||||
AllowUsers your-username
|
|
||||||
PermitRootLogin no
|
|
||||||
PasswordAuthentication no
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Use SSL/TLS**
|
|
||||||
- Always use HTTPS in production
|
|
||||||
- Configure proper SSL certificates
|
|
||||||
- Use secure WebSocket connections (wss://)
|
|
||||||
|
|
||||||
## Performance Optimization
|
|
||||||
|
|
||||||
1. **Audio Quality Settings**
|
|
||||||
- Use 16kHz sample rate for better performance
|
|
||||||
- Enable noise suppression and echo cancellation
|
|
||||||
- Adjust chunk size based on network conditions
|
|
||||||
|
|
||||||
2. **Network Optimization**
|
|
||||||
- Use compression in SSH tunnel: `ssh -C -R ...`
|
|
||||||
- Optimize nginx buffer settings
|
|
||||||
- Consider using a VPN for better tunnel stability
|
|
||||||
|
|
||||||
3. **WhisperLive Settings**
|
|
||||||
```bash
|
|
||||||
# Start with optimized settings
|
|
||||||
python -m whisper_live.server \
|
|
||||||
--port 9090 \
|
|
||||||
--host 0.0.0.0 \
|
|
||||||
--model base \
|
|
||||||
--device cuda # if GPU available
|
|
||||||
```
|
|
||||||
|
|
||||||
## Systemd Service (Optional)
|
|
||||||
|
|
||||||
Create a systemd service for auto-starting the SSH tunnel:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# /etc/systemd/system/whisper-tunnel.service
|
|
||||||
[Unit]
|
|
||||||
Description=WhisperLive SSH Tunnel
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=your-username
|
|
||||||
ExecStart=/usr/bin/autossh -M 0 -R 9090:localhost:9090 -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@your-cloud-instance.com
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Enable and start the service
|
|
||||||
sudo systemctl enable whisper-tunnel.service
|
|
||||||
sudo systemctl start whisper-tunnel.service
|
|
||||||
sudo systemctl status whisper-tunnel.service
|
|
||||||
```
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Add this to your nginx server block configuration
|
|
||||||
# Usually located in /etc/nginx/sites-available/your-site or /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80; # or 443 for SSL
|
|
||||||
server_name yourserver.com; # Replace with your domain
|
|
||||||
|
|
||||||
# Your existing web application routes
|
|
||||||
location / {
|
|
||||||
# Your existing configuration
|
|
||||||
root /var/www/html;
|
|
||||||
index index.html index.htm;
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# WebSocket route for WhisperLive
|
|
||||||
location /whisper-ws {
|
|
||||||
# Proxy to localhost port where your remote port forwarding is set up
|
|
||||||
proxy_pass http://localhost:9090; # Replace 9090 with your forwarded port
|
|
||||||
|
|
||||||
# WebSocket specific headers
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# WebSocket timeout settings
|
|
||||||
proxy_connect_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
|
|
||||||
# Buffer settings for real-time streaming
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_request_buffering off;
|
|
||||||
|
|
||||||
# Optional: Add CORS headers if needed
|
|
||||||
add_header Access-Control-Allow-Origin *;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
|
|
||||||
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optional: Handle preflight requests for CORS
|
|
||||||
location = /whisper-ws {
|
|
||||||
if ($request_method = OPTIONS) {
|
|
||||||
add_header Access-Control-Allow-Origin *;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
|
|
||||||
add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization";
|
|
||||||
add_header Access-Control-Max-Age 86400;
|
|
||||||
add_header Content-Type "text/plain charset=UTF-8";
|
|
||||||
add_header Content-Length 0;
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# If you're using SSL (recommended), add this SSL configuration
|
|
||||||
# server {
|
|
||||||
# listen 443 ssl http2;
|
|
||||||
# server_name yourserver.com;
|
|
||||||
#
|
|
||||||
# ssl_certificate /path/to/your/cert.pem;
|
|
||||||
# ssl_certificate_key /path/to/your/private.key;
|
|
||||||
#
|
|
||||||
# # SSL configuration
|
|
||||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
# ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
|
||||||
# ssl_prefer_server_ciphers off;
|
|
||||||
#
|
|
||||||
# # Same location blocks as above
|
|
||||||
# location / {
|
|
||||||
# root /var/www/html;
|
|
||||||
# index index.html index.htm;
|
|
||||||
# try_files $uri $uri/ =404;
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# location /whisper-ws {
|
|
||||||
# proxy_pass http://localhost:9090;
|
|
||||||
# proxy_http_version 1.1;
|
|
||||||
# proxy_set_header Upgrade $http_upgrade;
|
|
||||||
# proxy_set_header Connection "upgrade";
|
|
||||||
# proxy_set_header Host $host;
|
|
||||||
# proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
#
|
|
||||||
# proxy_connect_timeout 60s;
|
|
||||||
# proxy_send_timeout 60s;
|
|
||||||
# proxy_read_timeout 60s;
|
|
||||||
#
|
|
||||||
# proxy_buffering off;
|
|
||||||
# proxy_request_buffering off;
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
||||||
# Redirect HTTP to HTTPS if using SSL
|
|
||||||
# server {
|
|
||||||
# listen 80;
|
|
||||||
# server_name yourserver.com;
|
|
||||||
# return 301 https://$server_name$request_uri;
|
|
||||||
# }
|
|
||||||
@@ -1,659 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WhisperLive Audio Streaming</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 15px;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
color: #4a5568;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-size: 2.5em;
|
|
||||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 12px 24px;
|
|
||||||
font-size: 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-btn {
|
|
||||||
background: linear-gradient(45deg, #48bb78, #38a169);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 15px rgba(72, 187, 120, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop-btn {
|
|
||||||
background: linear-gradient(45deg, #f56565, #e53e3e);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop-btn:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 15px rgba(245, 101, 101, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
text-align: center;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 18px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.disconnected {
|
|
||||||
background: linear-gradient(45deg, #fed7d7, #feb2b2);
|
|
||||||
color: #c53030;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.connecting {
|
|
||||||
background: linear-gradient(45deg, #feebc8, #fbd38d);
|
|
||||||
color: #c05621;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status.connected {
|
|
||||||
background: linear-gradient(45deg, #c6f6d5, #9ae6b4);
|
|
||||||
color: #2f855a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-visualizer {
|
|
||||||
width: 100%;
|
|
||||||
height: 100px;
|
|
||||||
background: #f7fafc;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 20px 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border: 2px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.volume-bar {
|
|
||||||
width: 80%;
|
|
||||||
height: 20px;
|
|
||||||
background: #e2e8f0;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.volume-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #48bb78, #38a169, #2f855a);
|
|
||||||
width: 0%;
|
|
||||||
transition: width 0.1s ease;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transcription {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
min-height: 200px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transcription h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #4a5568;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config label {
|
|
||||||
display: block;
|
|
||||||
margin: 10px 0 5px 0;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config input, .config select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background: #fed7d7;
|
|
||||||
color: #c53030;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-left: 4px solid #f56565;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.recording {
|
|
||||||
animation: pulse 1s infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>🎤 WhisperLive Audio Streaming</h1>
|
|
||||||
|
|
||||||
<div class="config">
|
|
||||||
<h3>Configuration</h3>
|
|
||||||
<label for="wsUrl">WebSocket URL:</label>
|
|
||||||
<!-- // Use wss://yourserver.com/whisper-ws in production -->
|
|
||||||
<input type="text" id="wsUrl" value="ws://localhost:9090" placeholder="ws://localhost:9090">
|
|
||||||
|
|
||||||
<label for="sampleRate">Sample Rate:</label>
|
|
||||||
<select id="sampleRate">
|
|
||||||
<option value="16000" selected>16000 Hz</option>
|
|
||||||
<option value="44100">44100 Hz</option>
|
|
||||||
<option value="48000">48000 Hz</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="language">Language:</label>
|
|
||||||
<select id="language">
|
|
||||||
<option value="en" selected>English</option>
|
|
||||||
<option value="es">Spanish</option>
|
|
||||||
<option value="fr">French</option>
|
|
||||||
<option value="de">German</option>
|
|
||||||
<option value="it">Italian</option>
|
|
||||||
<option value="pt">Portuguese</option>
|
|
||||||
<option value="auto">Auto-detect</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="audioMethod">Audio Processing Method:</label>
|
|
||||||
<select id="audioMethod">
|
|
||||||
<option value="webaudio" selected>Web Audio API (Direct PCM)</option>
|
|
||||||
<option value="mediarecorder">MediaRecorder (Encoded Audio)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button id="startBtn" class="start-btn">Start Recording</button>
|
|
||||||
<button id="stopBtn" class="stop-btn" disabled>Stop Recording</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="status" class="status disconnected">Disconnected</div>
|
|
||||||
|
|
||||||
<div class="audio-visualizer">
|
|
||||||
<div class="volume-bar">
|
|
||||||
<div id="volumeFill" class="volume-fill"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="transcription">
|
|
||||||
<h3>Live Transcription</h3>
|
|
||||||
<div id="transcriptionText">Click "Start Recording" to begin transcription...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="errorDiv" class="error" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
class WhisperLiveClient {
|
|
||||||
constructor() {
|
|
||||||
this.websocket = null;
|
|
||||||
this.mediaRecorder = null;
|
|
||||||
this.audioStream = null;
|
|
||||||
this.audioContext = null;
|
|
||||||
this.analyser = null;
|
|
||||||
this.dataArray = null;
|
|
||||||
this.isRecording = false;
|
|
||||||
this.isConnected = false;
|
|
||||||
this.scriptProcessor = null;
|
|
||||||
this.useWebAudioAPI = true; // Use Web Audio API for direct PCM processing
|
|
||||||
|
|
||||||
this.config = {
|
|
||||||
sampleRate: 16000,
|
|
||||||
language: 'en',
|
|
||||||
wsUrl: 'ws://localhost:9090' // Use wss://yourserver.com/whisper-ws in production
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.updateStatus('disconnected', `Disconnected from ${this.config.wsUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
document.getElementById('startBtn').addEventListener('click', () => this.startRecording());
|
|
||||||
document.getElementById('stopBtn').addEventListener('click', () => this.stopRecording());
|
|
||||||
|
|
||||||
// Configuration change listeners
|
|
||||||
document.getElementById('wsUrl').addEventListener('change', (e) => {
|
|
||||||
this.config.wsUrl = e.target.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('sampleRate').addEventListener('change', (e) => {
|
|
||||||
this.config.sampleRate = parseInt(e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('language').addEventListener('change', (e) => {
|
|
||||||
this.config.language = e.target.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('audioMethod').addEventListener('change', (e) => {
|
|
||||||
this.useWebAudioAPI = e.target.value === 'webaudio';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async startRecording() {
|
|
||||||
try {
|
|
||||||
this.hideError();
|
|
||||||
|
|
||||||
// Connect to WebSocket first
|
|
||||||
await this.connectWebSocket();
|
|
||||||
|
|
||||||
// Get microphone access
|
|
||||||
await this.initializeAudio();
|
|
||||||
|
|
||||||
// Start recording based on the method used
|
|
||||||
if (this.useWebAudioAPI) {
|
|
||||||
// Web Audio API is already processing in real-time
|
|
||||||
this.isRecording = true;
|
|
||||||
} else {
|
|
||||||
// Start MediaRecorder with smaller chunks for better real-time performance
|
|
||||||
this.mediaRecorder.start(250); // Send data every 250ms
|
|
||||||
this.isRecording = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateUI(true);
|
|
||||||
this.updateStatus('connected', 'Recording... 🎙️');
|
|
||||||
|
|
||||||
// Start audio visualization
|
|
||||||
this.startAudioVisualization();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.showError('Failed to start recording: ' + error.message);
|
|
||||||
console.error('Error starting recording:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async stopRecording() {
|
|
||||||
this.isRecording = false;
|
|
||||||
|
|
||||||
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
||||||
this.mediaRecorder.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.scriptProcessor) {
|
|
||||||
this.scriptProcessor.disconnect();
|
|
||||||
this.scriptProcessor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.audioStream) {
|
|
||||||
this.audioStream.getTracks().forEach(track => track.stop());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
||||||
this.audioContext.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.websocket) {
|
|
||||||
this.websocket.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateUI(false);
|
|
||||||
this.updateStatus('disconnected', `Disconnected from ${this.config.wsUrl}`);
|
|
||||||
this.updateVolumeBar(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
async connectWebSocket() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
console.log(`Connecting to WebSocket: ${this.config.wsUrl}`);
|
|
||||||
this.updateStatus('connecting', `Connecting to ${this.config.wsUrl}...`);
|
|
||||||
|
|
||||||
this.websocket = new WebSocket(this.config.wsUrl);
|
|
||||||
|
|
||||||
this.websocket.onopen = () => {
|
|
||||||
console.log(`WebSocket connected to: ${this.config.wsUrl}`);
|
|
||||||
this.isConnected = true;
|
|
||||||
|
|
||||||
// Generate a random UID
|
|
||||||
const uid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
||||||
|
|
||||||
// Send configuration in the required format
|
|
||||||
const configMessage = {
|
|
||||||
type: 'config',
|
|
||||||
language: this.config.language,
|
|
||||||
use_vad: true,
|
|
||||||
max_clients: 4,
|
|
||||||
max_connection_time: 600,
|
|
||||||
uid: uid,
|
|
||||||
task: 'transcribe',
|
|
||||||
sample_rate: this.config.sampleRate,
|
|
||||||
send_last_n_segments: 10,
|
|
||||||
no_speech_thresh: 0.45,
|
|
||||||
clip_audio: false,
|
|
||||||
same_output_threshold: 10,
|
|
||||||
initial_prompt: '',
|
|
||||||
vad_parameters: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.send(JSON.stringify(configMessage));
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const message = JSON.parse(event.data);
|
|
||||||
console.log('websocket message:', message);
|
|
||||||
this.handleWebSocketMessage(message);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing WebSocket message:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onerror = (error) => {
|
|
||||||
console.error(`WebSocket error for ${this.config.wsUrl}:`, error);
|
|
||||||
this.showError(`WebSocket connection error for ${this.config.wsUrl}`);
|
|
||||||
reject(new Error('WebSocket connection failed'));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onclose = () => {
|
|
||||||
console.log(`WebSocket disconnected from: ${this.config.wsUrl}`);
|
|
||||||
this.isConnected = false;
|
|
||||||
if (this.isRecording) {
|
|
||||||
this.updateStatus('disconnected', `Connection lost to ${this.config.wsUrl}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Timeout after 10 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this.isConnected) {
|
|
||||||
reject(new Error('WebSocket connection timeout'));
|
|
||||||
}
|
|
||||||
}, 10000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeAudio() {
|
|
||||||
try {
|
|
||||||
this.audioStream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: {
|
|
||||||
sampleRate: this.config.sampleRate,
|
|
||||||
channelCount: 1,
|
|
||||||
echoCancellation: true,
|
|
||||||
noiseSuppression: true,
|
|
||||||
autoGainControl: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup audio context for visualization and processing
|
|
||||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
|
||||||
sampleRate: this.config.sampleRate
|
|
||||||
});
|
|
||||||
|
|
||||||
this.analyser = this.audioContext.createAnalyser();
|
|
||||||
const source = this.audioContext.createMediaStreamSource(this.audioStream);
|
|
||||||
source.connect(this.analyser);
|
|
||||||
|
|
||||||
this.analyser.fftSize = 256;
|
|
||||||
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
|
||||||
|
|
||||||
if (this.useWebAudioAPI) {
|
|
||||||
// Use Web Audio API for direct PCM processing
|
|
||||||
this.setupWebAudioProcessing(source);
|
|
||||||
} else {
|
|
||||||
// Use MediaRecorder as fallback
|
|
||||||
this.setupMediaRecorder();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error('Microphone access denied or not available: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupWebAudioProcessing(source) {
|
|
||||||
// Create a script processor for real-time audio processing
|
|
||||||
const bufferSize = 4096;
|
|
||||||
this.scriptProcessor = this.audioContext.createScriptProcessor(bufferSize, 1, 1);
|
|
||||||
|
|
||||||
this.scriptProcessor.onaudioprocess = (event) => {
|
|
||||||
if (!this.isRecording || !this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputBuffer = event.inputBuffer;
|
|
||||||
const inputData = inputBuffer.getChannelData(0);
|
|
||||||
// Send PCM data directly
|
|
||||||
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
||||||
this.websocket.send(inputData.buffer)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Connect the audio processing chain
|
|
||||||
source.connect(this.scriptProcessor);
|
|
||||||
this.scriptProcessor.connect(this.audioContext.destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMediaRecorder() {
|
|
||||||
// Try to use the best available audio format
|
|
||||||
const mimeTypes = [
|
|
||||||
'audio/webm;codecs=pcm',
|
|
||||||
'audio/wav',
|
|
||||||
'audio/webm;codecs=opus',
|
|
||||||
'audio/webm',
|
|
||||||
'audio/mp4'
|
|
||||||
];
|
|
||||||
|
|
||||||
let selectedMimeType = '';
|
|
||||||
for (const mimeType of mimeTypes) {
|
|
||||||
if (MediaRecorder.isTypeSupported(mimeType)) {
|
|
||||||
selectedMimeType = mimeType;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedMimeType) {
|
|
||||||
throw new Error('No supported audio format found');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Using audio format:', selectedMimeType);
|
|
||||||
|
|
||||||
// Setup MediaRecorder
|
|
||||||
this.mediaRecorder = new MediaRecorder(this.audioStream, {
|
|
||||||
mimeType: selectedMimeType
|
|
||||||
});
|
|
||||||
|
|
||||||
this.mediaRecorder.ondataavailable = (event) => {
|
|
||||||
if (event.data.size > 0 && this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
||||||
this.sendAudioData(event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendAudioData(audioBlob) {
|
|
||||||
try {
|
|
||||||
// Convert blob to ArrayBuffer
|
|
||||||
const arrayBuffer = await audioBlob.arrayBuffer();
|
|
||||||
|
|
||||||
// Decode audio data to get raw PCM
|
|
||||||
const audioData = await this.decodeAudioData(arrayBuffer);
|
|
||||||
|
|
||||||
// Send raw PCM data
|
|
||||||
this.websocket.send(audioData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending audio data:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async decodeAudioData(arrayBuffer) {
|
|
||||||
try {
|
|
||||||
// Create a temporary audio context for decoding
|
|
||||||
const tempAudioContext = new (window.AudioContext || window.webkitAudioContext)({
|
|
||||||
sampleRate: this.config.sampleRate
|
|
||||||
});
|
|
||||||
|
|
||||||
// Decode the audio data
|
|
||||||
const audioBuffer = await tempAudioContext.decodeAudioData(arrayBuffer);
|
|
||||||
|
|
||||||
// Get the first channel (mono)
|
|
||||||
const channelData = audioBuffer.getChannelData(0);
|
|
||||||
|
|
||||||
// Close the temporary context
|
|
||||||
tempAudioContext.close();
|
|
||||||
|
|
||||||
return channelData.buffer;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error decoding audio data:', error);
|
|
||||||
// Fallback: send raw data
|
|
||||||
return arrayBuffer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWebSocketMessage(message) {
|
|
||||||
if(!message.segments) {
|
|
||||||
console.log('Ignoring non-transcription message:', message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let messageText = '';
|
|
||||||
message.segments.forEach((segment) => {
|
|
||||||
// each segment has start, end, text, and completed
|
|
||||||
//messageText += segment.completed ? segment.text : '';
|
|
||||||
messageText += segment.text;
|
|
||||||
});
|
|
||||||
this.updateTranscription({text: messageText});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTranscription(data) {
|
|
||||||
const transcriptionDiv = document.getElementById('transcriptionText');
|
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
|
||||||
|
|
||||||
if (data.text && data.text.trim()) {
|
|
||||||
const newText = `[${timestamp}] ${data.text}\n`;
|
|
||||||
transcriptionDiv.innerHTML += newText;
|
|
||||||
transcriptionDiv.scrollTop = transcriptionDiv.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startAudioVisualization() {
|
|
||||||
const animate = () => {
|
|
||||||
if (!this.isRecording) return;
|
|
||||||
|
|
||||||
this.analyser.getByteFrequencyData(this.dataArray);
|
|
||||||
|
|
||||||
// Calculate average volume
|
|
||||||
let sum = 0;
|
|
||||||
for (let i = 0; i < this.dataArray.length; i++) {
|
|
||||||
sum += this.dataArray[i];
|
|
||||||
}
|
|
||||||
const average = sum / this.dataArray.length;
|
|
||||||
const volume = (average / 255) * 100;
|
|
||||||
|
|
||||||
this.updateVolumeBar(volume);
|
|
||||||
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
};
|
|
||||||
|
|
||||||
animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVolumeBar(volume) {
|
|
||||||
const volumeFill = document.getElementById('volumeFill');
|
|
||||||
volumeFill.style.width = `${volume}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI(isRecording) {
|
|
||||||
document.getElementById('startBtn').disabled = isRecording;
|
|
||||||
document.getElementById('stopBtn').disabled = !isRecording;
|
|
||||||
|
|
||||||
const container = document.querySelector('.container');
|
|
||||||
if (isRecording) {
|
|
||||||
container.classList.add('recording');
|
|
||||||
} else {
|
|
||||||
container.classList.remove('recording');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable configuration inputs during recording
|
|
||||||
document.getElementById('wsUrl').disabled = isRecording;
|
|
||||||
document.getElementById('sampleRate').disabled = isRecording;
|
|
||||||
document.getElementById('language').disabled = isRecording;
|
|
||||||
document.getElementById('audioMethod').disabled = isRecording;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus(statusClass, statusText) {
|
|
||||||
const statusDiv = document.getElementById('status');
|
|
||||||
statusDiv.className = `status ${statusClass}`;
|
|
||||||
statusDiv.textContent = statusText;
|
|
||||||
}
|
|
||||||
|
|
||||||
showError(message) {
|
|
||||||
const errorDiv = document.getElementById('errorDiv');
|
|
||||||
errorDiv.textContent = message;
|
|
||||||
errorDiv.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
hideError() {
|
|
||||||
const errorDiv = document.getElementById('errorDiv');
|
|
||||||
errorDiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the client when the page loads
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
new WhisperLiveClient();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user