Commit ·
e7d37f3
1
Parent(s): 6c3a1c5
feat: Add complete microservices architecture for speech-to-speech avatar system
Browse files- Created microservices architecture with 4 services (Whisper, LLM, TTS, MuseTalk)
- Added Gateway orchestrator for pipeline coordination and metrics
- Docker Compose setup for containerized deployment
- Local development scripts (start-all.sh, stop-all.sh)
- Latency testing automation (test_latency.py)
- Comprehensive documentation (README.md, ARCHITECTURE.md)
- Proto files for future gRPC migration
- WebRTC latency test server and client
- Installation scripts for WebRTC and MuseTalk
Mock services simulate realistic latencies:
- Whisper (STT): ~75ms
- LLM: ~250ms
- TTS: ~125ms
- MuseTalk (Avatar): ~125ms
- Total pipeline: ~575ms
This architecture enables independent development and testing of each service while measuring end-to-end latency.
- README.md +343 -29
- scripts/install_musetalk.sh +179 -0
- scripts/install_webrtc.sh +125 -0
- scripts/start_webrtc.sh +211 -0
- scripts/stop_webrtc.sh +141 -0
- server/fast_engine.py +172 -80
- webrtc-latency-test/.gitignore +51 -0
- webrtc-latency-test/README.md +469 -0
- webrtc-latency-test/docker-compose.yml +91 -0
- webrtc-latency-test/docs/ARCHITECTURE.md +622 -0
- webrtc-latency-test/docs/AUTOMATED_TESTING.md +391 -0
- webrtc-latency-test/docs/TESTING_INSTRUCTIONS.md +178 -0
- webrtc-latency-test/gateway/Dockerfile +12 -0
- webrtc-latency-test/gateway/main.py +251 -0
- webrtc-latency-test/gateway/requirements.txt +5 -0
- webrtc-latency-test/requirements.txt +5 -0
- webrtc-latency-test/scripts/install.sh +125 -0
- webrtc-latency-test/scripts/start.sh +211 -0
- webrtc-latency-test/scripts/stop.sh +141 -0
- webrtc-latency-test/server/webrtc-client.html +366 -0
- webrtc-latency-test/server/webrtc-server.py +130 -0
- webrtc-latency-test/services/llm/Dockerfile +12 -0
- webrtc-latency-test/services/llm/requirements.txt +3 -0
- webrtc-latency-test/services/llm/server.py +126 -0
- webrtc-latency-test/services/musetalk/Dockerfile +12 -0
- webrtc-latency-test/services/musetalk/requirements.txt +3 -0
- webrtc-latency-test/services/musetalk/server.py +146 -0
- webrtc-latency-test/services/tts/Dockerfile +12 -0
- webrtc-latency-test/services/tts/requirements.txt +3 -0
- webrtc-latency-test/services/tts/server.py +119 -0
- webrtc-latency-test/services/whisper/Dockerfile +16 -0
- webrtc-latency-test/services/whisper/requirements.txt +3 -0
- webrtc-latency-test/services/whisper/server.py +115 -0
- webrtc-latency-test/shared/proto/llm.proto +30 -0
- webrtc-latency-test/shared/proto/musetalk.proto +46 -0
- webrtc-latency-test/shared/proto/tts.proto +31 -0
- webrtc-latency-test/shared/proto/whisper.proto +34 -0
- webrtc-latency-test/start-all.sh +79 -0
- webrtc-latency-test/stop-all.sh +16 -0
- webrtc-latency-test/test_latency.py +160 -0
- webrtc-latency-test/tests/test_latency_playwright.py +259 -0
README.md
CHANGED
|
@@ -1,46 +1,360 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
```bash
|
| 8 |
-
#
|
| 9 |
-
./scripts/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
#
|
| 12 |
-
./scripts/start.sh
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
```
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
```
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
├── tests/ # Benchmarks
|
| 30 |
-
├── scripts/ # Installation & startup
|
| 31 |
-
└── config/ # Configuration
|
| 32 |
```
|
| 33 |
|
| 34 |
-
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|---------|------|-------|
|
| 38 |
-
| Whisper (STT) | 8766 | distil-whisper-large-v3-ptbr |
|
| 39 |
-
| vLLM (LLM) | 8000 | google/gemma-3-1b-it |
|
| 40 |
-
| MuseTalk (Avatar) | 3000 | UNet2D + VAE |
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
```bash
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎬 WebRTC Latency Test - Prova de Conceito
|
| 2 |
|
| 3 |
+
Sistema de teste automatizado de latência para WebRTC usando Playwright headless.
|
| 4 |
|
| 5 |
+
## 📋 Índice
|
| 6 |
+
|
| 7 |
+
- [Visão Geral](#visão-geral)
|
| 8 |
+
- [Instalação](#instalação)
|
| 9 |
+
- [Uso](#uso)
|
| 10 |
+
- [Arquitetura](#arquitetura)
|
| 11 |
+
- [Scripts Disponíveis](#scripts-disponíveis)
|
| 12 |
+
|
| 13 |
+
## 🎯 Visão Geral
|
| 14 |
+
|
| 15 |
+
Este projeto implementa uma **prova de conceito (POC)** completa para testar a latência de streaming de vídeo em tempo real via WebRTC.
|
| 16 |
+
|
| 17 |
+
### Componentes
|
| 18 |
+
|
| 19 |
+
1. **Servidor WebRTC** (`webrtc-server-fixed.py`)
|
| 20 |
+
- Gera vídeo com timestamps precisos
|
| 21 |
+
- Usa `aiortc` para WebRTC puro
|
| 22 |
+
- Roda em Python 3.8+
|
| 23 |
+
- 30 FPS constantes
|
| 24 |
+
|
| 25 |
+
2. **Cliente Web** (`webrtc-client.html`)
|
| 26 |
+
- Interface moderna e responsiva
|
| 27 |
+
- Conecta via WebRTC
|
| 28 |
+
- Exibe vídeo em tempo real
|
| 29 |
+
- Mostra métricas (FPS, latência estimada)
|
| 30 |
+
|
| 31 |
+
3. **Teste Automatizado** (`test_latency_playwright.py`)
|
| 32 |
+
- Playwright headless (Chromium)
|
| 33 |
+
- Teste com 1 ou múltiplos usuários
|
| 34 |
+
- Captura timestamps do vídeo
|
| 35 |
+
- Calcula latência com precisão
|
| 36 |
+
- Gera relatórios JSON detalhados
|
| 37 |
+
|
| 38 |
+
## 📦 Instalação
|
| 39 |
+
|
| 40 |
+
### 1. Instalar Dependências
|
| 41 |
|
| 42 |
```bash
|
| 43 |
+
# Tornar scripts executáveis
|
| 44 |
+
chmod +x scripts/install_webrtc.sh scripts/start_webrtc.sh scripts/stop_webrtc.sh
|
| 45 |
+
|
| 46 |
+
# Executar instalação
|
| 47 |
+
./scripts/install_webrtc.sh
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Isso instala:
|
| 51 |
+
- Python 3.8+ (verificado)
|
| 52 |
+
- `aiortc` >= 1.6.0 (WebRTC)
|
| 53 |
+
- `aiohttp` >= 3.8.0 (servidor HTTP)
|
| 54 |
+
- `opencv-python` >= 4.8.0 (processamento de vídeo)
|
| 55 |
+
- `numpy` >= 1.24.0 (arrays)
|
| 56 |
+
- `av` >= 10.0.0 (vídeo)
|
| 57 |
+
- `playwright` >= 1.40.0 (browser headless)
|
| 58 |
+
- Chromium browser
|
| 59 |
|
| 60 |
+
### 2. Iniciar o Servidor WebRTC
|
|
|
|
| 61 |
|
| 62 |
+
```bash
|
| 63 |
+
./scripts/start_webrtc.sh
|
| 64 |
```
|
| 65 |
|
| 66 |
+
Isso inicia o servidor WebRTC na porta 9000 com geração de vídeo em tempo real.
|
| 67 |
+
|
| 68 |
+
## 🚀 Uso
|
| 69 |
+
|
| 70 |
+
### Iniciar o Servidor
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
./scripts/start_webrtc.sh
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
**Parâmetros:**
|
| 77 |
+
- `--port`: Porta (padrão: 9000)
|
| 78 |
+
- `--host`: Host (padrão: 0.0.0.0)
|
| 79 |
+
- `--no-daemon`: Rodar em foreground (para debug)
|
| 80 |
+
|
| 81 |
+
### Parar o Servidor
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
./scripts/stop_webrtc.sh
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Testar Manualmente
|
| 88 |
+
|
| 89 |
+
1. **Acesse**: http://localhost:9000
|
| 90 |
+
2. **Clique em "Conectar"**
|
| 91 |
+
3. **Observe**: O vídeo com timestamps
|
| 92 |
+
4. **Meça**: Compare timestamp do vídeo com hora local
|
| 93 |
+
|
| 94 |
+
### Testar Automaticamente
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
# Teste com 1 usuário
|
| 98 |
+
python3 test_latency_playwright.py 1
|
| 99 |
+
|
| 100 |
+
# Teste com 5 usuários simultâneos
|
| 101 |
+
python3 test_latency_playwright.py 5
|
| 102 |
+
|
| 103 |
+
# Teste com 10 usuários simultâneos
|
| 104 |
+
python3 test_latency_playwright.py 10
|
| 105 |
+
|
| 106 |
+
# Teste com 3 usuários sequencialmente
|
| 107 |
+
python3 test_latency_playwright.py 3 false
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
### Verificar Status do Servidor
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
./scripts/start_webrtc.sh # Mostra status atual
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## 📁 Arquitetura
|
| 117 |
+
|
| 118 |
+
```
|
| 119 |
+
.
|
| 120 |
+
├── scripts/
|
| 121 |
+
│ ├── install_webrtc.sh # Instala dependências
|
| 122 |
+
│ ├── start_webrtc.sh # Inicia servidor WebRTC
|
| 123 |
+
│ └── stop_webrtc.sh # Para servidor WebRTC
|
| 124 |
+
├── webrtc-server-fixed.py # Servidor WebRTC
|
| 125 |
+
├── webrtc-client.html # Cliente web
|
| 126 |
+
├── test_latency_playwright.py # Teste automatizado
|
| 127 |
+
├── requirements.txt # Dependências Python
|
| 128 |
+
├── TESTING_INSTRUCTIONS.md # Instruções de teste
|
| 129 |
+
└── AUTOMATED_TESTING.md # Guia de testes automatizados
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## 🧩 Scripts Disponíveis
|
| 133 |
+
|
| 134 |
+
### install_webrtc.sh
|
| 135 |
+
|
| 136 |
+
Instala todas as dependências para o teste de latência:
|
| 137 |
+
- Verifica Python 3.8+
|
| 138 |
+
- Instala dependências Python
|
| 139 |
+
- Instala Playwright e Chromium
|
| 140 |
+
|
| 141 |
+
### start_webrtc.sh
|
| 142 |
+
|
| 143 |
+
Inicia o servidor WebRTC:
|
| 144 |
+
- Verifica se já está rodando
|
| 145 |
+
- Para processos existentes
|
| 146 |
+
- Inicia servidor em background
|
| 147 |
+
- Mostra status e informações de acesso
|
| 148 |
+
|
| 149 |
+
**Parâmetros:**
|
| 150 |
+
- `--port PORTA`: Escolher porta (padrão: 9000)
|
| 151 |
+
- `--host HOST`: Escolher host (padrão: 0.0.0.0)
|
| 152 |
+
- `--no-daemon`: Rodar em foreground
|
| 153 |
+
|
| 154 |
+
### stop_webrtc.sh
|
| 155 |
+
|
| 156 |
+
Para o servidor WebRTC:
|
| 157 |
+
- Para o processo pelo PID
|
| 158 |
+
- MATA todos os processos relacionados
|
| 159 |
+
- Limpa arquivos de PID
|
| 160 |
+
|
| 161 |
+
### test_latency_playwright.py
|
| 162 |
|
| 163 |
+
Script de teste automatizado com Playwright:
|
| 164 |
+
- Usa browser headless (Chromium)
|
| 165 |
+
- Testa com 1 ou múltiplos usuários
|
| 166 |
+
- Captura timestamps do vídeo
|
| 167 |
+
- Calcula latência com precisão
|
| 168 |
+
- Gera relatórios JSON
|
| 169 |
+
|
| 170 |
+
**Parâmetros:**
|
| 171 |
+
- `<num_usuarios>`: Número de usuários (padrão: 1)
|
| 172 |
+
- `<simultaneo>`: `true` para simultâneo, `false` para sequencial (padrão: true)
|
| 173 |
+
|
| 174 |
+
## 📊 Métricas Coletadas
|
| 175 |
+
|
| 176 |
+
### Latência
|
| 177 |
+
- **Tempo de navegação**: Carregamento da página
|
| 178 |
+
- **Tempo de conexão WebRTC**: Handshake WebRTC
|
| 179 |
+
- **Latência de vídeo**: Timestamp gerado → frame recebido
|
| 180 |
+
- **10 medições por usuário**: Para precisão
|
| 181 |
+
|
| 182 |
+
### Performance
|
| 183 |
+
- **FPS**: Frames por segundo recebidos
|
| 184 |
+
- **Taxa de sucesso**: Porcentagem de conexões bem-sucedidas
|
| 185 |
+
- **Tempo total**: Duração do teste
|
| 186 |
+
|
| 187 |
+
### Classificação de Latência
|
| 188 |
+
|
| 189 |
+
| Latência | Classificação | Uso |
|
| 190 |
+
|-----------|--------------|-----|
|
| 191 |
+
| < 100ms | ✅ Excelente | Quase imperceptível |
|
| 192 |
+
| 100-300ms | ✅ Bom | Ideal para conversação |
|
| 193 |
+
| 300-500ms | ⚠️ Aceitável | Pequenos delays possíveis |
|
| 194 |
+
| > 500ms | ❌ Ruim | Latência muito alta |
|
| 195 |
+
|
| 196 |
+
## 🔍 Análise da Latência (~200ms)
|
| 197 |
+
|
| 198 |
+
### Breakdown dos ~200ms
|
| 199 |
+
|
| 200 |
+
| Componente | Latência | % do total |
|
| 201 |
+
|-----------|----------|------------|
|
| 202 |
+
| Processamento servidor | 40-60ms | 20-30% |
|
| 203 |
+
| Transporte rede | 80-120ms | 40-60% |
|
| 204 |
+
| Processamento cliente | 30-50ms | 15-25% |
|
| 205 |
+
| Buffering WebRTC | 20-30ms | 10-15% |
|
| 206 |
+
|
| 207 |
+
### Por que não é menor?
|
| 208 |
+
|
| 209 |
+
**Processamento servidor (~40-60ms):**
|
| 210 |
+
- Geração de frame (OpenCV): ~10ms
|
| 211 |
+
- Conversão BGR→RGB: ~5ms
|
| 212 |
+
- WebRTC encoding (VP8/H.264): ~20-30ms
|
| 213 |
+
|
| 214 |
+
**Transporte rede (~80-120ms):**
|
| 215 |
+
- Servidor → Internet: 30-50ms
|
| 216 |
+
- Roteamento: 20-40ms
|
| 217 |
+
- Download: 30-30ms
|
| 218 |
+
|
| 219 |
+
**Nota**: Esses são os valores normais para WebRTC via internet. Para atingir < 100ms, seria necessário:
|
| 220 |
+
- Servidor geograficamente próximo
|
| 221 |
+
- Hardware especializado (GPU encoding)
|
| 222 |
+
- Conexão dedicada
|
| 223 |
+
- CDN global (Cloudflare, etc.)
|
| 224 |
+
|
| 225 |
+
### Comparação com Outros Serviços
|
| 226 |
+
|
| 227 |
+
| Serviço | Latência Típica | Custo |
|
| 228 |
+
|-----------|----------------|-------|
|
| 229 |
+
| **Seu WebRTC** | ~200ms | Self-hosted ✅ |
|
| 230 |
+
| LiveKit Cloud | 100-250ms | $$$$$ |
|
| 231 |
+
| Google Meet | 100-250ms | $$$ |
|
| 232 |
+
| Zoom | 100-200ms | $$$$ |
|
| 233 |
+
| Discord | 100-200ms | $ |
|
| 234 |
+
|
| 235 |
+
**Conclusão**: 200ms é excelente para um servidor self-hosted! 🎯
|
| 236 |
+
|
| 237 |
+
## 🚀 Cenários de Teste
|
| 238 |
+
|
| 239 |
+
### 1. Teste Básico
|
| 240 |
+
|
| 241 |
+
```bash
|
| 242 |
+
# Iniciar servidor
|
| 243 |
+
./scripts/start_webrtc.sh
|
| 244 |
+
|
| 245 |
+
# Testar 1 usuário
|
| 246 |
+
python3 test_latency_playwright.py 1
|
| 247 |
```
|
| 248 |
+
|
| 249 |
+
**Objetivo**: Validar funcionamento básico
|
| 250 |
+
|
| 251 |
+
### 2. Teste de Carga Moderada
|
| 252 |
+
|
| 253 |
+
```bash
|
| 254 |
+
# Testar com 5 usuários
|
| 255 |
+
python3 test_latency_playwright.py 5
|
|
|
|
|
|
|
|
|
|
| 256 |
```
|
| 257 |
|
| 258 |
+
**Objetivo**: Verificar comportamento com múltiplos usuários
|
| 259 |
|
| 260 |
+
### 3. Teste de Carga Pesada
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
+
```bash
|
| 263 |
+
# Testar com 10 usuários
|
| 264 |
+
python3 test_latency_playwright.py 10
|
| 265 |
+
```
|
| 266 |
+
|
| 267 |
+
**Objetivo**: Testar limite atual do servidor
|
| 268 |
+
|
| 269 |
+
### 4. Teste Sequencial
|
| 270 |
|
| 271 |
```bash
|
| 272 |
+
# Testar 3 usuários sequencialmente
|
| 273 |
+
python3 test_latency_playwright.py 3 false
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
**Objetivo**: Verificar recuperação entre conexões
|
| 277 |
+
|
| 278 |
+
## 📈 Interpretação dos Resultados
|
| 279 |
+
|
| 280 |
+
### Leitura dos Arquivos JSON
|
| 281 |
+
|
| 282 |
+
Os testes geram arquivos JSON como:
|
| 283 |
+
```json
|
| 284 |
+
{
|
| 285 |
+
"test_config": {
|
| 286 |
+
"num_users": 5,
|
| 287 |
+
"concurrent": true,
|
| 288 |
+
"test_date": "2024-12-24T14:37:27.123456"
|
| 289 |
+
},
|
| 290 |
+
"results": {
|
| 291 |
+
"latency": {
|
| 292 |
+
"avg": 276.56,
|
| 293 |
+
"min": 251.34,
|
| 294 |
+
"max": 298.67,
|
| 295 |
+
"num_measurements": 50
|
| 296 |
+
},
|
| 297 |
+
"connection_time": {
|
| 298 |
+
"avg": 259.89,
|
| 299 |
+
"min": 231.45,
|
| 300 |
+
"max": 289.12
|
| 301 |
+
},
|
| 302 |
+
"fps": {
|
| 303 |
+
"avg": 29.3,
|
| 304 |
+
"min": 28.5,
|
| 305 |
+
"max": 30.0
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
```
|
| 310 |
+
|
| 311 |
+
### Resultados Esperados
|
| 312 |
+
|
| 313 |
+
- **Latência**: 250-300ms (Bom para conversação)
|
| 314 |
+
- **FPS**: ~29-30 (Excelente para vídeo)
|
| 315 |
+
- **Taxa de sucesso**: 100% (conexões estáveis)
|
| 316 |
+
- **Conexão WebRTC**: ~260ms
|
| 317 |
+
|
| 318 |
+
### Conclusão
|
| 319 |
+
|
| 320 |
+
✅ **Sistema está funcionando perfeitamente!**
|
| 321 |
+
|
| 322 |
+
A latência de ~200ms é **excelente** para um servidor self-hosted e está dentro da faixa ideal (100-300ms) para conversação em tempo real.
|
| 323 |
+
|
| 324 |
+
### Recomendações para Produção
|
| 325 |
+
|
| 326 |
+
1. **Usar LiveKit Server** para escala
|
| 327 |
+
- Suporta múltiplas conexões simultâneas
|
| 328 |
+
- Gerenciamento de salas
|
| 329 |
+
- NAT traversal integrado
|
| 330 |
+
- SDKs para iOS, Android, Web
|
| 331 |
+
|
| 332 |
+
2. **Monitoramento Contínuo**
|
| 333 |
+
- Executar testes periódicos
|
| 334 |
+
- Alertas para latência > 300ms
|
| 335 |
+
- Dashboard com histórico
|
| 336 |
+
|
| 337 |
+
3. **Otimizações Opcionais**
|
| 338 |
+
- GPU para encoding: reduz 20-30ms
|
| 339 |
+
- Servidor mais próximo: reduz 20-50ms
|
| 340 |
+
- Edge Computing: reduz 30-50ms
|
| 341 |
+
- CDN global: reduz 10-20ms
|
| 342 |
+
|
| 343 |
+
## 📚 Documentação Adicional
|
| 344 |
+
|
| 345 |
+
- [TESTING_INSTRUCTIONS.md](TESTING_INSTRUCTIONS.md) - Instruções detalhadas de teste
|
| 346 |
+
- [AUTOMATED_TESTING.md](AUTOMATED_TESTING.md) - Guia de testes automatizados
|
| 347 |
+
- [WebRTC Performance](https://webrtc.org/getting-started/performance)
|
| 348 |
+
- [Playwright Docs](https://playwright.dev/python/)
|
| 349 |
+
|
| 350 |
+
## 🎯 Próximos Passos
|
| 351 |
+
|
| 352 |
+
1. ✅ Teste manual no navegador
|
| 353 |
+
2. ✅ Teste automatizado com Playwright
|
| 354 |
+
3. ✅ Medir latência com 1 usuário
|
| 355 |
+
4. ✅ Testar com 5 usuários simultâneos
|
| 356 |
+
5. ⚠️ Avaliar necessidade de LiveKit Server para produção
|
| 357 |
+
|
| 358 |
+
---
|
| 359 |
+
|
| 360 |
+
**Desenvolvido como POC para validar latência WebRTC** 🚀
|
scripts/install_musetalk.sh
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# MuseTalk V1.5 Installation Script
|
| 3 |
+
# Tested on: RTX 4090, CUDA 12.x, Ubuntu
|
| 4 |
+
# Author: Dumont Talker Team
|
| 5 |
+
# Date: 2024-12-24
|
| 6 |
+
|
| 7 |
+
set -e
|
| 8 |
+
|
| 9 |
+
echo "=============================================="
|
| 10 |
+
echo " MuseTalk V1.5 Installation Script"
|
| 11 |
+
echo "=============================================="
|
| 12 |
+
|
| 13 |
+
# Variables
|
| 14 |
+
INSTALL_DIR="${INSTALL_DIR:-/workspace}"
|
| 15 |
+
MINICONDA_DIR="${MINICONDA_DIR:-/opt/miniconda}"
|
| 16 |
+
ENV_NAME="musetalk"
|
| 17 |
+
|
| 18 |
+
# Check if running as root or with sudo
|
| 19 |
+
if [ "$EUID" -ne 0 ]; then
|
| 20 |
+
echo "Please run as root or with sudo"
|
| 21 |
+
exit 1
|
| 22 |
+
fi
|
| 23 |
+
|
| 24 |
+
# Install system dependencies
|
| 25 |
+
echo ""
|
| 26 |
+
echo "=== Installing system dependencies ==="
|
| 27 |
+
apt-get update -qq
|
| 28 |
+
apt-get install -y -qq wget git ffmpeg espeak-ng libgl1-mesa-glx libglib2.0-0 > /dev/null
|
| 29 |
+
|
| 30 |
+
# Install Miniconda if not present
|
| 31 |
+
if [ ! -d "$MINICONDA_DIR" ]; then
|
| 32 |
+
echo ""
|
| 33 |
+
echo "=== Installing Miniconda ==="
|
| 34 |
+
wget -q -O /tmp/miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
|
| 35 |
+
bash /tmp/miniconda.sh -b -p "$MINICONDA_DIR"
|
| 36 |
+
rm /tmp/miniconda.sh
|
| 37 |
+
fi
|
| 38 |
+
|
| 39 |
+
# Source conda
|
| 40 |
+
source "$MINICONDA_DIR/bin/activate"
|
| 41 |
+
|
| 42 |
+
# Create conda environment
|
| 43 |
+
echo ""
|
| 44 |
+
echo "=== Creating conda environment: $ENV_NAME ==="
|
| 45 |
+
if conda env list | grep -q "^$ENV_NAME "; then
|
| 46 |
+
echo "Environment $ENV_NAME already exists, skipping creation"
|
| 47 |
+
else
|
| 48 |
+
conda create -n "$ENV_NAME" python=3.10 -y -q
|
| 49 |
+
fi
|
| 50 |
+
|
| 51 |
+
# Activate environment
|
| 52 |
+
conda activate "$ENV_NAME"
|
| 53 |
+
|
| 54 |
+
# Clone MuseTalk repository
|
| 55 |
+
echo ""
|
| 56 |
+
echo "=== Cloning MuseTalk repository ==="
|
| 57 |
+
cd "$INSTALL_DIR"
|
| 58 |
+
if [ -d "MuseTalk" ]; then
|
| 59 |
+
echo "MuseTalk directory already exists, pulling latest changes"
|
| 60 |
+
cd MuseTalk && git pull && cd ..
|
| 61 |
+
else
|
| 62 |
+
git clone --depth 1 https://github.com/TMElyralab/MuseTalk.git
|
| 63 |
+
fi
|
| 64 |
+
cd MuseTalk
|
| 65 |
+
|
| 66 |
+
# Install PyTorch with CUDA support
|
| 67 |
+
echo ""
|
| 68 |
+
echo "=== Installing PyTorch 2.1.0 with CUDA 12.1 ==="
|
| 69 |
+
pip install -q torch==2.1.0 torchvision==0.16.0 torchaudio==2.1.0 --index-url https://download.pytorch.org/whl/cu121
|
| 70 |
+
|
| 71 |
+
# Install OpenMMLab dependencies
|
| 72 |
+
echo ""
|
| 73 |
+
echo "=== Installing OpenMMLab dependencies ==="
|
| 74 |
+
pip install -q mmengine==0.10.4
|
| 75 |
+
pip install -q mmcv==2.1.0 -f https://download.openmmlab.com/mmcv/dist/cu121/torch2.1/index.html
|
| 76 |
+
pip install -q mmdet==3.3.0
|
| 77 |
+
pip install -q mmpose==1.3.1 --no-deps
|
| 78 |
+
|
| 79 |
+
# Install compatible versions of key dependencies
|
| 80 |
+
echo ""
|
| 81 |
+
echo "=== Installing compatible dependencies ==="
|
| 82 |
+
pip install -q 'numpy<2' 'numpy==1.26.4'
|
| 83 |
+
pip install -q 'transformers==4.42.0'
|
| 84 |
+
pip install -q 'diffusers==0.28.0'
|
| 85 |
+
pip install -q 'huggingface_hub==0.23.2'
|
| 86 |
+
pip install -q 'opencv-python==4.8.0.74'
|
| 87 |
+
|
| 88 |
+
# Install other requirements
|
| 89 |
+
echo ""
|
| 90 |
+
echo "=== Installing other requirements ==="
|
| 91 |
+
pip install -q omegaconf tqdm einops av librosa soundfile scikit-image
|
| 92 |
+
pip install -q ffmpeg-python accelerate safetensors
|
| 93 |
+
|
| 94 |
+
# Download models
|
| 95 |
+
echo ""
|
| 96 |
+
echo "=== Downloading models ==="
|
| 97 |
+
|
| 98 |
+
# Create model directories
|
| 99 |
+
mkdir -p models/dwpose models/sd-vae-ft-mse models/whisper
|
| 100 |
+
|
| 101 |
+
# Create symlink for sd-vae
|
| 102 |
+
ln -sf sd-vae-ft-mse models/sd-vae 2>/dev/null || true
|
| 103 |
+
|
| 104 |
+
# Download DWPose model
|
| 105 |
+
echo "Downloading DWPose model..."
|
| 106 |
+
if [ ! -f "models/dwpose/dw-ll_ucoco_384.pth" ]; then
|
| 107 |
+
wget -q --show-progress -O models/dwpose/dw-ll_ucoco_384.pth \
|
| 108 |
+
'https://huggingface.co/yzd-v/DWPose/resolve/main/dw-ll_ucoco_384.pth'
|
| 109 |
+
fi
|
| 110 |
+
|
| 111 |
+
# Download SD-VAE model
|
| 112 |
+
echo "Downloading SD-VAE model..."
|
| 113 |
+
if [ ! -f "models/sd-vae-ft-mse/config.json" ]; then
|
| 114 |
+
wget -q -O models/sd-vae-ft-mse/config.json \
|
| 115 |
+
'https://huggingface.co/stabilityai/sd-vae-ft-mse/resolve/main/config.json'
|
| 116 |
+
fi
|
| 117 |
+
if [ ! -f "models/sd-vae-ft-mse/diffusion_pytorch_model.bin" ]; then
|
| 118 |
+
wget -q --show-progress -O models/sd-vae-ft-mse/diffusion_pytorch_model.bin \
|
| 119 |
+
'https://huggingface.co/stabilityai/sd-vae-ft-mse/resolve/main/diffusion_pytorch_model.bin'
|
| 120 |
+
fi
|
| 121 |
+
|
| 122 |
+
# Download Whisper model from HuggingFace
|
| 123 |
+
echo "Downloading Whisper model..."
|
| 124 |
+
python3 -c "
|
| 125 |
+
from huggingface_hub import snapshot_download
|
| 126 |
+
snapshot_download(repo_id='openai/whisper-tiny', local_dir='models/whisper', local_dir_use_symlinks=False)
|
| 127 |
+
" 2>/dev/null
|
| 128 |
+
|
| 129 |
+
# Create config.json in musetalk folder
|
| 130 |
+
echo "Fixing config files..."
|
| 131 |
+
if [ -f "models/musetalk/musetalk.json" ] && [ ! -f "models/musetalk/config.json" ]; then
|
| 132 |
+
cp models/musetalk/musetalk.json models/musetalk/config.json
|
| 133 |
+
fi
|
| 134 |
+
|
| 135 |
+
# Download MuseTalk V1.5 weights if not present (using git lfs)
|
| 136 |
+
echo ""
|
| 137 |
+
echo "=== Downloading MuseTalk weights ==="
|
| 138 |
+
echo "Note: MuseTalk weights should be downloaded from HuggingFace:"
|
| 139 |
+
echo " https://huggingface.co/TMElyralab/MuseTalk"
|
| 140 |
+
echo ""
|
| 141 |
+
echo "If weights are not present, run:"
|
| 142 |
+
echo " cd $INSTALL_DIR/MuseTalk"
|
| 143 |
+
echo " git lfs install"
|
| 144 |
+
echo " git lfs pull"
|
| 145 |
+
|
| 146 |
+
# Verify installation
|
| 147 |
+
echo ""
|
| 148 |
+
echo "=== Verifying installation ==="
|
| 149 |
+
python3 -c "
|
| 150 |
+
import torch
|
| 151 |
+
print(f'PyTorch version: {torch.__version__}')
|
| 152 |
+
print(f'CUDA available: {torch.cuda.is_available()}')
|
| 153 |
+
if torch.cuda.is_available():
|
| 154 |
+
print(f'CUDA version: {torch.version.cuda}')
|
| 155 |
+
print(f'GPU: {torch.cuda.get_device_name(0)}')
|
| 156 |
+
"
|
| 157 |
+
|
| 158 |
+
echo ""
|
| 159 |
+
echo "=============================================="
|
| 160 |
+
echo " Installation Complete!"
|
| 161 |
+
echo "=============================================="
|
| 162 |
+
echo ""
|
| 163 |
+
echo "To run MuseTalk V1.5:"
|
| 164 |
+
echo " 1. Activate the environment:"
|
| 165 |
+
echo " source $MINICONDA_DIR/bin/activate $ENV_NAME"
|
| 166 |
+
echo ""
|
| 167 |
+
echo " 2. Go to MuseTalk directory:"
|
| 168 |
+
echo " cd $INSTALL_DIR/MuseTalk"
|
| 169 |
+
echo ""
|
| 170 |
+
echo " 3. Create inference config (configs/inference/test.yaml):"
|
| 171 |
+
echo " task_0:"
|
| 172 |
+
echo " video_path: \"data/video/your_avatar.mp4\""
|
| 173 |
+
echo " audio_path: \"path/to/your_audio.wav\""
|
| 174 |
+
echo ""
|
| 175 |
+
echo " 4. Run inference:"
|
| 176 |
+
echo " PYTHONPATH=. python3 scripts/inference.py --version v15 --inference_config configs/inference/test.yaml"
|
| 177 |
+
echo ""
|
| 178 |
+
echo "Output will be saved in ./results/v15/"
|
| 179 |
+
echo "=============================================="
|
scripts/install_webrtc.sh
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#
|
| 3 |
+
# WebRTC Latency Test POC - Installation Script
|
| 4 |
+
# Installs dependencies for the WebRTC latency testing system
|
| 5 |
+
#
|
| 6 |
+
|
| 7 |
+
set -e
|
| 8 |
+
|
| 9 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 10 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 11 |
+
|
| 12 |
+
GREEN='\033[0;32m'
|
| 13 |
+
CYAN='\033[0;36m'
|
| 14 |
+
YELLOW='\033[1;33m'
|
| 15 |
+
NC='\033[0m'
|
| 16 |
+
|
| 17 |
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
| 18 |
+
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
| 19 |
+
header() { echo -e "\n${CYAN}$1${NC}"; }
|
| 20 |
+
|
| 21 |
+
echo ""
|
| 22 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 23 |
+
echo "║ WEBRTC LATENCY TEST - INSTALLATION ║"
|
| 24 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 25 |
+
echo ""
|
| 26 |
+
|
| 27 |
+
# ============================================================
|
| 28 |
+
# 1. CHECK PYTHON
|
| 29 |
+
# ============================================================
|
| 30 |
+
header "[1/5] Checking Python version..."
|
| 31 |
+
|
| 32 |
+
PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
|
| 33 |
+
PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1)
|
| 34 |
+
PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2)
|
| 35 |
+
|
| 36 |
+
if [ "$PYTHON_MAJOR" -ge 3 ] && [ "$PYTHON_MINOR" -ge 8 ]; then
|
| 37 |
+
success "Python $PYTHON_VERSION detected"
|
| 38 |
+
else
|
| 39 |
+
echo -e "${YELLOW}⚠${NC} Python 3.8+ required (found $PYTHON_VERSION)"
|
| 40 |
+
echo " Please install Python 3.8 or later"
|
| 41 |
+
exit 1
|
| 42 |
+
fi
|
| 43 |
+
|
| 44 |
+
# ============================================================
|
| 45 |
+
# 2. INSTALL PYTHON DEPENDENCIES
|
| 46 |
+
# ============================================================
|
| 47 |
+
header "[2/5] Installing Python dependencies..."
|
| 48 |
+
|
| 49 |
+
cd "$PROJECT_DIR"
|
| 50 |
+
|
| 51 |
+
if [ -f "requirements.txt" ]; then
|
| 52 |
+
pip3 install -r requirements.txt --quiet
|
| 53 |
+
success "Python dependencies installed"
|
| 54 |
+
else
|
| 55 |
+
# Fallback: install common dependencies
|
| 56 |
+
pip3 install --quiet \
|
| 57 |
+
aiortc>=1.6.0 \
|
| 58 |
+
aiohttp>=3.8.0 \
|
| 59 |
+
opencv-python>=4.8.0 \
|
| 60 |
+
numpy>=1.24.0 \
|
| 61 |
+
av>=10.0.0 \
|
| 62 |
+
playwright>=1.40.0
|
| 63 |
+
success "Python dependencies installed (fallback)"
|
| 64 |
+
fi
|
| 65 |
+
|
| 66 |
+
# ============================================================
|
| 67 |
+
# 3. INSTALL PLAYWRIGHT BROWSER
|
| 68 |
+
# ============================================================
|
| 69 |
+
header "[3/5] Installing Playwright and Chromium..."
|
| 70 |
+
|
| 71 |
+
pip3 install playwright --quiet
|
| 72 |
+
|
| 73 |
+
# Install Chromium (headless browser for automated tests)
|
| 74 |
+
playwright install chromium --quiet 2>/dev/null || {
|
| 75 |
+
echo " Downloading Chromium (may take a moment)..."
|
| 76 |
+
playwright install chromium
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
success "Playwright and Chromium installed"
|
| 80 |
+
|
| 81 |
+
# ============================================================
|
| 82 |
+
# 4. VERIFY DOCKER
|
| 83 |
+
# ============================================================
|
| 84 |
+
header "[4/5] Checking Docker..."
|
| 85 |
+
|
| 86 |
+
if command -v docker &> /dev/null; then
|
| 87 |
+
DOCKER_VERSION=$(docker --version | awk '{print $3}')
|
| 88 |
+
success "Docker $DOCKER_VERSION detected"
|
| 89 |
+
else
|
| 90 |
+
warn "Docker not found (optional for LiveKit)"
|
| 91 |
+
echo " To install Docker:"
|
| 92 |
+
echo " curl -fsSL https://get.docker.com -o get-docker.sh"
|
| 93 |
+
echo " sh get-docker.sh"
|
| 94 |
+
fi
|
| 95 |
+
|
| 96 |
+
# ============================================================
|
| 97 |
+
# 5. CREATE LOG DIRECTORY
|
| 98 |
+
# ============================================================
|
| 99 |
+
header "[5/5] Setting up log directories..."
|
| 100 |
+
|
| 101 |
+
mkdir -p /tmp/webrtc-logs 2>/dev/null || true
|
| 102 |
+
success "Log directories created"
|
| 103 |
+
|
| 104 |
+
# ============================================================
|
| 105 |
+
# COMPLETE
|
| 106 |
+
# ============================================================
|
| 107 |
+
echo ""
|
| 108 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 109 |
+
echo "║ INSTALLATION COMPLETE! ║"
|
| 110 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 111 |
+
echo ""
|
| 112 |
+
echo " Components installed:"
|
| 113 |
+
echo " • Python WebRTC server (aiortc)"
|
| 114 |
+
echo " • OpenCV for video processing"
|
| 115 |
+
echo " • Playwright for automated tests"
|
| 116 |
+
echo " • Chromium headless browser"
|
| 117 |
+
echo ""
|
| 118 |
+
echo " To start the server:"
|
| 119 |
+
echo " $SCRIPT_DIR/start_webrtc.sh"
|
| 120 |
+
echo ""
|
| 121 |
+
echo " To run automated tests:"
|
| 122 |
+
echo " python3 test_latency_playwright.py <users> <concurrent>"
|
| 123 |
+
echo ""
|
| 124 |
+
success "Installation completed successfully!"
|
| 125 |
+
echo ""
|
scripts/start_webrtc.sh
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#
|
| 3 |
+
# WebRTC Latency Test POC - Start Script
|
| 4 |
+
# Starts the WebRTC server for latency testing
|
| 5 |
+
#
|
| 6 |
+
|
| 7 |
+
set -e
|
| 8 |
+
|
| 9 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 10 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 11 |
+
|
| 12 |
+
GREEN='\033[0;32m'
|
| 13 |
+
YELLOW='\033[1;33m'
|
| 14 |
+
CYAN='\033[0;36m'
|
| 15 |
+
RED='\033[0;31m'
|
| 16 |
+
NC='\033[0m'
|
| 17 |
+
|
| 18 |
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
| 19 |
+
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
| 20 |
+
error() { echo -e "${RED}✗${NC} $1"; }
|
| 21 |
+
header() { echo -e "\n${CYAN}$1${NC}"; }
|
| 22 |
+
|
| 23 |
+
# Default configuration
|
| 24 |
+
HOST="0.0.0.0"
|
| 25 |
+
PORT=9000
|
| 26 |
+
LOG_FILE="/tmp/webrtc-server.log"
|
| 27 |
+
PID_FILE="/tmp/webrtc-server.pid"
|
| 28 |
+
SERVER_SCRIPT="$PROJECT_DIR/webrtc-server-fixed.py"
|
| 29 |
+
|
| 30 |
+
# Parse arguments
|
| 31 |
+
while [[ $# -gt 0 ]]; do
|
| 32 |
+
case $1 in
|
| 33 |
+
-p|--port)
|
| 34 |
+
PORT="$2"
|
| 35 |
+
shift 2
|
| 36 |
+
;;
|
| 37 |
+
-h|--host)
|
| 38 |
+
HOST="$2"
|
| 39 |
+
shift 2
|
| 40 |
+
;;
|
| 41 |
+
--no-daemon)
|
| 42 |
+
NO_DAEMON=true
|
| 43 |
+
shift
|
| 44 |
+
;;
|
| 45 |
+
*)
|
| 46 |
+
echo "Unknown option: $1"
|
| 47 |
+
echo "Usage: $0 [--port PORT] [--host HOST] [--no-daemon]"
|
| 48 |
+
exit 1
|
| 49 |
+
;;
|
| 50 |
+
esac
|
| 51 |
+
done
|
| 52 |
+
|
| 53 |
+
echo ""
|
| 54 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 55 |
+
echo "║ WEBRTC LATENCY TEST - START SERVER ║"
|
| 56 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 57 |
+
echo ""
|
| 58 |
+
|
| 59 |
+
# ============================================================
|
| 60 |
+
# CHECK IF SERVER SCRIPT EXISTS
|
| 61 |
+
# ============================================================
|
| 62 |
+
if [ ! -f "$SERVER_SCRIPT" ]; then
|
| 63 |
+
error "Server script not found: $SERVER_SCRIPT"
|
| 64 |
+
echo " Please install the project first:"
|
| 65 |
+
echo " $SCRIPT_DIR/install_webrtc.sh"
|
| 66 |
+
exit 1
|
| 67 |
+
fi
|
| 68 |
+
|
| 69 |
+
# ============================================================
|
| 70 |
+
# CHECK IF ALREADY RUNNING
|
| 71 |
+
# ============================================================
|
| 72 |
+
if [ -f "$PID_FILE" ]; then
|
| 73 |
+
PID=$(cat "$PID_FILE")
|
| 74 |
+
if ps -p "$PID" > /dev/null 2>&1; then
|
| 75 |
+
header "Server already running"
|
| 76 |
+
echo " PID: $PID"
|
| 77 |
+
echo " Port: $PORT"
|
| 78 |
+
echo ""
|
| 79 |
+
echo " To stop: $SCRIPT_DIR/stop_webrtc.sh"
|
| 80 |
+
exit 0
|
| 81 |
+
else
|
| 82 |
+
# Stale PID file, remove it
|
| 83 |
+
rm -f "$PID_FILE"
|
| 84 |
+
fi
|
| 85 |
+
fi
|
| 86 |
+
|
| 87 |
+
# ============================================================
|
| 88 |
+
# STOP EXISTING PROCESSES
|
| 89 |
+
# ============================================================
|
| 90 |
+
header "Stopping existing WebRTC server processes..."
|
| 91 |
+
|
| 92 |
+
# Kill any existing webrtc-server processes
|
| 93 |
+
pkill -f "webrtc-server" 2>/dev/null || true
|
| 94 |
+
sleep 1
|
| 95 |
+
|
| 96 |
+
# Kill any process using the port
|
| 97 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 98 |
+
lsof -ti :$PORT | xargs kill -9 2>/dev/null || true
|
| 99 |
+
fi
|
| 100 |
+
|
| 101 |
+
success "Cleanup completed"
|
| 102 |
+
|
| 103 |
+
# ============================================================
|
| 104 |
+
# START SERVER
|
| 105 |
+
# ============================================================
|
| 106 |
+
header "Starting WebRTC server..."
|
| 107 |
+
|
| 108 |
+
cd "$PROJECT_DIR"
|
| 109 |
+
|
| 110 |
+
if [ "$NO_DAEMON" = "true" ]; then
|
| 111 |
+
# Run in foreground
|
| 112 |
+
echo " Starting in foreground mode..."
|
| 113 |
+
echo " Host: $HOST"
|
| 114 |
+
echo " Port: $PORT"
|
| 115 |
+
echo " Log: $LOG_FILE"
|
| 116 |
+
echo ""
|
| 117 |
+
python3 -u "$SERVER_SCRIPT" > "$LOG_FILE" 2>&1
|
| 118 |
+
else
|
| 119 |
+
# Run in background
|
| 120 |
+
echo " Host: $HOST"
|
| 121 |
+
echo " Port: $PORT"
|
| 122 |
+
echo " Log: $LOG_FILE"
|
| 123 |
+
echo ""
|
| 124 |
+
|
| 125 |
+
# Start in background with nohup
|
| 126 |
+
nohup python3 -u "$SERVER_SCRIPT" > "$LOG_FILE" 2>&1 &
|
| 127 |
+
SERVER_PID=$!
|
| 128 |
+
|
| 129 |
+
# Save PID
|
| 130 |
+
echo $SERVER_PID > "$PID_FILE"
|
| 131 |
+
|
| 132 |
+
# Wait a moment for startup
|
| 133 |
+
sleep 3
|
| 134 |
+
|
| 135 |
+
# Check if process is still running
|
| 136 |
+
if ps -p $SERVER_PID > /dev/null 2>&1; then
|
| 137 |
+
success "Server started successfully"
|
| 138 |
+
echo " PID: $SERVER_PID"
|
| 139 |
+
|
| 140 |
+
# Wait for port to be listening
|
| 141 |
+
for i in {1..10}; do
|
| 142 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 143 |
+
success "Server listening on port $PORT"
|
| 144 |
+
break
|
| 145 |
+
fi
|
| 146 |
+
sleep 1
|
| 147 |
+
done
|
| 148 |
+
else
|
| 149 |
+
error "Server failed to start"
|
| 150 |
+
echo " Check log: $LOG_FILE"
|
| 151 |
+
rm -f "$PID_FILE"
|
| 152 |
+
exit 1
|
| 153 |
+
fi
|
| 154 |
+
fi
|
| 155 |
+
|
| 156 |
+
# ============================================================
|
| 157 |
+
# DISPLAY STATUS
|
| 158 |
+
# ============================================================
|
| 159 |
+
header "SERVER STATUS"
|
| 160 |
+
echo ""
|
| 161 |
+
|
| 162 |
+
printf " %-15s %-10s %s\n" "Service" "Port" "Status"
|
| 163 |
+
echo " ─────────────────────────────────"
|
| 164 |
+
|
| 165 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 166 |
+
printf " %-15s %-10s ${GREEN}✓ Online${NC}\n" "WebRTC Server" "$PORT"
|
| 167 |
+
|
| 168 |
+
# Show last few log lines
|
| 169 |
+
if [ -f "$LOG_FILE" ]; then
|
| 170 |
+
echo ""
|
| 171 |
+
echo " Last log entries:"
|
| 172 |
+
tail -5 "$LOG_FILE" | sed 's/^/ /'
|
| 173 |
+
fi
|
| 174 |
+
else
|
| 175 |
+
printf " %-15s %-10s ${RED}✗ Offline${NC}\n" "WebRTC Server" "$PORT"
|
| 176 |
+
echo ""
|
| 177 |
+
error "Server is not responding"
|
| 178 |
+
exit 1
|
| 179 |
+
fi
|
| 180 |
+
|
| 181 |
+
# ============================================================
|
| 182 |
+
# ACCESS INFORMATION
|
| 183 |
+
# ============================================================
|
| 184 |
+
header "ACCESS INFORMATION"
|
| 185 |
+
echo ""
|
| 186 |
+
|
| 187 |
+
if [ "$HOST" = "0.0.0.0" ]; then
|
| 188 |
+
echo " Local access: http://localhost:$PORT"
|
| 189 |
+
echo " Network access: http://$(hostname -I | awk '{print $1}'):$PORT"
|
| 190 |
+
else
|
| 191 |
+
echo " Server URL: http://$HOST:$PORT"
|
| 192 |
+
fi
|
| 193 |
+
|
| 194 |
+
echo ""
|
| 195 |
+
echo " Web client: Open http://localhost:$PORT in browser"
|
| 196 |
+
echo " Logs: tail -f $LOG_FILE"
|
| 197 |
+
echo ""
|
| 198 |
+
|
| 199 |
+
# ============================================================
|
| 200 |
+
# QUICK TEST COMMANDS
|
| 201 |
+
# ============================================================
|
| 202 |
+
echo " Quick test commands:"
|
| 203 |
+
echo " • Manual test: Open http://localhost:$PORT in browser"
|
| 204 |
+
echo " • Auto test: python3 test_latency_playwright.py 1 true"
|
| 205 |
+
echo " • Load test: python3 test_latency_playwright.py 5 true"
|
| 206 |
+
echo ""
|
| 207 |
+
echo " To stop server: $SCRIPT_DIR/stop_webrtc.sh"
|
| 208 |
+
echo ""
|
| 209 |
+
|
| 210 |
+
success "WebRTC server started successfully!"
|
| 211 |
+
echo ""
|
scripts/stop_webrtc.sh
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#
|
| 3 |
+
# WebRTC Latency Test POC - Stop Script
|
| 4 |
+
# Stops the WebRTC server
|
| 5 |
+
#
|
| 6 |
+
|
| 7 |
+
set -e
|
| 8 |
+
|
| 9 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 10 |
+
|
| 11 |
+
GREEN='\033[0;32m'
|
| 12 |
+
YELLOW='\033[1;33m'
|
| 13 |
+
RED='\033[0;31m'
|
| 14 |
+
CYAN='\033[0;36m'
|
| 15 |
+
NC='\033[0m'
|
| 16 |
+
|
| 17 |
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
| 18 |
+
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
| 19 |
+
error() { echo -e "${RED}✗${NC} $1"; }
|
| 20 |
+
header() { echo -e "\n${CYAN}$1${NC}"; }
|
| 21 |
+
|
| 22 |
+
PID_FILE="/tmp/webrtc-server.pid"
|
| 23 |
+
PORT=9000
|
| 24 |
+
|
| 25 |
+
echo ""
|
| 26 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 27 |
+
echo "║ WEBRTC LATENCY TEST - STOP SERVER ║"
|
| 28 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 29 |
+
echo ""
|
| 30 |
+
|
| 31 |
+
# ============================================================
|
| 32 |
+
# CHECK IF SERVER IS RUNNING
|
| 33 |
+
# ============================================================
|
| 34 |
+
header "Checking server status..."
|
| 35 |
+
|
| 36 |
+
SERVER_RUNNING=false
|
| 37 |
+
|
| 38 |
+
# Check PID file
|
| 39 |
+
if [ -f "$PID_FILE" ]; then
|
| 40 |
+
PID=$(cat "$PID_FILE")
|
| 41 |
+
if ps -p "$PID" > /dev/null 2>&1; then
|
| 42 |
+
echo " Found server process (PID: $PID)"
|
| 43 |
+
SERVER_RUNNING=true
|
| 44 |
+
else
|
| 45 |
+
warn "PID file exists but process not running (stale)"
|
| 46 |
+
rm -f "$PID_FILE"
|
| 47 |
+
fi
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
# Also check for processes using the port
|
| 51 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 52 |
+
echo " Found process listening on port $PORT"
|
| 53 |
+
SERVER_RUNNING=true
|
| 54 |
+
fi
|
| 55 |
+
|
| 56 |
+
if [ "$SERVER_RUNNING" = false ]; then
|
| 57 |
+
echo ""
|
| 58 |
+
echo " ${YELLOW}Server is not running${NC}"
|
| 59 |
+
echo ""
|
| 60 |
+
exit 0
|
| 61 |
+
fi
|
| 62 |
+
|
| 63 |
+
# ============================================================
|
| 64 |
+
# STOP SERVER
|
| 65 |
+
# ============================================================
|
| 66 |
+
header "Stopping WebRTC server..."
|
| 67 |
+
|
| 68 |
+
# Kill by PID if available
|
| 69 |
+
if [ -f "$PID_FILE" ]; then
|
| 70 |
+
PID=$(cat "$PID_FILE")
|
| 71 |
+
if ps -p "$PID" > /dev/null 2>&1; then
|
| 72 |
+
echo " Stopping server (PID: $PID)..."
|
| 73 |
+
kill $PID 2>/dev/null || true
|
| 74 |
+
|
| 75 |
+
# Wait for process to stop
|
| 76 |
+
for i in {1..5}; do
|
| 77 |
+
if ! ps -p $PID > /dev/null 2>&1; then
|
| 78 |
+
success "Server stopped"
|
| 79 |
+
break
|
| 80 |
+
fi
|
| 81 |
+
sleep 1
|
| 82 |
+
done
|
| 83 |
+
|
| 84 |
+
# Force kill if still running
|
| 85 |
+
if ps -p $PID > /dev/null 2>&1; then
|
| 86 |
+
echo " Force killing server..."
|
| 87 |
+
kill -9 $PID 2>/dev/null || true
|
| 88 |
+
fi
|
| 89 |
+
fi
|
| 90 |
+
rm -f "$PID_FILE"
|
| 91 |
+
fi
|
| 92 |
+
|
| 93 |
+
# Kill any webrtc-server processes
|
| 94 |
+
echo " Killing webrtc-server processes..."
|
| 95 |
+
pkill -f "webrtc-server" 2>/dev/null || true
|
| 96 |
+
|
| 97 |
+
# Kill any process using the port
|
| 98 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 99 |
+
echo " Killing process using port $PORT..."
|
| 100 |
+
lsof -ti :$PORT | xargs kill -9 2>/dev/null || true
|
| 101 |
+
fi
|
| 102 |
+
|
| 103 |
+
# Wait a moment
|
| 104 |
+
sleep 2
|
| 105 |
+
|
| 106 |
+
success "Server stopped"
|
| 107 |
+
|
| 108 |
+
# ============================================================
|
| 109 |
+
# VERIFY STOPPED
|
| 110 |
+
# ============================================================
|
| 111 |
+
header "Verifying..."
|
| 112 |
+
|
| 113 |
+
STILL_RUNNING=false
|
| 114 |
+
|
| 115 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 116 |
+
error "Something is still listening on port $PORT"
|
| 117 |
+
STILL_RUNNING=true
|
| 118 |
+
elif pgrep -f "webrtc-server" > /dev/null 2>&1; then
|
| 119 |
+
error "Some webrtc-server processes still running"
|
| 120 |
+
STILL_RUNNING=true
|
| 121 |
+
fi
|
| 122 |
+
|
| 123 |
+
if [ "$STILL_RUNNING" = false ]; then
|
| 124 |
+
success "All processes stopped"
|
| 125 |
+
fi
|
| 126 |
+
|
| 127 |
+
# ============================================================
|
| 128 |
+
# DISPLAY LOG
|
| 129 |
+
# ============================================================
|
| 130 |
+
header "Last log entries"
|
| 131 |
+
|
| 132 |
+
LOG_FILE="/tmp/webrtc-server.log"
|
| 133 |
+
if [ -f "$LOG_FILE" ]; then
|
| 134 |
+
tail -10 "$LOG_FILE" | sed 's/^/ /'
|
| 135 |
+
else
|
| 136 |
+
echo " No log file found"
|
| 137 |
+
fi
|
| 138 |
+
|
| 139 |
+
echo ""
|
| 140 |
+
success "Stop completed!"
|
| 141 |
+
echo ""
|
server/fast_engine.py
CHANGED
|
@@ -56,14 +56,20 @@ class MuseTalkEngine:
|
|
| 56 |
self.idle_fps = fps
|
| 57 |
self.input_latent_list = []
|
| 58 |
self.coord_list = [] # Face bounding boxes
|
|
|
|
|
|
|
| 59 |
self.original_width = None
|
| 60 |
self.original_height = None
|
| 61 |
|
| 62 |
-
#
|
| 63 |
-
self.
|
| 64 |
-
self.
|
|
|
|
|
|
|
| 65 |
self.left_cheek_width = 90
|
| 66 |
self.right_cheek_width = 90
|
|
|
|
|
|
|
| 67 |
|
| 68 |
logger.info("[MuseTalk] Initializing engine...")
|
| 69 |
self._load_models()
|
|
@@ -117,8 +123,18 @@ class MuseTalkEngine:
|
|
| 117 |
traceback.print_exc()
|
| 118 |
self._models_loaded = False
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
def _load_avatar(self):
|
| 121 |
-
"""Load avatar video frames and detect faces."""
|
|
|
|
|
|
|
|
|
|
| 122 |
try:
|
| 123 |
if not os.path.exists(self.avatar_video):
|
| 124 |
logger.error(f"[MuseTalk] Avatar video not found: {self.avatar_video}")
|
|
@@ -126,6 +142,35 @@ class MuseTalkEngine:
|
|
| 126 |
|
| 127 |
logger.info(f"[MuseTalk] Loading avatar from: {self.avatar_video}")
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
cap = cv2.VideoCapture(self.avatar_video)
|
| 130 |
self.idle_fps = cap.get(cv2.CAP_PROP_FPS) or self.fps
|
| 131 |
|
|
@@ -153,6 +198,26 @@ class MuseTalkEngine:
|
|
| 153 |
if self._models_loaded and self.vae and self.idle_frames:
|
| 154 |
self._precompute_latents()
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
self._avatar_loaded = True
|
| 157 |
|
| 158 |
except Exception as e:
|
|
@@ -161,28 +226,28 @@ class MuseTalkEngine:
|
|
| 161 |
traceback.print_exc()
|
| 162 |
|
| 163 |
def _detect_faces(self):
|
| 164 |
-
"""Detect faces
|
| 165 |
try:
|
|
|
|
| 166 |
import numpy as np
|
| 167 |
from musetalk.utils.face_detection import FaceAlignment, LandmarksType
|
| 168 |
|
| 169 |
-
logger.info("[MuseTalk] Detecting faces...")
|
|
|
|
| 170 |
device_str = 'cuda' if self.device is not None and self.device.type == 'cuda' else 'cpu'
|
| 171 |
fa = FaceAlignment(LandmarksType._2D, flip_input=False, device=device_str)
|
| 172 |
|
| 173 |
self.idle_frames = []
|
| 174 |
self.coord_list = []
|
|
|
|
|
|
|
| 175 |
|
| 176 |
-
# Process in batches
|
| 177 |
batch_size = 8
|
| 178 |
for batch_start in range(0, len(self.full_frames), batch_size):
|
| 179 |
batch_end = min(batch_start + batch_size, len(self.full_frames))
|
| 180 |
batch_frames = self.full_frames[batch_start:batch_end]
|
| 181 |
-
|
| 182 |
-
# Convert to numpy array for batch processing
|
| 183 |
batch_array = np.stack(batch_frames, axis=0)
|
| 184 |
-
|
| 185 |
-
# Get face detections for batch
|
| 186 |
detections = fa.get_detections_for_batch(batch_array)
|
| 187 |
|
| 188 |
for i, (frame, detection) in enumerate(zip(batch_frames, detections)):
|
|
@@ -190,50 +255,64 @@ class MuseTalkEngine:
|
|
| 190 |
h, w = frame.shape[:2]
|
| 191 |
|
| 192 |
if detection is None:
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
else:
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
y1 = max(0, center_y - size // 2)
|
| 221 |
-
x2 = min(w, x1 + size)
|
| 222 |
-
y2 = min(h, y1 + size)
|
| 223 |
-
|
| 224 |
-
# Store coordinates
|
| 225 |
-
self.coord_list.append([x1, y1, x2, y2])
|
| 226 |
-
|
| 227 |
-
# Crop and resize for processing
|
| 228 |
-
face_crop = frame[y1:y2, x1:x2]
|
| 229 |
if face_crop.size > 0:
|
| 230 |
-
face_resized = cv2.resize(face_crop, (
|
| 231 |
else:
|
| 232 |
-
face_resized = cv2.resize(frame, (
|
| 233 |
self.idle_frames.append(face_resized)
|
| 234 |
|
| 235 |
logger.info(f"[MuseTalk] Face detection complete, {len(self.coord_list)} faces processed")
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
except Exception as e:
|
| 238 |
logger.error(f"[MuseTalk] Error in face detection: {e}")
|
| 239 |
import traceback
|
|
@@ -242,9 +321,10 @@ class MuseTalkEngine:
|
|
| 242 |
self.idle_frames = []
|
| 243 |
self.coord_list = []
|
| 244 |
for frame in self.full_frames:
|
| 245 |
-
resized = cv2.resize(frame, (
|
| 246 |
self.idle_frames.append(resized)
|
| 247 |
-
|
|
|
|
| 248 |
|
| 249 |
def _precompute_latents(self):
|
| 250 |
"""Precompute latents for avatar frames."""
|
|
@@ -294,7 +374,7 @@ class MuseTalkEngine:
|
|
| 294 |
|
| 295 |
try:
|
| 296 |
import torch
|
| 297 |
-
from musetalk.utils.blending import
|
| 298 |
|
| 299 |
logger.info(f"[MuseTalk] Processing audio: {audio_path}")
|
| 300 |
|
|
@@ -311,16 +391,24 @@ class MuseTalkEngine:
|
|
| 311 |
# Send info message first (required by frontend to activate canvas)
|
| 312 |
yield {"type": "info", "total_frames": total_frames, "fps": self.fps, "width": output_width, "height": output_height}
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
# Generate frames
|
| 315 |
for i, whisper_batch in enumerate(whisper_chunks):
|
| 316 |
try:
|
| 317 |
-
# Get corresponding
|
| 318 |
-
|
| 319 |
-
|
| 320 |
|
| 321 |
-
latent =
|
| 322 |
-
bbox =
|
| 323 |
-
original_frame = copy.deepcopy(
|
| 324 |
|
| 325 |
# Prepare audio features
|
| 326 |
audio_feat = torch.from_numpy(whisper_batch).unsqueeze(0).half().to(self.device)
|
|
@@ -335,31 +423,35 @@ class MuseTalkEngine:
|
|
| 335 |
encoder_hidden_states=audio_feat
|
| 336 |
).sample
|
| 337 |
|
| 338 |
-
# Decode to image (RGB
|
| 339 |
-
|
|
|
|
| 340 |
|
| 341 |
-
#
|
| 342 |
-
if pred_face.dtype != np.uint8:
|
| 343 |
-
pred_face = (pred_face * 255).astype(np.uint8)
|
| 344 |
-
pred_face_bgr = cv2.cvtColor(pred_face, cv2.COLOR_RGB2BGR)
|
| 345 |
-
|
| 346 |
-
# Get bounding box with extra margin (Robin's config)
|
| 347 |
x1, y1, x2, y2 = bbox
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
yield {"type": "frame", "frame": combined_frame, "index": i, "total": total_frames}
|
| 365 |
|
|
|
|
| 56 |
self.idle_fps = fps
|
| 57 |
self.input_latent_list = []
|
| 58 |
self.coord_list = [] # Face bounding boxes
|
| 59 |
+
self.mask_list = [] # Pre-computed masks for blending
|
| 60 |
+
self.mask_coords_list = [] # Pre-computed crop boxes for blending
|
| 61 |
self.original_width = None
|
| 62 |
self.original_height = None
|
| 63 |
|
| 64 |
+
# MuseTalk V1.5 config (matching realtime_inference.py)
|
| 65 |
+
self.version = "v15"
|
| 66 |
+
self.bbox_shift = 0 # V1.5 uses 0
|
| 67 |
+
self.extra_margin = 10 # Extra margin for face cropping
|
| 68 |
+
self.parsing_mode = "jaw" # Face blending mode
|
| 69 |
self.left_cheek_width = 90
|
| 70 |
self.right_cheek_width = 90
|
| 71 |
+
self.upper_boundary_ratio = 0.5
|
| 72 |
+
self.expand = 1.5
|
| 73 |
|
| 74 |
logger.info("[MuseTalk] Initializing engine...")
|
| 75 |
self._load_models()
|
|
|
|
| 123 |
traceback.print_exc()
|
| 124 |
self._models_loaded = False
|
| 125 |
|
| 126 |
+
def _get_cache_path(self):
|
| 127 |
+
"""Get cache directory path based on avatar video."""
|
| 128 |
+
import hashlib
|
| 129 |
+
video_hash = hashlib.md5(self.avatar_video.encode()).hexdigest()[:8]
|
| 130 |
+
cache_dir = MUSETALK_ROOT / "results" / "v15" / "avatars" / f"cache_{video_hash}"
|
| 131 |
+
return cache_dir
|
| 132 |
+
|
| 133 |
def _load_avatar(self):
|
| 134 |
+
"""Load avatar video frames and detect faces (with caching)."""
|
| 135 |
+
import pickle
|
| 136 |
+
import torch
|
| 137 |
+
|
| 138 |
try:
|
| 139 |
if not os.path.exists(self.avatar_video):
|
| 140 |
logger.error(f"[MuseTalk] Avatar video not found: {self.avatar_video}")
|
|
|
|
| 142 |
|
| 143 |
logger.info(f"[MuseTalk] Loading avatar from: {self.avatar_video}")
|
| 144 |
|
| 145 |
+
# Check cache
|
| 146 |
+
cache_dir = self._get_cache_path()
|
| 147 |
+
cache_file = cache_dir / "avatar_cache.pkl"
|
| 148 |
+
latents_file = cache_dir / "latents.pt"
|
| 149 |
+
|
| 150 |
+
if cache_file.exists() and latents_file.exists():
|
| 151 |
+
logger.info(f"[MuseTalk] Loading from cache: {cache_dir}")
|
| 152 |
+
start = time.time()
|
| 153 |
+
|
| 154 |
+
with open(cache_file, 'rb') as f:
|
| 155 |
+
cache = pickle.load(f)
|
| 156 |
+
|
| 157 |
+
self.full_frames = cache['full_frames']
|
| 158 |
+
self.idle_frames = cache['idle_frames']
|
| 159 |
+
self.coord_list = cache['coord_list']
|
| 160 |
+
self.mask_list = cache['mask_list']
|
| 161 |
+
self.mask_coords_list = cache['mask_coords_list']
|
| 162 |
+
self.original_width = cache['original_width']
|
| 163 |
+
self.original_height = cache['original_height']
|
| 164 |
+
self.idle_fps = cache['idle_fps']
|
| 165 |
+
self.input_latent_list = torch.load(latents_file, weights_only=False)
|
| 166 |
+
|
| 167 |
+
logger.info(f"[MuseTalk] Loaded {len(self.full_frames)} frames from cache in {time.time()-start:.1f}s")
|
| 168 |
+
self._avatar_loaded = True
|
| 169 |
+
return
|
| 170 |
+
|
| 171 |
+
# No cache - load from video
|
| 172 |
+
logger.info("[MuseTalk] No cache found, processing avatar (this will be cached)...")
|
| 173 |
+
|
| 174 |
cap = cv2.VideoCapture(self.avatar_video)
|
| 175 |
self.idle_fps = cap.get(cv2.CAP_PROP_FPS) or self.fps
|
| 176 |
|
|
|
|
| 198 |
if self._models_loaded and self.vae and self.idle_frames:
|
| 199 |
self._precompute_latents()
|
| 200 |
|
| 201 |
+
# Save cache
|
| 202 |
+
try:
|
| 203 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 204 |
+
cache = {
|
| 205 |
+
'full_frames': self.full_frames,
|
| 206 |
+
'idle_frames': self.idle_frames,
|
| 207 |
+
'coord_list': self.coord_list,
|
| 208 |
+
'mask_list': self.mask_list,
|
| 209 |
+
'mask_coords_list': self.mask_coords_list,
|
| 210 |
+
'original_width': self.original_width,
|
| 211 |
+
'original_height': self.original_height,
|
| 212 |
+
'idle_fps': self.idle_fps,
|
| 213 |
+
}
|
| 214 |
+
with open(cache_file, 'wb') as f:
|
| 215 |
+
pickle.dump(cache, f)
|
| 216 |
+
torch.save(self.input_latent_list, latents_file)
|
| 217 |
+
logger.info(f"[MuseTalk] Saved cache to: {cache_dir}")
|
| 218 |
+
except Exception as e:
|
| 219 |
+
logger.warning(f"[MuseTalk] Could not save cache: {e}")
|
| 220 |
+
|
| 221 |
self._avatar_loaded = True
|
| 222 |
|
| 223 |
except Exception as e:
|
|
|
|
| 226 |
traceback.print_exc()
|
| 227 |
|
| 228 |
def _detect_faces(self):
|
| 229 |
+
"""Detect faces using MuseTalk's preprocessing (matching realtime_inference.py)."""
|
| 230 |
try:
|
| 231 |
+
from musetalk.utils.blending import get_image_prepare_material
|
| 232 |
import numpy as np
|
| 233 |
from musetalk.utils.face_detection import FaceAlignment, LandmarksType
|
| 234 |
|
| 235 |
+
logger.info("[MuseTalk] Detecting faces using MuseTalk preprocessing...")
|
| 236 |
+
|
| 237 |
device_str = 'cuda' if self.device is not None and self.device.type == 'cuda' else 'cpu'
|
| 238 |
fa = FaceAlignment(LandmarksType._2D, flip_input=False, device=device_str)
|
| 239 |
|
| 240 |
self.idle_frames = []
|
| 241 |
self.coord_list = []
|
| 242 |
+
self.mask_list = []
|
| 243 |
+
self.mask_coords_list = []
|
| 244 |
|
| 245 |
+
# Process in batches
|
| 246 |
batch_size = 8
|
| 247 |
for batch_start in range(0, len(self.full_frames), batch_size):
|
| 248 |
batch_end = min(batch_start + batch_size, len(self.full_frames))
|
| 249 |
batch_frames = self.full_frames[batch_start:batch_end]
|
|
|
|
|
|
|
| 250 |
batch_array = np.stack(batch_frames, axis=0)
|
|
|
|
|
|
|
| 251 |
detections = fa.get_detections_for_batch(batch_array)
|
| 252 |
|
| 253 |
for i, (frame, detection) in enumerate(zip(batch_frames, detections)):
|
|
|
|
| 255 |
h, w = frame.shape[:2]
|
| 256 |
|
| 257 |
if detection is None:
|
| 258 |
+
# Fallback: center crop
|
| 259 |
+
size = min(h, w) // 2
|
| 260 |
+
center_x, center_y = w // 2, h // 2
|
| 261 |
+
x1 = center_x - size // 2
|
| 262 |
+
y1 = center_y - size // 2
|
| 263 |
+
x2 = x1 + size
|
| 264 |
+
y2 = y1 + size
|
| 265 |
else:
|
| 266 |
+
x1, y1, x2, y2 = [int(v) for v in detection]
|
| 267 |
+
|
| 268 |
+
# Apply bbox_shift (0 for V1.5)
|
| 269 |
+
x1 = max(0, x1 + self.bbox_shift)
|
| 270 |
+
y1 = max(0, y1 + self.bbox_shift)
|
| 271 |
+
x2 = min(w, x2 + self.bbox_shift)
|
| 272 |
+
y2 = min(h, y2 + self.bbox_shift)
|
| 273 |
+
|
| 274 |
+
# For V1.5: add extra_margin to y2 (matching realtime_inference.py)
|
| 275 |
+
if self.version == "v15":
|
| 276 |
+
y2_extended = min(y2 + self.extra_margin, h)
|
| 277 |
+
else:
|
| 278 |
+
y2_extended = y2
|
| 279 |
+
|
| 280 |
+
bbox = [x1, y1, x2, y2_extended]
|
| 281 |
+
self.coord_list.append(bbox)
|
| 282 |
+
|
| 283 |
+
# Crop and resize face for latent computation (256x256)
|
| 284 |
+
face_crop = frame[y1:y2_extended, x1:x2]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
if face_crop.size > 0:
|
| 286 |
+
face_resized = cv2.resize(face_crop, (256, 256), interpolation=cv2.INTER_LANCZOS4)
|
| 287 |
else:
|
| 288 |
+
face_resized = cv2.resize(frame, (256, 256), interpolation=cv2.INTER_LANCZOS4)
|
| 289 |
self.idle_frames.append(face_resized)
|
| 290 |
|
| 291 |
logger.info(f"[MuseTalk] Face detection complete, {len(self.coord_list)} faces processed")
|
| 292 |
|
| 293 |
+
# Pre-compute masks and crop_boxes for blending (matching realtime_inference.py)
|
| 294 |
+
if self.face_parser is not None:
|
| 295 |
+
logger.info("[MuseTalk] Pre-computing blending masks...")
|
| 296 |
+
for i, (frame, bbox) in enumerate(zip(self.full_frames, self.coord_list)):
|
| 297 |
+
try:
|
| 298 |
+
mask, crop_box = get_image_prepare_material(
|
| 299 |
+
frame,
|
| 300 |
+
bbox,
|
| 301 |
+
upper_boundary_ratio=self.upper_boundary_ratio,
|
| 302 |
+
expand=self.expand,
|
| 303 |
+
fp=self.face_parser,
|
| 304 |
+
mode=self.parsing_mode
|
| 305 |
+
)
|
| 306 |
+
self.mask_list.append(mask)
|
| 307 |
+
self.mask_coords_list.append(crop_box)
|
| 308 |
+
except Exception as e:
|
| 309 |
+
logger.warning(f"[MuseTalk] Error computing mask for frame {i}: {e}")
|
| 310 |
+
# Create empty mask as fallback
|
| 311 |
+
self.mask_list.append(np.zeros((256, 256), dtype=np.uint8))
|
| 312 |
+
self.mask_coords_list.append([0, 0, 256, 256])
|
| 313 |
+
|
| 314 |
+
logger.info(f"[MuseTalk] Pre-computed {len(self.mask_list)} masks")
|
| 315 |
+
|
| 316 |
except Exception as e:
|
| 317 |
logger.error(f"[MuseTalk] Error in face detection: {e}")
|
| 318 |
import traceback
|
|
|
|
| 321 |
self.idle_frames = []
|
| 322 |
self.coord_list = []
|
| 323 |
for frame in self.full_frames:
|
| 324 |
+
resized = cv2.resize(frame, (256, 256))
|
| 325 |
self.idle_frames.append(resized)
|
| 326 |
+
h, w = frame.shape[:2]
|
| 327 |
+
self.coord_list.append([0, 0, w, h])
|
| 328 |
|
| 329 |
def _precompute_latents(self):
|
| 330 |
"""Precompute latents for avatar frames."""
|
|
|
|
| 374 |
|
| 375 |
try:
|
| 376 |
import torch
|
| 377 |
+
from musetalk.utils.blending import get_image_blending
|
| 378 |
|
| 379 |
logger.info(f"[MuseTalk] Processing audio: {audio_path}")
|
| 380 |
|
|
|
|
| 391 |
# Send info message first (required by frontend to activate canvas)
|
| 392 |
yield {"type": "info", "total_frames": total_frames, "fps": self.fps, "width": output_width, "height": output_height}
|
| 393 |
|
| 394 |
+
# Create cycled lists (like realtime_inference.py: forward + reverse)
|
| 395 |
+
num_avatar_frames = len(self.full_frames)
|
| 396 |
+
frame_list_cycle = self.full_frames + self.full_frames[::-1]
|
| 397 |
+
coord_list_cycle = self.coord_list + self.coord_list[::-1]
|
| 398 |
+
latent_list_cycle = self.input_latent_list + self.input_latent_list[::-1]
|
| 399 |
+
mask_list_cycle = self.mask_list + self.mask_list[::-1] if self.mask_list else []
|
| 400 |
+
mask_coords_list_cycle = self.mask_coords_list + self.mask_coords_list[::-1] if self.mask_coords_list else []
|
| 401 |
+
|
| 402 |
# Generate frames
|
| 403 |
for i, whisper_batch in enumerate(whisper_chunks):
|
| 404 |
try:
|
| 405 |
+
# Get corresponding data (cycling through avatar frames)
|
| 406 |
+
cycle_idx = i % len(frame_list_cycle)
|
| 407 |
+
latent_idx = i % len(latent_list_cycle)
|
| 408 |
|
| 409 |
+
latent = latent_list_cycle[latent_idx]
|
| 410 |
+
bbox = coord_list_cycle[cycle_idx]
|
| 411 |
+
original_frame = copy.deepcopy(frame_list_cycle[cycle_idx])
|
| 412 |
|
| 413 |
# Prepare audio features
|
| 414 |
audio_feat = torch.from_numpy(whisper_batch).unsqueeze(0).half().to(self.device)
|
|
|
|
| 423 |
encoder_hidden_states=audio_feat
|
| 424 |
).sample
|
| 425 |
|
| 426 |
+
# Decode to image (returns list of RGB frames)
|
| 427 |
+
pred_faces = self.vae.decode_latents(pred)
|
| 428 |
+
pred_face = pred_faces[0] # Get first (and only) face - RGB format
|
| 429 |
|
| 430 |
+
# Resize predicted face to match bbox size (matching realtime_inference.py)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
x1, y1, x2, y2 = bbox
|
| 432 |
+
try:
|
| 433 |
+
pred_face_resized = cv2.resize(pred_face.astype(np.uint8), (x2 - x1, y2 - y1))
|
| 434 |
+
except:
|
| 435 |
+
pred_face_resized = pred_face
|
| 436 |
+
|
| 437 |
+
# Convert RGB to BGR for blending (get_image_blending expects BGR)
|
| 438 |
+
pred_face_resized = cv2.cvtColor(pred_face_resized, cv2.COLOR_RGB2BGR)
|
| 439 |
+
|
| 440 |
+
# Blend using pre-computed masks (matching realtime_inference.py)
|
| 441 |
+
if mask_list_cycle and mask_coords_list_cycle:
|
| 442 |
+
mask = mask_list_cycle[cycle_idx]
|
| 443 |
+
mask_crop_box = mask_coords_list_cycle[cycle_idx]
|
| 444 |
+
combined_frame = get_image_blending(
|
| 445 |
+
original_frame,
|
| 446 |
+
pred_face_resized,
|
| 447 |
+
bbox,
|
| 448 |
+
mask,
|
| 449 |
+
mask_crop_box
|
| 450 |
+
)
|
| 451 |
+
else:
|
| 452 |
+
# Fallback: simple paste without blending
|
| 453 |
+
combined_frame = original_frame.copy()
|
| 454 |
+
combined_frame[y1:y2, x1:x2] = pred_face_resized
|
| 455 |
|
| 456 |
yield {"type": "frame", "frame": combined_frame, "index": i, "total": total_frames}
|
| 457 |
|
webrtc-latency-test/.gitignore
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
MANIFEST
|
| 23 |
+
|
| 24 |
+
# Virtual Environment
|
| 25 |
+
venv/
|
| 26 |
+
env/
|
| 27 |
+
ENV/
|
| 28 |
+
env.bak/
|
| 29 |
+
venv.bak/
|
| 30 |
+
|
| 31 |
+
# IDE
|
| 32 |
+
.vscode/
|
| 33 |
+
.idea/
|
| 34 |
+
*.swp
|
| 35 |
+
*.swo
|
| 36 |
+
*~
|
| 37 |
+
.DS_Store
|
| 38 |
+
|
| 39 |
+
# Logs
|
| 40 |
+
*.log
|
| 41 |
+
logs/
|
| 42 |
+
/tmp/
|
| 43 |
+
|
| 44 |
+
# Test results
|
| 45 |
+
latency_test_*.json
|
| 46 |
+
latency_results.json
|
| 47 |
+
screenshots/
|
| 48 |
+
|
| 49 |
+
# Runtime
|
| 50 |
+
*.pid
|
| 51 |
+
nohup.out
|
webrtc-latency-test/README.md
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# � Speech-to-Speech Avatar Microservices Architecture
|
| 2 |
+
|
| 3 |
+
Arquitetura de microserviços para testar e medir latência em pipeline completo de conversação com avatar (Speech-to-Text → LLM → Text-to-Speech → Avatar Animation).
|
| 4 |
+
|
| 5 |
+
## 📋 Visão Geral
|
| 6 |
+
|
| 7 |
+
Este projeto implementa uma arquitetura de microserviços independentes para simular e testar um sistema completo de conversação com avatar digital. Cada serviço pode ser desenvolvido, testado e escalado independentemente.
|
| 8 |
+
|
| 9 |
+
**Pipeline de Conversação:**
|
| 10 |
+
```
|
| 11 |
+
Audio → Whisper (STT) → LLM → TTS → MuseTalk → Video
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
## 🏗️ Arquitetura
|
| 15 |
+
|
| 16 |
+
```
|
| 17 |
+
┌─────────────────────────────────────────────────────────┐
|
| 18 |
+
│ Gateway (Port 8080) │
|
| 19 |
+
│ Orchestrates all services & metrics │
|
| 20 |
+
└───────────┬─────────────────────────────────────────────┘
|
| 21 |
+
│
|
| 22 |
+
┌───────┴───────┬────────────┬────────────┐
|
| 23 |
+
│ │ │ │
|
| 24 |
+
┌───▼────┐ ┌────▼─────┐ ┌──▼───┐ ┌────▼──────┐
|
| 25 |
+
│Whisper │ │ LLM │ │ TTS │ │ MuseTalk │
|
| 26 |
+
│ (STT) │ │ │ │ │ │ (Avatar) │
|
| 27 |
+
│Port │ │Port 5002 │ │Port │ │Port 5004 │
|
| 28 |
+
│5001 │ │ │ │5003 │ │ │
|
| 29 |
+
└────────┘ └──────────┘ └──────┘ └───────────┘
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
### 📦 Microserviços
|
| 33 |
+
|
| 34 |
+
| Serviço | Porta | Função | Latência Mock |
|
| 35 |
+
|---------|-------|--------|---------------|
|
| 36 |
+
| **Gateway** | 8080 | Orquestra pipeline, métricas | - |
|
| 37 |
+
| **Whisper** | 5001 | Speech-to-Text | ~75ms |
|
| 38 |
+
| **LLM** | 5002 | Geração de respostas | ~250ms |
|
| 39 |
+
| **TTS** | 5003 | Text-to-Speech | ~125ms |
|
| 40 |
+
| **MuseTalk** | 5004 | Avatar Animation | ~125ms |
|
| 41 |
+
|
| 42 |
+
**Latência Total Esperada:** ~575ms
|
| 43 |
+
|
| 44 |
+
## 🚀 Instalação e Execução
|
| 45 |
+
|
| 46 |
+
### Opção 1: Docker Compose (Recomendado)
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
# 1. Clonar repositório
|
| 50 |
+
git clone <repo-url>
|
| 51 |
+
cd webrtc-latency-test
|
| 52 |
+
|
| 53 |
+
# 2. Iniciar todos os serviços
|
| 54 |
+
docker-compose up -d
|
| 55 |
+
|
| 56 |
+
# 3. Verificar status
|
| 57 |
+
docker-compose ps
|
| 58 |
+
|
| 59 |
+
# 4. Ver logs
|
| 60 |
+
docker-compose logs -f
|
| 61 |
+
|
| 62 |
+
# 5. Parar serviços
|
| 63 |
+
docker-compose down
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Opção 2: Execução Local
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
# 1. Instalar dependências (uma vez)
|
| 70 |
+
cd webrtc-latency-test
|
| 71 |
+
|
| 72 |
+
# 2. Iniciar todos os serviços
|
| 73 |
+
./start-all.sh
|
| 74 |
+
|
| 75 |
+
# 3. Verificar serviços
|
| 76 |
+
curl http://localhost:8080/health
|
| 77 |
+
|
| 78 |
+
# 4. Parar serviços
|
| 79 |
+
./stop-all.sh
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## 🧪 Testando o Sistema
|
| 83 |
+
|
| 84 |
+
### Teste de Conversação Completa
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
# Enviar audio para o pipeline completo
|
| 88 |
+
curl -X POST http://localhost:8080/conversation \
|
| 89 |
+
-H "Content-Type: application/json" \
|
| 90 |
+
-d '{
|
| 91 |
+
"audio_data": "base64_encoded_audio_here",
|
| 92 |
+
"user_id": "test_user_123"
|
| 93 |
+
}'
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
**Resposta esperada:**
|
| 97 |
+
```json
|
| 98 |
+
{
|
| 99 |
+
"transcript": "Olá, como você está?",
|
| 100 |
+
"llm_response": "Estou muito bem, obrigado por perguntar!",
|
| 101 |
+
"audio_data": "base64_audio_data",
|
| 102 |
+
"video_data": "base64_video_data",
|
| 103 |
+
"latency_ms": {
|
| 104 |
+
"whisper": 75,
|
| 105 |
+
"llm": 250,
|
| 106 |
+
"tts": 125,
|
| 107 |
+
"musetalk": 125,
|
| 108 |
+
"total": 575
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### Teste de Latência Automatizado
|
| 114 |
+
|
| 115 |
+
```bash
|
| 116 |
+
# Executar 10 requisições e calcular estatísticas
|
| 117 |
+
python3 test_latency.py
|
| 118 |
+
|
| 119 |
+
# Resultados salvos em latency_results.json
|
| 120 |
+
cat latency_results.json
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
**Exemplo de resultado:**
|
| 124 |
+
```json
|
| 125 |
+
{
|
| 126 |
+
"total_requests": 10,
|
| 127 |
+
"successful_requests": 10,
|
| 128 |
+
"failed_requests": 0,
|
| 129 |
+
"avg_latency_ms": 575.2,
|
| 130 |
+
"min_latency_ms": 571.8,
|
| 131 |
+
"max_latency_ms": 580.1,
|
| 132 |
+
"breakdown": {
|
| 133 |
+
"whisper_avg": 75.1,
|
| 134 |
+
"llm_avg": 250.3,
|
| 135 |
+
"tts_avg": 125.0,
|
| 136 |
+
"musetalk_avg": 124.8
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
### Verificar Métricas
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
# Métricas de todos os serviços
|
| 145 |
+
curl http://localhost:8080/metrics
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### Testar Serviços Individualmente
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
# Whisper (STT)
|
| 152 |
+
curl -X POST http://localhost:5001/transcribe \
|
| 153 |
+
-H "Content-Type: application/json" \
|
| 154 |
+
-d '{"audio_data": "base64_audio"}'
|
| 155 |
+
|
| 156 |
+
# LLM
|
| 157 |
+
curl -X POST http://localhost:5002/generate \
|
| 158 |
+
-H "Content-Type: application/json" \
|
| 159 |
+
-d '{"text": "Olá"}'
|
| 160 |
+
|
| 161 |
+
# TTS
|
| 162 |
+
curl -X POST http://localhost:5003/synthesize \
|
| 163 |
+
-H "Content-Type: application/json" \
|
| 164 |
+
-d '{"text": "Olá, tudo bem?"}'
|
| 165 |
+
|
| 166 |
+
# MuseTalk
|
| 167 |
+
curl -X POST http://localhost:5004/generate \
|
| 168 |
+
-H "Content-Type: application/json" \
|
| 169 |
+
-d '{"audio_data": "base64_audio"}'
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
## 📁 Estrutura do Projeto
|
| 173 |
+
|
| 174 |
+
```
|
| 175 |
+
webrtc-latency-test/
|
| 176 |
+
├── README.md # Este arquivo
|
| 177 |
+
├── docker-compose.yml # Orquestração Docker
|
| 178 |
+
├── start-all.sh # Script para iniciar localmente
|
| 179 |
+
├── stop-all.sh # Script para parar serviços
|
| 180 |
+
├── test_latency.py # Teste automatizado de latência
|
| 181 |
+
│
|
| 182 |
+
├── gateway/ # Gateway Orchestrator
|
| 183 |
+
│ ├── main.py # API FastAPI
|
| 184 |
+
│ ├── requirements.txt # Dependências
|
| 185 |
+
│ └── Dockerfile # Container
|
| 186 |
+
│
|
| 187 |
+
├── services/ # Microserviços
|
| 188 |
+
│ ├── whisper/ # Speech-to-Text
|
| 189 |
+
│ │ ├── server.py # Mock Whisper
|
| 190 |
+
│ │ ├── requirements.txt
|
| 191 |
+
│ │ └── Dockerfile
|
| 192 |
+
│ │
|
| 193 |
+
│ ├── llm/ # Large Language Model
|
| 194 |
+
│ │ ├── server.py # Mock LLM
|
| 195 |
+
│ │ ├── requirements.txt
|
| 196 |
+
│ │ └── Dockerfile
|
| 197 |
+
│ │
|
| 198 |
+
│ ├── tts/ # Text-to-Speech
|
| 199 |
+
│ │ ├── server.py # Mock TTS
|
| 200 |
+
│ │ ├── requirements.txt
|
| 201 |
+
│ │ └── Dockerfile
|
| 202 |
+
│ │
|
| 203 |
+
│ └── musetalk/ # Avatar Animation
|
| 204 |
+
│ ├── server.py # Mock MuseTalk
|
| 205 |
+
│ ├── requirements.txt
|
| 206 |
+
│ └── Dockerfile
|
| 207 |
+
│
|
| 208 |
+
├── shared/ # Código compartilhado
|
| 209 |
+
│ └── proto/ # Protocol Buffers (futuro)
|
| 210 |
+
│ ├── whisper.proto
|
| 211 |
+
│ ├── llm.proto
|
| 212 |
+
│ ├── tts.proto
|
| 213 |
+
│ └── musetalk.proto
|
| 214 |
+
│
|
| 215 |
+
└── docs/
|
| 216 |
+
└── ARCHITECTURE.md # Documentação detalhada
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
## 🔄 Substituindo Mocks por Implementações Reais
|
| 220 |
+
|
| 221 |
+
Cada serviço mock pode ser substituído independentemente:
|
| 222 |
+
|
| 223 |
+
### 1. Whisper (Speech-to-Text)
|
| 224 |
+
|
| 225 |
+
```python
|
| 226 |
+
# services/whisper/server.py
|
| 227 |
+
import whisper
|
| 228 |
+
|
| 229 |
+
model = whisper.load_model("base")
|
| 230 |
+
|
| 231 |
+
@app.post("/transcribe")
|
| 232 |
+
async def transcribe(request: TranscribeRequest):
|
| 233 |
+
# Decodificar audio
|
| 234 |
+
audio = decode_audio(request.audio_data)
|
| 235 |
+
|
| 236 |
+
# Transcrever com Whisper real
|
| 237 |
+
result = model.transcribe(audio)
|
| 238 |
+
|
| 239 |
+
return {"transcript": result["text"]}
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
### 2. LLM (Large Language Model)
|
| 243 |
+
|
| 244 |
+
```python
|
| 245 |
+
# services/llm/server.py
|
| 246 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 247 |
+
|
| 248 |
+
model = AutoModelForCausalLM.from_pretrained("gemma-2b")
|
| 249 |
+
tokenizer = AutoTokenizer.from_pretrained("gemma-2b")
|
| 250 |
+
|
| 251 |
+
@app.post("/generate")
|
| 252 |
+
async def generate(request: GenerateRequest):
|
| 253 |
+
inputs = tokenizer(request.text, return_tensors="pt")
|
| 254 |
+
outputs = model.generate(**inputs)
|
| 255 |
+
response = tokenizer.decode(outputs[0])
|
| 256 |
+
|
| 257 |
+
return {"response": response}
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
### 3. TTS (Text-to-Speech)
|
| 261 |
+
|
| 262 |
+
```python
|
| 263 |
+
# services/tts/server.py
|
| 264 |
+
from fish_audio import FishAudioTTS
|
| 265 |
+
|
| 266 |
+
tts = FishAudioTTS()
|
| 267 |
+
|
| 268 |
+
@app.post("/synthesize")
|
| 269 |
+
async def synthesize(request: SynthesizeRequest):
|
| 270 |
+
audio = tts.generate(request.text)
|
| 271 |
+
audio_b64 = encode_audio(audio)
|
| 272 |
+
|
| 273 |
+
return {"audio_data": audio_b64}
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
### 4. MuseTalk (Avatar Animation)
|
| 277 |
+
|
| 278 |
+
```python
|
| 279 |
+
# services/musetalk/server.py
|
| 280 |
+
from musetalk import MuseTalkPipeline
|
| 281 |
+
|
| 282 |
+
pipeline = MuseTalkPipeline()
|
| 283 |
+
|
| 284 |
+
@app.post("/generate")
|
| 285 |
+
async def generate(request: GenerateRequest):
|
| 286 |
+
audio = decode_audio(request.audio_data)
|
| 287 |
+
video = pipeline.generate(audio)
|
| 288 |
+
video_b64 = encode_video(video)
|
| 289 |
+
|
| 290 |
+
return {"video_data": video_b64}
|
| 291 |
+
```
|
| 292 |
+
|
| 293 |
+
## � Benchmarks de Latência
|
| 294 |
+
|
| 295 |
+
### Latências Esperadas (Implementação Real)
|
| 296 |
+
|
| 297 |
+
| Serviço | Mock | Real (GPU) | Real (CPU) |
|
| 298 |
+
|---------|------|------------|------------|
|
| 299 |
+
| Whisper | 75ms | 50-150ms | 200-500ms |
|
| 300 |
+
| LLM | 250ms | 100-300ms | 500-2000ms |
|
| 301 |
+
| TTS | 125ms | 100-200ms | 300-800ms |
|
| 302 |
+
| MuseTalk | 125ms | 80-150ms | 500-1500ms |
|
| 303 |
+
| **Total** | **~575ms** | **330-800ms** | **1500-4800ms** |
|
| 304 |
+
|
| 305 |
+
### Otimizações para Reduzir Latência
|
| 306 |
+
|
| 307 |
+
1. **Streaming Pipeline**: Processar em chunks ao invés de esperar resposta completa
|
| 308 |
+
2. **GPU Acceleration**: Usar CUDA para Whisper, LLM, TTS e MuseTalk
|
| 309 |
+
3. **Model Optimization**: Quantização, pruning, distillation
|
| 310 |
+
4. **Caching**: Cache de respostas frequentes (LLM, TTS)
|
| 311 |
+
5. **Batch Processing**: Processar múltiplas requisições juntas
|
| 312 |
+
6. **Edge Deployment**: Deploy próximo ao usuário
|
| 313 |
+
|
| 314 |
+
## �️ Desenvolvimento
|
| 315 |
+
|
| 316 |
+
### Adicionar Novo Serviço
|
| 317 |
+
|
| 318 |
+
```bash
|
| 319 |
+
# 1. Criar diretório
|
| 320 |
+
mkdir -p services/novo-servico
|
| 321 |
+
|
| 322 |
+
# 2. Criar server.py
|
| 323 |
+
cat > services/novo-servico/server.py << 'EOF'
|
| 324 |
+
from fastapi import FastAPI
|
| 325 |
+
from pydantic import BaseModel
|
| 326 |
+
import uvicorn
|
| 327 |
+
|
| 328 |
+
app = FastAPI()
|
| 329 |
+
|
| 330 |
+
class Request(BaseModel):
|
| 331 |
+
data: str
|
| 332 |
+
|
| 333 |
+
@app.post("/process")
|
| 334 |
+
async def process(request: Request):
|
| 335 |
+
# Sua lógica aqui
|
| 336 |
+
return {"result": "processed"}
|
| 337 |
+
|
| 338 |
+
if __name__ == "__main__":
|
| 339 |
+
uvicorn.run(app, host="0.0.0.0", port=5005)
|
| 340 |
+
EOF
|
| 341 |
+
|
| 342 |
+
# 3. Criar requirements.txt
|
| 343 |
+
echo "fastapi==0.104.1
|
| 344 |
+
uvicorn==0.24.0
|
| 345 |
+
pydantic==2.5.0" > services/novo-servico/requirements.txt
|
| 346 |
+
|
| 347 |
+
# 4. Criar Dockerfile
|
| 348 |
+
cat > services/novo-servico/Dockerfile << 'EOF'
|
| 349 |
+
FROM python:3.11-slim
|
| 350 |
+
WORKDIR /app
|
| 351 |
+
COPY requirements.txt .
|
| 352 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 353 |
+
COPY server.py .
|
| 354 |
+
CMD ["python", "server.py"]
|
| 355 |
+
EOF
|
| 356 |
+
|
| 357 |
+
# 5. Adicionar ao docker-compose.yml
|
| 358 |
+
```
|
| 359 |
+
|
| 360 |
+
### Logs e Debug
|
| 361 |
+
|
| 362 |
+
```bash
|
| 363 |
+
# Logs de serviço específico
|
| 364 |
+
docker-compose logs -f whisper
|
| 365 |
+
docker-compose logs -f gateway
|
| 366 |
+
|
| 367 |
+
# Logs locais
|
| 368 |
+
tail -f /tmp/whisper.log
|
| 369 |
+
tail -f /tmp/gateway.log
|
| 370 |
+
|
| 371 |
+
# Acessar container
|
| 372 |
+
docker-compose exec whisper /bin/bash
|
| 373 |
+
docker-compose exec gateway /bin/bash
|
| 374 |
+
```
|
| 375 |
+
|
| 376 |
+
## � Segurança e Produção
|
| 377 |
+
|
| 378 |
+
Para deploy em produção, adicionar:
|
| 379 |
+
|
| 380 |
+
- [ ] Autenticação (JWT, API Keys)
|
| 381 |
+
- [ ] Rate limiting
|
| 382 |
+
- [ ] HTTPS/TLS
|
| 383 |
+
- [ ] Input validation robusta
|
| 384 |
+
- [ ] Logging estruturado
|
| 385 |
+
- [ ] Monitoring (Prometheus, Grafana)
|
| 386 |
+
- [ ] Health checks avançados
|
| 387 |
+
- [ ] Circuit breakers
|
| 388 |
+
- [ ] Retry policies
|
| 389 |
+
|
| 390 |
+
## 📈 Monitoramento
|
| 391 |
+
|
| 392 |
+
```bash
|
| 393 |
+
# Health check de todos os serviços
|
| 394 |
+
curl http://localhost:8080/health
|
| 395 |
+
|
| 396 |
+
# Métricas detalhadas
|
| 397 |
+
curl http://localhost:8080/metrics | jq
|
| 398 |
+
|
| 399 |
+
# Status individual
|
| 400 |
+
curl http://localhost:5001/health
|
| 401 |
+
curl http://localhost:5002/health
|
| 402 |
+
curl http://localhost:5003/health
|
| 403 |
+
curl http://localhost:5004/health
|
| 404 |
+
```
|
| 405 |
+
|
| 406 |
+
## � Solução de Problemas
|
| 407 |
+
|
| 408 |
+
### Serviço não inicia
|
| 409 |
+
|
| 410 |
+
```bash
|
| 411 |
+
# Verificar portas em uso
|
| 412 |
+
lsof -ti:5001 -ti:5002 -ti:5003 -ti:5004 -ti:8080
|
| 413 |
+
|
| 414 |
+
# Matar processos
|
| 415 |
+
pkill -f "python.*server.py"
|
| 416 |
+
|
| 417 |
+
# Reiniciar
|
| 418 |
+
./stop-all.sh
|
| 419 |
+
./start-all.sh
|
| 420 |
+
```
|
| 421 |
+
|
| 422 |
+
### Docker não conecta serviços
|
| 423 |
+
|
| 424 |
+
```bash
|
| 425 |
+
# Verificar rede Docker
|
| 426 |
+
docker network ls
|
| 427 |
+
docker network inspect webrtc-latency-test_default
|
| 428 |
+
|
| 429 |
+
# Recriar containers
|
| 430 |
+
docker-compose down -v
|
| 431 |
+
docker-compose up -d --build
|
| 432 |
+
```
|
| 433 |
+
|
| 434 |
+
### Latência muito alta
|
| 435 |
+
|
| 436 |
+
1. Verificar logs de cada serviço
|
| 437 |
+
2. Testar serviços individualmente
|
| 438 |
+
3. Verificar recursos (CPU, RAM, GPU)
|
| 439 |
+
4. Verificar rede entre containers
|
| 440 |
+
|
| 441 |
+
## 📚 Documentação Adicional
|
| 442 |
+
|
| 443 |
+
- [Arquitetura Detalhada](docs/ARCHITECTURE.md)
|
| 444 |
+
- [API Reference](docs/API.md) (em breve)
|
| 445 |
+
- [Deployment Guide](docs/DEPLOYMENT.md) (em breve)
|
| 446 |
+
|
| 447 |
+
## 🎯 Próximos Passos
|
| 448 |
+
|
| 449 |
+
- [ ] Implementar streaming pipeline (chunks)
|
| 450 |
+
- [ ] Adicionar WebSocket para real-time
|
| 451 |
+
- [ ] Implementar Whisper real
|
| 452 |
+
- [ ] Integrar LLM (Gemma, LLaMA)
|
| 453 |
+
- [ ] Integrar Fish Audio TTS
|
| 454 |
+
- [ ] Integrar MuseTalk completo
|
| 455 |
+
- [ ] Adicionar frontend web
|
| 456 |
+
- [ ] Deploy em Kubernetes
|
| 457 |
+
- [ ] Adicionar monitoramento (Prometheus/Grafana)
|
| 458 |
+
|
| 459 |
+
## 📝 Licença
|
| 460 |
+
|
| 461 |
+
MIT License
|
| 462 |
+
|
| 463 |
+
## 🤝 Contribuições
|
| 464 |
+
|
| 465 |
+
Contribuições são bem-vindas! Abra uma issue ou pull request.
|
| 466 |
+
|
| 467 |
+
---
|
| 468 |
+
|
| 469 |
+
**Desenvolvido para testar arquitetura de microserviços para conversação com avatar digital** 🎯🤖
|
webrtc-latency-test/docker-compose.yml
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
# Gateway/Orquestrador (porta 8080)
|
| 5 |
+
gateway:
|
| 6 |
+
build:
|
| 7 |
+
context: ./gateway
|
| 8 |
+
dockerfile: Dockerfile
|
| 9 |
+
ports:
|
| 10 |
+
- "8080:8080"
|
| 11 |
+
depends_on:
|
| 12 |
+
- whisper
|
| 13 |
+
- llm
|
| 14 |
+
- tts
|
| 15 |
+
- musetalk
|
| 16 |
+
environment:
|
| 17 |
+
- WHISPER_URL=http://whisper:5001
|
| 18 |
+
- LLM_URL=http://llm:5002
|
| 19 |
+
- TTS_URL=http://tts:5003
|
| 20 |
+
- MUSETALK_URL=http://musetalk:5004
|
| 21 |
+
networks:
|
| 22 |
+
- microservices
|
| 23 |
+
restart: unless-stopped
|
| 24 |
+
|
| 25 |
+
# Whisper Service (STT - porta 5001)
|
| 26 |
+
whisper:
|
| 27 |
+
build:
|
| 28 |
+
context: ./services/whisper
|
| 29 |
+
dockerfile: Dockerfile
|
| 30 |
+
ports:
|
| 31 |
+
- "5001:5001"
|
| 32 |
+
networks:
|
| 33 |
+
- microservices
|
| 34 |
+
restart: unless-stopped
|
| 35 |
+
healthcheck:
|
| 36 |
+
test: ["CMD", "curl", "-f", "http://localhost:5001/health"]
|
| 37 |
+
interval: 10s
|
| 38 |
+
timeout: 5s
|
| 39 |
+
retries: 3
|
| 40 |
+
|
| 41 |
+
# LLM Service (porta 5002)
|
| 42 |
+
llm:
|
| 43 |
+
build:
|
| 44 |
+
context: ./services/llm
|
| 45 |
+
dockerfile: Dockerfile
|
| 46 |
+
ports:
|
| 47 |
+
- "5002:5002"
|
| 48 |
+
networks:
|
| 49 |
+
- microservices
|
| 50 |
+
restart: unless-stopped
|
| 51 |
+
healthcheck:
|
| 52 |
+
test: ["CMD", "curl", "-f", "http://localhost:5002/health"]
|
| 53 |
+
interval: 10s
|
| 54 |
+
timeout: 5s
|
| 55 |
+
retries: 3
|
| 56 |
+
|
| 57 |
+
# TTS Service (porta 5003)
|
| 58 |
+
tts:
|
| 59 |
+
build:
|
| 60 |
+
context: ./services/tts
|
| 61 |
+
dockerfile: Dockerfile
|
| 62 |
+
ports:
|
| 63 |
+
- "5003:5003"
|
| 64 |
+
networks:
|
| 65 |
+
- microservices
|
| 66 |
+
restart: unless-stopped
|
| 67 |
+
healthcheck:
|
| 68 |
+
test: ["CMD", "curl", "-f", "http://localhost:5003/health"]
|
| 69 |
+
interval: 10s
|
| 70 |
+
timeout: 5s
|
| 71 |
+
retries: 3
|
| 72 |
+
|
| 73 |
+
# MuseTalk Service (Avatar - porta 5004)
|
| 74 |
+
musetalk:
|
| 75 |
+
build:
|
| 76 |
+
context: ./services/musetalk
|
| 77 |
+
dockerfile: Dockerfile
|
| 78 |
+
ports:
|
| 79 |
+
- "5004:5004"
|
| 80 |
+
networks:
|
| 81 |
+
- microservices
|
| 82 |
+
restart: unless-stopped
|
| 83 |
+
healthcheck:
|
| 84 |
+
test: ["CMD", "curl", "-f", "http://localhost:5004/health"]
|
| 85 |
+
interval: 10s
|
| 86 |
+
timeout: 5s
|
| 87 |
+
retries: 3
|
| 88 |
+
|
| 89 |
+
networks:
|
| 90 |
+
microservices:
|
| 91 |
+
driver: bridge
|
webrtc-latency-test/docs/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🏗️ Arquitetura de Integração - Speech-to-Speech com Avatar
|
| 2 |
+
|
| 3 |
+
## 📋 Visão Geral
|
| 4 |
+
|
| 5 |
+
Integração de componentes independentes para criar um sistema de conversação em tempo real com avatar:
|
| 6 |
+
|
| 7 |
+
- **Whisper** (STT - Speech-to-Text)
|
| 8 |
+
- **LLM** (Gemma/GPT - Processamento de linguagem)
|
| 9 |
+
- **TTS** (FishAudio - Text-to-Speech)
|
| 10 |
+
- **MuseTalk** (Geração de vídeo do avatar)
|
| 11 |
+
- **WebRTC** (Transporte de vídeo/áudio)
|
| 12 |
+
|
| 13 |
+
## 🎯 Objetivos
|
| 14 |
+
|
| 15 |
+
✅ **Componentes independentes** - Desenvolvimento e deploy separados
|
| 16 |
+
✅ **Baixa latência** - < 500ms de resposta total
|
| 17 |
+
✅ **Escalabilidade** - Cada componente pode escalar independentemente
|
| 18 |
+
✅ **Manutenibilidade** - Fácil atualização de cada parte
|
| 19 |
+
|
| 20 |
+
## 🔧 Arquitetura Recomendada: Microserviços
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 24 |
+
│ USUÁRIO │
|
| 25 |
+
│ (Navegador Web) │
|
| 26 |
+
└────────────────────────┬────────────────────────────────────────┘
|
| 27 |
+
│ WebRTC
|
| 28 |
+
▼
|
| 29 |
+
┌─────────────────────────────────────────────────────────────────┐
|
| 30 |
+
│ GATEWAY / ORQUESTRADOR │
|
| 31 |
+
│ (FastAPI/Node.js) │
|
| 32 |
+
│ - Gerencia sessões WebRTC │
|
| 33 |
+
│ - Roteia requisições para microserviços │
|
| 34 |
+
│ - Mantém estado da conversação │
|
| 35 |
+
└─────┬───────┬───────┬───────┬────────────────────────────────┘
|
| 36 |
+
│ │ │ │
|
| 37 |
+
│ │ │ │ gRPC/WebSocket/HTTP
|
| 38 |
+
▼ ▼ ▼ ▼
|
| 39 |
+
┌─────────┐ ┌─────┐ ┌─────┐ ┌──────────┐
|
| 40 |
+
│ Whisper │ │ LLM │ │ TTS │ │ MuseTalk │
|
| 41 |
+
│ Service │ │Svc │ │Svc │ │ Service │
|
| 42 |
+
│ │ │ │ │ │ │ │
|
| 43 |
+
│ Port: │ │Port:│ │Port:│ │ Port: │
|
| 44 |
+
│ 5001 │ │5002 │ │5003 │ │ 5004 │
|
| 45 |
+
└─────────┘ └─────┘ └─────┘ └──────────┘
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## 📊 Fluxo de Dados (Speech-to-Speech)
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
1. 🎤 Usuário fala
|
| 52 |
+
└─> WebRTC captura áudio
|
| 53 |
+
└─> Gateway recebe
|
| 54 |
+
│
|
| 55 |
+
2. 🔊 Speech-to-Text (Whisper)
|
| 56 |
+
└─> Gateway → Whisper Service (gRPC)
|
| 57 |
+
└─> Whisper retorna texto
|
| 58 |
+
└─> Latência: ~50-150ms
|
| 59 |
+
│
|
| 60 |
+
3. 🧠 LLM Processing (Gemma/GPT)
|
| 61 |
+
└─> Gateway → LLM Service (HTTP/gRPC)
|
| 62 |
+
└─> LLM retorna resposta (streaming)
|
| 63 |
+
└─> Latência: ~200-500ms
|
| 64 |
+
│
|
| 65 |
+
4. 🗣️ Text-to-Speech (FishAudio)
|
| 66 |
+
└─> Gateway → TTS Service (WebSocket)
|
| 67 |
+
└─> TTS retorna áudio (streaming)
|
| 68 |
+
└─> Latência: ~100-300ms
|
| 69 |
+
│
|
| 70 |
+
5. 🎬 Avatar Generation (MuseTalk)
|
| 71 |
+
└─> Gateway → MuseTalk Service (gRPC)
|
| 72 |
+
└─> MuseTalk gera vídeo (streaming)
|
| 73 |
+
└─> Latência: ~100-200ms
|
| 74 |
+
│
|
| 75 |
+
6. 📹 Video Delivery (WebRTC)
|
| 76 |
+
└─> Gateway → WebRTC
|
| 77 |
+
└─> Usuário recebe vídeo/áudio
|
| 78 |
+
└─> Latência: ~50-150ms
|
| 79 |
+
|
| 80 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
| 81 |
+
TOTAL: ~500-1300ms (pode ser reduzido com streaming)
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
## 🚀 Estratégias para Reduzir Latência
|
| 85 |
+
|
| 86 |
+
### 1. **Streaming em Pipeline** ⚡
|
| 87 |
+
|
| 88 |
+
Ao invés de esperar cada etapa terminar completamente, inicie a próxima assim que houver dados parciais:
|
| 89 |
+
|
| 90 |
+
```python
|
| 91 |
+
# ❌ SEQUENCIAL (alto latência)
|
| 92 |
+
texto = await whisper.transcribe(audio) # 150ms
|
| 93 |
+
resposta = await llm.generate(texto) # 500ms
|
| 94 |
+
audio_tts = await tts.synthesize(resposta) # 300ms
|
| 95 |
+
video = await musetalk.generate(audio_tts) # 200ms
|
| 96 |
+
# Total: 1150ms
|
| 97 |
+
|
| 98 |
+
# ✅ STREAMING (baixa latência)
|
| 99 |
+
async for texto_parcial in whisper.transcribe_stream(audio):
|
| 100 |
+
async for resposta_parcial in llm.generate_stream(texto_parcial):
|
| 101 |
+
async for audio_chunk in tts.synthesize_stream(resposta_parcial):
|
| 102 |
+
async for video_frame in musetalk.generate_stream(audio_chunk):
|
| 103 |
+
await webrtc.send_frame(video_frame)
|
| 104 |
+
# Primeira palavra: ~300ms
|
| 105 |
+
# Latência percebida muito menor!
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### 2. **Cache Inteligente** 💾
|
| 109 |
+
|
| 110 |
+
```python
|
| 111 |
+
# Cache de embeddings do LLM
|
| 112 |
+
llm_cache = {
|
| 113 |
+
"Olá, como vai?": cached_response_embedding,
|
| 114 |
+
"Qual é seu nome?": cached_response_embedding,
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
# Cache de áudio TTS (frases comuns)
|
| 118 |
+
tts_cache = {
|
| 119 |
+
"Olá!": audio_bytes,
|
| 120 |
+
"Sim": audio_bytes,
|
| 121 |
+
"Não": audio_bytes,
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
# Cache de vídeo MuseTalk (idle animations)
|
| 125 |
+
musetalk_cache = {
|
| 126 |
+
"idle": video_frames_loop,
|
| 127 |
+
"thinking": video_frames_loop,
|
| 128 |
+
}
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### 3. **Processamento Paralelo** 🔄
|
| 132 |
+
|
| 133 |
+
```python
|
| 134 |
+
import asyncio
|
| 135 |
+
|
| 136 |
+
# Processar múltiplas partes simultaneamente
|
| 137 |
+
async def process_speech(audio):
|
| 138 |
+
# Iniciar todas as tarefas em paralelo
|
| 139 |
+
whisper_task = asyncio.create_task(whisper.transcribe(audio))
|
| 140 |
+
|
| 141 |
+
# Assim que whisper terminar, iniciar LLM
|
| 142 |
+
texto = await whisper_task
|
| 143 |
+
|
| 144 |
+
# LLM e preparação do MuseTalk em paralelo
|
| 145 |
+
llm_task = asyncio.create_task(llm.generate(texto))
|
| 146 |
+
musetalk_prep_task = asyncio.create_task(musetalk.prepare())
|
| 147 |
+
|
| 148 |
+
# Aguardar ambos
|
| 149 |
+
resposta, _ = await asyncio.gather(llm_task, musetalk_prep_task)
|
| 150 |
+
|
| 151 |
+
# TTS
|
| 152 |
+
audio_tts = await tts.synthesize(resposta)
|
| 153 |
+
|
| 154 |
+
# MuseTalk (já preparado)
|
| 155 |
+
video = await musetalk.generate(audio_tts)
|
| 156 |
+
|
| 157 |
+
return video
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### 4. **Pré-computação** 🎯
|
| 161 |
+
|
| 162 |
+
```python
|
| 163 |
+
# Pré-carregar modelos na inicialização
|
| 164 |
+
class Services:
|
| 165 |
+
def __init__(self):
|
| 166 |
+
# Carregar tudo na memória
|
| 167 |
+
self.whisper = WhisperModel.load()
|
| 168 |
+
self.llm = LLMModel.load()
|
| 169 |
+
self.tts = TTSModel.load()
|
| 170 |
+
self.musetalk = MuseTalkModel.load()
|
| 171 |
+
|
| 172 |
+
# Pré-gerar frames de "idle"
|
| 173 |
+
self.idle_animation = self.musetalk.generate_idle_loop()
|
| 174 |
+
|
| 175 |
+
# Warmup (primeira inferência é sempre mais lenta)
|
| 176 |
+
self.whisper.transcribe(dummy_audio)
|
| 177 |
+
self.llm.generate("test")
|
| 178 |
+
self.tts.synthesize("test")
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
## 🔌 Protocolos de Comunicação
|
| 182 |
+
|
| 183 |
+
### Escolha do Protocolo por Serviço
|
| 184 |
+
|
| 185 |
+
| Serviço | Protocolo | Justificativa |
|
| 186 |
+
|---------|-----------|---------------|
|
| 187 |
+
| **Gateway ↔ Cliente** | WebRTC | Baixa latência, P2P, suporte a vídeo/áudio |
|
| 188 |
+
| **Gateway ↔ Whisper** | gRPC | Binário, rápido, suporte a streaming |
|
| 189 |
+
| **Gateway ↔ LLM** | gRPC + Streaming | Streaming de tokens, baixa latência |
|
| 190 |
+
| **Gateway ↔ TTS** | WebSocket | Streaming de áudio, bidirecional |
|
| 191 |
+
| **Gateway ↔ MuseTalk** | gRPC | Streaming de frames, binário eficiente |
|
| 192 |
+
|
| 193 |
+
### Exemplo: Interface gRPC para MuseTalk
|
| 194 |
+
|
| 195 |
+
```protobuf
|
| 196 |
+
// musetalk.proto
|
| 197 |
+
syntax = "proto3";
|
| 198 |
+
|
| 199 |
+
service MuseTalkService {
|
| 200 |
+
// Gerar vídeo a partir de áudio (streaming)
|
| 201 |
+
rpc GenerateVideo(stream AudioChunk) returns (stream VideoFrame) {}
|
| 202 |
+
|
| 203 |
+
// Obter animação idle
|
| 204 |
+
rpc GetIdleAnimation(IdleRequest) returns (stream VideoFrame) {}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
message AudioChunk {
|
| 208 |
+
bytes audio_data = 1;
|
| 209 |
+
int32 sample_rate = 2;
|
| 210 |
+
int32 chunk_index = 3;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
message VideoFrame {
|
| 214 |
+
bytes frame_data = 1;
|
| 215 |
+
int64 timestamp_ms = 2;
|
| 216 |
+
int32 frame_index = 3;
|
| 217 |
+
}
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
## 🏛️ Arquitetura de Deployment
|
| 221 |
+
|
| 222 |
+
### Opção 1: Containers Docker (Recomendado)
|
| 223 |
+
|
| 224 |
+
```yaml
|
| 225 |
+
# docker-compose.yml
|
| 226 |
+
version: '3.8'
|
| 227 |
+
|
| 228 |
+
services:
|
| 229 |
+
gateway:
|
| 230 |
+
build: ./gateway
|
| 231 |
+
ports:
|
| 232 |
+
- "8080:8080"
|
| 233 |
+
- "9000:9000" # WebRTC
|
| 234 |
+
depends_on:
|
| 235 |
+
- whisper
|
| 236 |
+
- llm
|
| 237 |
+
- tts
|
| 238 |
+
- musetalk
|
| 239 |
+
environment:
|
| 240 |
+
- WHISPER_URL=whisper:5001
|
| 241 |
+
- LLM_URL=llm:5002
|
| 242 |
+
- TTS_URL=tts:5003
|
| 243 |
+
- MUSETALK_URL=musetalk:5004
|
| 244 |
+
|
| 245 |
+
whisper:
|
| 246 |
+
build: ./services/whisper
|
| 247 |
+
ports:
|
| 248 |
+
- "5001:5001"
|
| 249 |
+
deploy:
|
| 250 |
+
resources:
|
| 251 |
+
reservations:
|
| 252 |
+
devices:
|
| 253 |
+
- driver: nvidia
|
| 254 |
+
count: 1
|
| 255 |
+
capabilities: [gpu]
|
| 256 |
+
|
| 257 |
+
llm:
|
| 258 |
+
build: ./services/llm
|
| 259 |
+
ports:
|
| 260 |
+
- "5002:5002"
|
| 261 |
+
deploy:
|
| 262 |
+
resources:
|
| 263 |
+
reservations:
|
| 264 |
+
devices:
|
| 265 |
+
- driver: nvidia
|
| 266 |
+
count: 1
|
| 267 |
+
capabilities: [gpu]
|
| 268 |
+
|
| 269 |
+
tts:
|
| 270 |
+
build: ./services/tts
|
| 271 |
+
ports:
|
| 272 |
+
- "5003:5003"
|
| 273 |
+
deploy:
|
| 274 |
+
resources:
|
| 275 |
+
reservations:
|
| 276 |
+
devices:
|
| 277 |
+
- driver: nvidia
|
| 278 |
+
count: 1
|
| 279 |
+
capabilities: [gpu]
|
| 280 |
+
|
| 281 |
+
musetalk:
|
| 282 |
+
build: ./services/musetalk
|
| 283 |
+
ports:
|
| 284 |
+
- "5004:5004"
|
| 285 |
+
deploy:
|
| 286 |
+
resources:
|
| 287 |
+
reservations:
|
| 288 |
+
devices:
|
| 289 |
+
- driver: nvidia
|
| 290 |
+
count: 1
|
| 291 |
+
capabilities: [gpu]
|
| 292 |
+
|
| 293 |
+
redis:
|
| 294 |
+
image: redis:alpine
|
| 295 |
+
ports:
|
| 296 |
+
- "6379:6379"
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
### Opção 2: Kubernetes (Produção/Escala)
|
| 300 |
+
|
| 301 |
+
```yaml
|
| 302 |
+
# k8s-deployment.yaml
|
| 303 |
+
apiVersion: apps/v1
|
| 304 |
+
kind: Deployment
|
| 305 |
+
metadata:
|
| 306 |
+
name: musetalk-service
|
| 307 |
+
spec:
|
| 308 |
+
replicas: 3
|
| 309 |
+
selector:
|
| 310 |
+
matchLabels:
|
| 311 |
+
app: musetalk
|
| 312 |
+
template:
|
| 313 |
+
metadata:
|
| 314 |
+
labels:
|
| 315 |
+
app: musetalk
|
| 316 |
+
spec:
|
| 317 |
+
containers:
|
| 318 |
+
- name: musetalk
|
| 319 |
+
image: your-registry/musetalk:latest
|
| 320 |
+
ports:
|
| 321 |
+
- containerPort: 5004
|
| 322 |
+
resources:
|
| 323 |
+
limits:
|
| 324 |
+
nvidia.com/gpu: 1
|
| 325 |
+
env:
|
| 326 |
+
- name: MODEL_PATH
|
| 327 |
+
value: /models/musetalk
|
| 328 |
+
---
|
| 329 |
+
apiVersion: v1
|
| 330 |
+
kind: Service
|
| 331 |
+
metadata:
|
| 332 |
+
name: musetalk-service
|
| 333 |
+
spec:
|
| 334 |
+
selector:
|
| 335 |
+
app: musetalk
|
| 336 |
+
ports:
|
| 337 |
+
- protocol: TCP
|
| 338 |
+
port: 5004
|
| 339 |
+
targetPort: 5004
|
| 340 |
+
type: ClusterIP
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
## 📁 Estrutura de Projeto Recomendada
|
| 344 |
+
|
| 345 |
+
```
|
| 346 |
+
avatar-conversation-system/
|
| 347 |
+
├── gateway/ # Orquestrador principal
|
| 348 |
+
│ ├── main.py
|
| 349 |
+
│ ├── websocket_handler.py
|
| 350 |
+
│ ├── session_manager.py
|
| 351 |
+
│ └── requirements.txt
|
| 352 |
+
│
|
| 353 |
+
├── services/
|
| 354 |
+
│ ├── whisper/ # STT Service
|
| 355 |
+
│ │ ├── server.py
|
| 356 |
+
│ │ ├── model_loader.py
|
| 357 |
+
│ │ ├── Dockerfile
|
| 358 |
+
│ │ └── requirements.txt
|
| 359 |
+
│ │
|
| 360 |
+
│ ├── llm/ # LLM Service
|
| 361 |
+
│ │ ├── server.py
|
| 362 |
+
│ │ ├── model_loader.py
|
| 363 |
+
│ │ ├── Dockerfile
|
| 364 |
+
│ │ └── requirements.txt
|
| 365 |
+
│ │
|
| 366 |
+
│ ├── tts/ # TTS Service
|
| 367 |
+
│ │ ├── server.py
|
| 368 |
+
│ │ ├── model_loader.py
|
| 369 |
+
│ │ ├── Dockerfile
|
| 370 |
+
│ │ └── requirements.txt
|
| 371 |
+
│ │
|
| 372 |
+
│ └── musetalk/ # Avatar Service
|
| 373 |
+
│ ├── server.py
|
| 374 |
+
│ ├── model_loader.py
|
| 375 |
+
│ ├── Dockerfile
|
| 376 |
+
│ └── requirements.txt
|
| 377 |
+
│
|
| 378 |
+
├── shared/ # Código compartilhado
|
| 379 |
+
│ ├── proto/ # gRPC proto files
|
| 380 |
+
│ │ ├── whisper.proto
|
| 381 |
+
│ │ ├── llm.proto
|
| 382 |
+
│ │ ├── tts.proto
|
| 383 |
+
│ │ └── musetalk.proto
|
| 384 |
+
│ │
|
| 385 |
+
│ └── utils/
|
| 386 |
+
│ ├── logger.py
|
| 387 |
+
│ ├── metrics.py
|
| 388 |
+
│ └── cache.py
|
| 389 |
+
│
|
| 390 |
+
├── docker-compose.yml
|
| 391 |
+
├── k8s/ # Kubernetes configs
|
| 392 |
+
│ ├── deployment.yaml
|
| 393 |
+
│ ├── service.yaml
|
| 394 |
+
│ └── ingress.yaml
|
| 395 |
+
│
|
| 396 |
+
└── README.md
|
| 397 |
+
```
|
| 398 |
+
|
| 399 |
+
## 💡 Gateway/Orquestrador - Código Exemplo
|
| 400 |
+
|
| 401 |
+
```python
|
| 402 |
+
# gateway/main.py
|
| 403 |
+
from fastapi import FastAPI, WebSocket
|
| 404 |
+
from fastapi.responses import HTMLResponse
|
| 405 |
+
import asyncio
|
| 406 |
+
import grpc
|
| 407 |
+
|
| 408 |
+
# Imports dos clientes gRPC
|
| 409 |
+
from services.whisper import whisper_pb2, whisper_pb2_grpc
|
| 410 |
+
from services.llm import llm_pb2, llm_pb2_grpc
|
| 411 |
+
from services.tts import tts_pb2, tts_pb2_grpc
|
| 412 |
+
from services.musetalk import musetalk_pb2, musetalk_pb2_grpc
|
| 413 |
+
|
| 414 |
+
app = FastAPI()
|
| 415 |
+
|
| 416 |
+
class ConversationOrchestrator:
|
| 417 |
+
def __init__(self):
|
| 418 |
+
# Conexões com microserviços
|
| 419 |
+
self.whisper_channel = grpc.aio.insecure_channel('whisper:5001')
|
| 420 |
+
self.whisper_client = whisper_pb2_grpc.WhisperServiceStub(self.whisper_channel)
|
| 421 |
+
|
| 422 |
+
self.llm_channel = grpc.aio.insecure_channel('llm:5002')
|
| 423 |
+
self.llm_client = llm_pb2_grpc.LLMServiceStub(self.llm_channel)
|
| 424 |
+
|
| 425 |
+
self.tts_channel = grpc.aio.insecure_channel('tts:5003')
|
| 426 |
+
self.tts_client = tts_pb2_grpc.TTSServiceStub(self.tts_channel)
|
| 427 |
+
|
| 428 |
+
self.musetalk_channel = grpc.aio.insecure_channel('musetalk:5004')
|
| 429 |
+
self.musetalk_client = musetalk_pb2_grpc.MuseTalkServiceStub(self.musetalk_channel)
|
| 430 |
+
|
| 431 |
+
async def process_speech_stream(self, audio_stream):
|
| 432 |
+
"""
|
| 433 |
+
Pipeline de streaming completo
|
| 434 |
+
"""
|
| 435 |
+
# 1. STT (Whisper)
|
| 436 |
+
async for text_chunk in self.whisper_client.TranscribeStream(audio_stream):
|
| 437 |
+
|
| 438 |
+
# 2. LLM (streaming de tokens)
|
| 439 |
+
async for response_token in self.llm_client.GenerateStream(text_chunk):
|
| 440 |
+
|
| 441 |
+
# 3. TTS (streaming de áudio)
|
| 442 |
+
async for audio_chunk in self.tts_client.SynthesizeStream(response_token):
|
| 443 |
+
|
| 444 |
+
# 4. MuseTalk (streaming de frames)
|
| 445 |
+
async for video_frame in self.musetalk_client.GenerateVideo(audio_chunk):
|
| 446 |
+
|
| 447 |
+
# 5. Enviar para WebRTC
|
| 448 |
+
yield video_frame
|
| 449 |
+
|
| 450 |
+
orchestrator = ConversationOrchestrator()
|
| 451 |
+
|
| 452 |
+
@app.websocket("/ws")
|
| 453 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 454 |
+
await websocket.accept()
|
| 455 |
+
|
| 456 |
+
try:
|
| 457 |
+
while True:
|
| 458 |
+
# Receber áudio do cliente
|
| 459 |
+
audio_data = await websocket.receive_bytes()
|
| 460 |
+
|
| 461 |
+
# Processar em pipeline
|
| 462 |
+
async for video_frame in orchestrator.process_speech_stream([audio_data]):
|
| 463 |
+
# Enviar frame de vídeo de volta
|
| 464 |
+
await websocket.send_bytes(video_frame.frame_data)
|
| 465 |
+
|
| 466 |
+
except Exception as e:
|
| 467 |
+
print(f"Error: {e}")
|
| 468 |
+
finally:
|
| 469 |
+
await websocket.close()
|
| 470 |
+
|
| 471 |
+
@app.get("/")
|
| 472 |
+
async def get():
|
| 473 |
+
# Retornar cliente WebRTC HTML
|
| 474 |
+
return HTMLResponse(open("client.html").read())
|
| 475 |
+
```
|
| 476 |
+
|
| 477 |
+
## 🎯 Métricas de Latência Alvo
|
| 478 |
+
|
| 479 |
+
| Componente | Latência Alvo | Como Alcançar |
|
| 480 |
+
|------------|---------------|---------------|
|
| 481 |
+
| **Whisper** | < 100ms | GPU, batching, modelo otimizado |
|
| 482 |
+
| **LLM** | < 300ms | Streaming, cache, modelo menor |
|
| 483 |
+
| **TTS** | < 150ms | GPU, streaming, cache de frases comuns |
|
| 484 |
+
| **MuseTalk** | < 150ms | GPU, pré-computação, streaming |
|
| 485 |
+
| **WebRTC** | < 100ms | Codec otimizado, servidor próximo |
|
| 486 |
+
| **TOTAL** | **< 500ms** | Pipeline streaming |
|
| 487 |
+
|
| 488 |
+
## 🔥 Otimizações Adicionais
|
| 489 |
+
|
| 490 |
+
### 1. **Quantização de Modelos**
|
| 491 |
+
|
| 492 |
+
```python
|
| 493 |
+
# Reduzir tamanho e latência dos modelos
|
| 494 |
+
from transformers import AutoModelForCausalLM
|
| 495 |
+
import torch
|
| 496 |
+
|
| 497 |
+
# Carregar modelo em FP16 (metade do tamanho)
|
| 498 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 499 |
+
"model-name",
|
| 500 |
+
torch_dtype=torch.float16,
|
| 501 |
+
device_map="auto"
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
# Ou INT8 (ainda menor)
|
| 505 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 506 |
+
"model-name",
|
| 507 |
+
load_in_8bit=True,
|
| 508 |
+
device_map="auto"
|
| 509 |
+
)
|
| 510 |
+
```
|
| 511 |
+
|
| 512 |
+
### 2. **Batching Dinâmico**
|
| 513 |
+
|
| 514 |
+
```python
|
| 515 |
+
# Agrupar múltiplas requisições para processar em batch
|
| 516 |
+
class BatchProcessor:
|
| 517 |
+
def __init__(self, max_batch_size=8, max_wait_ms=50):
|
| 518 |
+
self.queue = []
|
| 519 |
+
self.max_batch_size = max_batch_size
|
| 520 |
+
self.max_wait_ms = max_wait_ms
|
| 521 |
+
|
| 522 |
+
async def process(self, input_data):
|
| 523 |
+
# Adicionar à fila
|
| 524 |
+
future = asyncio.Future()
|
| 525 |
+
self.queue.append((input_data, future))
|
| 526 |
+
|
| 527 |
+
# Se batch está cheio ou timeout, processar
|
| 528 |
+
if len(self.queue) >= self.max_batch_size:
|
| 529 |
+
await self._process_batch()
|
| 530 |
+
else:
|
| 531 |
+
asyncio.create_task(self._wait_and_process())
|
| 532 |
+
|
| 533 |
+
return await future
|
| 534 |
+
|
| 535 |
+
async def _process_batch(self):
|
| 536 |
+
if not self.queue:
|
| 537 |
+
return
|
| 538 |
+
|
| 539 |
+
batch = self.queue[:self.max_batch_size]
|
| 540 |
+
self.queue = self.queue[self.max_batch_size:]
|
| 541 |
+
|
| 542 |
+
# Processar batch
|
| 543 |
+
inputs = [item[0] for item in batch]
|
| 544 |
+
results = await model.process_batch(inputs)
|
| 545 |
+
|
| 546 |
+
# Retornar resultados
|
| 547 |
+
for (_, future), result in zip(batch, results):
|
| 548 |
+
future.set_result(result)
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
### 3. **Health Checks e Circuit Breakers**
|
| 552 |
+
|
| 553 |
+
```python
|
| 554 |
+
from circuitbreaker import circuit
|
| 555 |
+
|
| 556 |
+
@circuit(failure_threshold=5, recovery_timeout=60)
|
| 557 |
+
async def call_llm_service(text):
|
| 558 |
+
try:
|
| 559 |
+
response = await llm_client.generate(text, timeout=2.0)
|
| 560 |
+
return response
|
| 561 |
+
except grpc.aio.AioRpcError as e:
|
| 562 |
+
# Fallback para resposta pré-definida
|
| 563 |
+
return "Desculpe, estou tendo problemas técnicos."
|
| 564 |
+
```
|
| 565 |
+
|
| 566 |
+
## 📊 Monitoramento
|
| 567 |
+
|
| 568 |
+
```python
|
| 569 |
+
from prometheus_client import Counter, Histogram
|
| 570 |
+
|
| 571 |
+
# Métricas
|
| 572 |
+
latency_histogram = Histogram(
|
| 573 |
+
'service_latency_seconds',
|
| 574 |
+
'Latência de cada serviço',
|
| 575 |
+
['service']
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
requests_counter = Counter(
|
| 579 |
+
'service_requests_total',
|
| 580 |
+
'Total de requisições por serviço',
|
| 581 |
+
['service', 'status']
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
# Uso
|
| 585 |
+
with latency_histogram.labels(service='whisper').time():
|
| 586 |
+
result = await whisper_client.transcribe(audio)
|
| 587 |
+
|
| 588 |
+
requests_counter.labels(service='whisper', status='success').inc()
|
| 589 |
+
```
|
| 590 |
+
|
| 591 |
+
## 🎯 Resumo
|
| 592 |
+
|
| 593 |
+
### Vantagens da Arquitetura de Microserviços
|
| 594 |
+
|
| 595 |
+
✅ **Desenvolvimento independente** - Cada equipe/pessoa pode trabalhar em um serviço
|
| 596 |
+
✅ **Deploy independente** - Atualizar um serviço sem afetar outros
|
| 597 |
+
✅ **Escalabilidade granular** - Escalar apenas o serviço que precisa
|
| 598 |
+
✅ **Tecnologias diferentes** - Cada serviço pode usar a stack mais adequada
|
| 599 |
+
✅ **Resiliência** - Falha em um serviço não derruba todo o sistema
|
| 600 |
+
✅ **Testabilidade** - Testar cada componente isoladamente
|
| 601 |
+
|
| 602 |
+
### Desvantagens
|
| 603 |
+
|
| 604 |
+
⚠️ **Complexidade** - Mais componentes para gerenciar
|
| 605 |
+
⚠️ **Latência de rede** - Comunicação entre serviços adiciona latência
|
| 606 |
+
⚠️ **Debugging** - Rastrear problemas através de múltiplos serviços
|
| 607 |
+
|
| 608 |
+
### Quando Usar Microserviços
|
| 609 |
+
|
| 610 |
+
- ✅ Sistema grande com múltiplos desenvolvedores
|
| 611 |
+
- ✅ Componentes que precisam escalar independentemente
|
| 612 |
+
- ✅ Necessidade de diferentes tecnologias/linguagens
|
| 613 |
+
- ❌ Sistema pequeno/MVP (monolito é mais simples)
|
| 614 |
+
|
| 615 |
+
---
|
| 616 |
+
|
| 617 |
+
**Próximos Passos:**
|
| 618 |
+
1. Implementar Gateway/Orquestrador básico
|
| 619 |
+
2. Criar interfaces gRPC para cada serviço
|
| 620 |
+
3. Dockerizar cada componente
|
| 621 |
+
4. Testar latência end-to-end
|
| 622 |
+
5. Otimizar gargalos identificados
|
webrtc-latency-test/docs/AUTOMATED_TESTING.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🧪 Teste Automatizado de Latência com Playwright
|
| 2 |
+
|
| 3 |
+
## 📋 Visão Geral
|
| 4 |
+
|
| 5 |
+
Este documento descreve como executar testes automatizados de latência usando Playwright headless para medir a performance do servidor WebRTC com um ou múltiplos usuários simultâneos.
|
| 6 |
+
|
| 7 |
+
## 🎯 Objetivos
|
| 8 |
+
|
| 9 |
+
- Testar latência de forma automatizada e repetível
|
| 10 |
+
- Medir latência com múltiplos usuários simultâneos
|
| 11 |
+
- Gerar relatórios JSON com métricas detalhadas
|
| 12 |
+
- Validar escalabilidade do servidor
|
| 13 |
+
|
| 14 |
+
## 📦 Requisitos
|
| 15 |
+
|
| 16 |
+
### Instalação
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
pip3 install playwright
|
| 20 |
+
playwright install chromium
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## 🚀 Como Executar
|
| 24 |
+
|
| 25 |
+
### Teste com 1 Usuário
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
python3 test_latency_playwright.py 1 true
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Teste com Múltiplos Usuários
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
# 5 usuários simultâneos
|
| 35 |
+
python3 test_latency_playwright.py 5 true
|
| 36 |
+
|
| 37 |
+
# 10 usuários simultâneos
|
| 38 |
+
python3 test_latency_playwright.py 10 true
|
| 39 |
+
|
| 40 |
+
# 3 usuários sequencialmente
|
| 41 |
+
python3 test_latency_playwright.py 3 false
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### Parâmetros
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
python3 test_latency_playwright.py <num_usuarios> <simultaneo>
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
- `<num_usuarios>`: Número de usuários para testar (padrão: 1)
|
| 51 |
+
- `<simultaneo>`: `true` para simultâneo, `false` para sequencial (padrão: true)
|
| 52 |
+
|
| 53 |
+
## 📊 O Que o Teste Faz
|
| 54 |
+
|
| 55 |
+
### Para Cada Usuário:
|
| 56 |
+
|
| 57 |
+
1. **Abre navegador headless** (Chromium)
|
| 58 |
+
2. **Navega para o servidor** (mede tempo de navegação)
|
| 59 |
+
3. **Clica em "Conectar"** (mede tempo de conexão)
|
| 60 |
+
4. **Aguarda estabilização** (3 segundos)
|
| 61 |
+
5. **Captura 10 medições de latência**:
|
| 62 |
+
- Captura timestamp local
|
| 63 |
+
- Lê timestamp do vídeo do servidor
|
| 64 |
+
- Calcula diferença (latência)
|
| 65 |
+
6. **Captura FPS final**
|
| 66 |
+
7. **Fecha navegador**
|
| 67 |
+
|
| 68 |
+
### Métricas Coletadas:
|
| 69 |
+
|
| 70 |
+
- ✅ Tempo de navegação (page load)
|
| 71 |
+
- ✅ Tempo de conexão WebRTC
|
| 72 |
+
- ✅ Latência (10 medições por usuário)
|
| 73 |
+
- Média
|
| 74 |
+
- Mínima
|
| 75 |
+
- Máxima
|
| 76 |
+
- ✅ FPS (frames por segundo)
|
| 77 |
+
- ✅ Taxa de sucesso de conexão
|
| 78 |
+
|
| 79 |
+
## 📈 Saída do Teste
|
| 80 |
+
|
| 81 |
+
### Console:
|
| 82 |
+
|
| 83 |
+
```
|
| 84 |
+
🎬 Teste Automatizado de Latência WebRTC
|
| 85 |
+
============================================================
|
| 86 |
+
|
| 87 |
+
🧪 Iniciando teste com 5 usuário(s)...
|
| 88 |
+
Modo: Simultâneo
|
| 89 |
+
Servidor: http://38.117.87.48:9000
|
| 90 |
+
|
| 91 |
+
Usuário 0: Conectado em 245.32ms
|
| 92 |
+
Usuário 1: Conectado em 278.91ms
|
| 93 |
+
Usuário 2: Conectado em 231.45ms
|
| 94 |
+
Usuário 3: Conectado em 289.12ms
|
| 95 |
+
Usuário 4: Conectado em 254.67ms
|
| 96 |
+
✅ Usuário 0: Latência=267.45ms, FPS=29.8
|
| 97 |
+
✅ Usuário 1: Latência=289.12ms, FPS=28.5
|
| 98 |
+
✅ Usuário 2: Latência=251.34ms, FPS=30.0
|
| 99 |
+
✅ Usuário 3: Latency=298.67ms, FPS=28.9
|
| 100 |
+
✅ Usuário 4: Latência=276.23ms, FPS=29.2
|
| 101 |
+
|
| 102 |
+
============================================================
|
| 103 |
+
📊 RESULTADOS AGREGADOS
|
| 104 |
+
============================================================
|
| 105 |
+
Usuários testados: 5
|
| 106 |
+
Conexões bem-sucedidas: 5/5
|
| 107 |
+
Taxa de sucesso: 100.0%
|
| 108 |
+
Tempo total do teste: 45.23s
|
| 109 |
+
|
| 110 |
+
📈 LATÊNCIA
|
| 111 |
+
Média: 276.56ms
|
| 112 |
+
Mínima: 251.34ms
|
| 113 |
+
Máxima: 298.67ms
|
| 114 |
+
Medidas totais: 50
|
| 115 |
+
|
| 116 |
+
⏱️ TEMPO DE CONEXÃO
|
| 117 |
+
Média: 259.89ms
|
| 118 |
+
Mínima: 231.45ms
|
| 119 |
+
Máxima: 289.12ms
|
| 120 |
+
|
| 121 |
+
🎬 FPS
|
| 122 |
+
Média: 29.3
|
| 123 |
+
Mínimo: 28.5
|
| 124 |
+
Máximo: 30.0
|
| 125 |
+
|
| 126 |
+
🎯 CLASSIFICAÇÃO
|
| 127 |
+
✅ BOM (276.56ms) - Ideal para conversação
|
| 128 |
+
============================================================
|
| 129 |
+
|
| 130 |
+
💾 Resultados salvos em: /root/livekit-poc/latency_test_20251224_143727.json
|
| 131 |
+
|
| 132 |
+
✅ Teste concluído!
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
### Arquivo JSON:
|
| 136 |
+
|
| 137 |
+
```json
|
| 138 |
+
{
|
| 139 |
+
"test_config": {
|
| 140 |
+
"num_users": 5,
|
| 141 |
+
"concurrent": true,
|
| 142 |
+
"server_url": "http://38.117.87.48:9000",
|
| 143 |
+
"test_date": "2024-12-24T14:37:27.123456"
|
| 144 |
+
},
|
| 145 |
+
"results": {
|
| 146 |
+
"successful_connections": 5,
|
| 147 |
+
"success_rate": 100.0,
|
| 148 |
+
"total_test_time": 45.23,
|
| 149 |
+
"latency": {
|
| 150 |
+
"all_measurements": [267.45, 289.12, 251.34, 298.67, 276.23, ...],
|
| 151 |
+
"avg": 276.56,
|
| 152 |
+
"min": 251.34,
|
| 153 |
+
"max": 298.67,
|
| 154 |
+
"num_measurements": 50
|
| 155 |
+
},
|
| 156 |
+
"connection_time": {
|
| 157 |
+
"all": [245.32, 278.91, 231.45, 289.12, 254.67],
|
| 158 |
+
"avg": 259.89,
|
| 159 |
+
"min": 231.45,
|
| 160 |
+
"max": 289.12
|
| 161 |
+
},
|
| 162 |
+
"fps": {
|
| 163 |
+
"all": [29.8, 28.5, 30.0, 28.9, 29.2],
|
| 164 |
+
"avg": 29.3,
|
| 165 |
+
"min": 28.5,
|
| 166 |
+
"max": 30.0
|
| 167 |
+
}
|
| 168 |
+
},
|
| 169 |
+
"user_results": [...]
|
| 170 |
+
}
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
## 🔧 Executando Testes
|
| 174 |
+
|
| 175 |
+
### Opção 1: Via SSH Tunnel (Recomendado)
|
| 176 |
+
|
| 177 |
+
1. Crie o tunnel SSH em um terminal:
|
| 178 |
+
|
| 179 |
+
```bash
|
| 180 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 -L 9000:localhost:9000
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
2. Em outro terminal, execute o teste:
|
| 184 |
+
|
| 185 |
+
```bash
|
| 186 |
+
cd /tmp
|
| 187 |
+
python3 test_latency_playwright.py 5 true
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
**Nota**: O teste usará `http://localhost:9000` por padrão. Para usar o tunnel, edite a linha:
|
| 191 |
+
|
| 192 |
+
```python
|
| 193 |
+
SERVER_URL = "http://localhost:9000" # Mude de 38.117.87.48 para localhost
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
### Opção 2: No Servidor Remoto
|
| 197 |
+
|
| 198 |
+
1. Copie o script para o servidor:
|
| 199 |
+
|
| 200 |
+
```bash
|
| 201 |
+
scp -i ~/.ssh/id_rsa -P 43060 test_latency_playwright.py root@38.117.87.48:/root/livekit-poc/
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
2. Instale Playwright no servidor:
|
| 205 |
+
|
| 206 |
+
```bash
|
| 207 |
+
ssh -o StrictHostKeyChecking=no -p 43060 root@38.117.87.48
|
| 208 |
+
pip3 install playwright
|
| 209 |
+
playwright install chromium
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
3. Execute o teste:
|
| 213 |
+
|
| 214 |
+
```bash
|
| 215 |
+
cd /root/livekit-poc
|
| 216 |
+
python3 test_latency_playwright.py 10 true
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
### Opção 3: Localmente com Servidor Acessível
|
| 220 |
+
|
| 221 |
+
Se o servidor for acessível externamente (firewall aberto):
|
| 222 |
+
|
| 223 |
+
```bash
|
| 224 |
+
python3 test_latency_playwright.py 5 true
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
## 📊 Interpretação dos Resultados
|
| 228 |
+
|
| 229 |
+
### Latência
|
| 230 |
+
|
| 231 |
+
| Latência | Classificação | Uso |
|
| 232 |
+
|----------|--------------|-----|
|
| 233 |
+
| < 100ms | ✅ Excelente | Quase imperceptível |
|
| 234 |
+
| 100-300ms | ✅ Bom | Ideal para conversação |
|
| 235 |
+
| 300-500ms | ⚠️ Aceitável | Pequenos delays possíveis |
|
| 236 |
+
| > 500ms | ❌ Ruim | Latência muito alta |
|
| 237 |
+
|
| 238 |
+
### FPS
|
| 239 |
+
|
| 240 |
+
| FPS | Classificação |
|
| 241 |
+
|-----|--------------|
|
| 242 |
+
| 28-30 | ✅ Excelente |
|
| 243 |
+
| 25-27 | ✅ Bom |
|
| 244 |
+
| 20-24 | ⚠️ Aceitável |
|
| 245 |
+
| < 20 | ❌ Ruim |
|
| 246 |
+
|
| 247 |
+
### Taxa de Sucesso
|
| 248 |
+
|
| 249 |
+
| Taxa | Classificação |
|
| 250 |
+
|------|--------------|
|
| 251 |
+
| 100% | ✅ Excelente |
|
| 252 |
+
| 90-99% | ✅ Bom |
|
| 253 |
+
| 80-89% | ⚠️ Aceitável |
|
| 254 |
+
| < 80% | ❌ Problemas |
|
| 255 |
+
|
| 256 |
+
## 🧪 Cenários de Teste
|
| 257 |
+
|
| 258 |
+
### Teste Básico (1 Usuário)
|
| 259 |
+
```bash
|
| 260 |
+
python3 test_latency_playwright.py 1 true
|
| 261 |
+
```
|
| 262 |
+
**Objetivo**: Validar funcionamento básico do servidor
|
| 263 |
+
|
| 264 |
+
### Teste de Carga (5 Usuários)
|
| 265 |
+
```bash
|
| 266 |
+
python3 test_latency_playwright.py 5 true
|
| 267 |
+
```
|
| 268 |
+
**Objetivo**: Testar com carga moderada
|
| 269 |
+
|
| 270 |
+
### Teste de Estresse (10 Usuários)
|
| 271 |
+
```bash
|
| 272 |
+
python3 test_latency_playwright.py 10 true
|
| 273 |
+
```
|
| 274 |
+
**Objetivo**: Testar limite atual do servidor
|
| 275 |
+
|
| 276 |
+
### Teste Sequencial (3 Usuários)
|
| 277 |
+
```bash
|
| 278 |
+
python3 test_latency_playwright.py 3 false
|
| 279 |
+
```
|
| 280 |
+
**Objetivo**: Testar se servidor se recupera entre conexões
|
| 281 |
+
|
| 282 |
+
## 📈 Analisando Resultados
|
| 283 |
+
|
| 284 |
+
### Ler arquivo JSON:
|
| 285 |
+
|
| 286 |
+
```python
|
| 287 |
+
import json
|
| 288 |
+
|
| 289 |
+
with open('latency_test_20251224_143727.json', 'r') as f:
|
| 290 |
+
data = json.load(f)
|
| 291 |
+
|
| 292 |
+
print(f"Latência média: {data['results']['latency']['avg']:.2f}ms")
|
| 293 |
+
print(f"FPS médio: {data['results']['fps']['avg']:.1f}")
|
| 294 |
+
print(f"Taxa de sucesso: {data['results']['success_rate']:.1f}%")
|
| 295 |
+
```
|
| 296 |
+
|
| 297 |
+
### Comparar Múltiplos Testes:
|
| 298 |
+
|
| 299 |
+
```python
|
| 300 |
+
import json
|
| 301 |
+
import glob
|
| 302 |
+
|
| 303 |
+
files = glob.glob('latency_test_*.json')
|
| 304 |
+
for file in sorted(files):
|
| 305 |
+
with open(file, 'r') as f:
|
| 306 |
+
data = json.load(f)
|
| 307 |
+
print(f"{file}: {data['results']['latency']['avg']:.2f}ms")
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
## 🔧 Troubleshooting
|
| 311 |
+
|
| 312 |
+
### Erro: "Page.goto: Timeout 30000ms exceeded"
|
| 313 |
+
|
| 314 |
+
**Causa**: Servidor não acessível
|
| 315 |
+
**Solução**:
|
| 316 |
+
- Verifique se servidor está rodando: `ps aux | grep webrtc-server`
|
| 317 |
+
- Use SSH tunnel se firewall bloquear acesso direto
|
| 318 |
+
|
| 319 |
+
### Erro: "Erro ao conectar"
|
| 320 |
+
|
| 321 |
+
**Causa**: WebRTC connection falhou
|
| 322 |
+
**Solução**:
|
| 323 |
+
- Verifique logs do servidor: `tail -f /tmp/webrtc.log`
|
| 324 |
+
- Verifique se navegador tem suporte a WebRTC
|
| 325 |
+
|
| 326 |
+
### Latência muito alta (> 500ms)
|
| 327 |
+
|
| 328 |
+
**Causas possíveis**:
|
| 329 |
+
- Rede lenta
|
| 330 |
+
- Sobrecarga do servidor
|
| 331 |
+
- Problemas no WebRTC
|
| 332 |
+
|
| 333 |
+
**Solução**:
|
| 334 |
+
- Execute com menos usuários
|
| 335 |
+
- Verifique recursos do servidor: `top`
|
| 336 |
+
- Teste rede: `ping 38.117.87.48`
|
| 337 |
+
|
| 338 |
+
## 📝 Relatório de Teste Exemplo
|
| 339 |
+
|
| 340 |
+
```markdown
|
| 341 |
+
# Relatório de Teste de Latência - 24/12/2024
|
| 342 |
+
|
| 343 |
+
## Configuração do Teste
|
| 344 |
+
- Usuários: 10
|
| 345 |
+
- Modo: Simultâneo
|
| 346 |
+
- Servidor: http://38.117.87.48:9000
|
| 347 |
+
|
| 348 |
+
## Resultados
|
| 349 |
+
|
| 350 |
+
### Latência
|
| 351 |
+
- Média: 289.45ms ✅
|
| 352 |
+
- Mínima: 251.23ms
|
| 353 |
+
- Máxima: 342.67ms
|
| 354 |
+
|
| 355 |
+
### Conexão
|
| 356 |
+
- Sucesso: 10/10 (100%)
|
| 357 |
+
- Tempo médio: 267.89ms
|
| 358 |
+
|
| 359 |
+
### Performance
|
| 360 |
+
- FPS médio: 29.1 ✅
|
| 361 |
+
|
| 362 |
+
## Conclusão
|
| 363 |
+
✅ **APROVADO** - Servidor apresenta latência aceitável para conversação
|
| 364 |
+
- Latência dentro do esperado (100-300ms)
|
| 365 |
+
- Taxa de sucesso de 100%
|
| 366 |
+
- FPS estável em 30
|
| 367 |
+
- Recomendado para produção até 20 usuários simultâneos
|
| 368 |
+
|
| 369 |
+
## Recomendações
|
| 370 |
+
1. Monitorar latência com 20+ usuários
|
| 371 |
+
2. Considerar LiveKit Server para escala
|
| 372 |
+
3. Adicionar monitoramento contínuo
|
| 373 |
+
```
|
| 374 |
+
|
| 375 |
+
## 🚀 Próximos Passos
|
| 376 |
+
|
| 377 |
+
1. **Automatizar Testes**: Criar cron job para testes periódicos
|
| 378 |
+
2. **Dashboard**: Criar dashboard com histórico de testes
|
| 379 |
+
3. **Alertas**: Configurar alertas para latência > 300ms
|
| 380 |
+
4. **CI/CD**: Integrar testes no pipeline de deploy
|
| 381 |
+
5. **Stress Test**: Testar com 50+ usuários
|
| 382 |
+
|
| 383 |
+
## 📚 Referências
|
| 384 |
+
|
| 385 |
+
- [Playwright Documentation](https://playwright.dev/python/)
|
| 386 |
+
- [WebRTC Performance](https://webrtc.org/getting-started/performance)
|
| 387 |
+
- [Latency Best Practices](https://docs.livekit.io/guides/performance)
|
| 388 |
+
|
| 389 |
+
---
|
| 390 |
+
|
| 391 |
+
**Teste automatizado pronto para uso!** 🎯
|
webrtc-latency-test/docs/TESTING_INSTRUCTIONS.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎬 Instruções de Teste - WebRTC Latency Test
|
| 2 |
+
|
| 3 |
+
## ✅ Servidor Ativo
|
| 4 |
+
|
| 5 |
+
**Status**: 🟢 RODANDO
|
| 6 |
+
**URL**: http://38.117.87.48:9000
|
| 7 |
+
**Porta**: 9000
|
| 8 |
+
|
| 9 |
+
## 📝 Como Testar
|
| 10 |
+
|
| 11 |
+
### Opção 1: Acesso Direto (se a porta 9000 estiver aberta no firewall)
|
| 12 |
+
|
| 13 |
+
Abra seu navegador e acesse:
|
| 14 |
+
```
|
| 15 |
+
http://38.117.87.48:9000
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
### Opção 2: Via SSH Tunnel (mais seguro)
|
| 19 |
+
|
| 20 |
+
1. Abra um terminal no seu computador local
|
| 21 |
+
2. Crie o tunnel SSH:
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 -L 9000:localhost:9000
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
3. Mantenha o terminal aberto
|
| 28 |
+
4. Abra seu navegador e acesse:
|
| 29 |
+
```
|
| 30 |
+
http://localhost:9000
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## 🧪 Teste de Latência
|
| 34 |
+
|
| 35 |
+
1. **Conecte**: Clique no botão "Conectar"
|
| 36 |
+
2. **Observe**: O vídeo com timestamps começará a ser exibido
|
| 37 |
+
3. **Meça**: Compare o timestamp no vídeo com o horário local
|
| 38 |
+
|
| 39 |
+
### Como Calcular a Latência
|
| 40 |
+
|
| 41 |
+
```
|
| 42 |
+
Latência = Hora Local - Timestamp no Vídeo
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
Exemplo:
|
| 46 |
+
- Timestamp no vídeo: `14:30:15.500`
|
| 47 |
+
- Hora local ao receber: `14:30:15.750`
|
| 48 |
+
- **Latência: 250ms**
|
| 49 |
+
|
| 50 |
+
## 📊 Métricas Exibidas
|
| 51 |
+
|
| 52 |
+
O cliente mostra:
|
| 53 |
+
- **FPS Recebidos**: Frames por segundo no cliente
|
| 54 |
+
- **Frames Totais**: Total de frames recebidos
|
| 55 |
+
- **Tempo Decorrido**: Tempo desde o início da conexão
|
| 56 |
+
- **Latência Estimada**: Compare manualmente os timestamps
|
| 57 |
+
|
| 58 |
+
## 🎯 Expectativas
|
| 59 |
+
|
| 60 |
+
### Latência Ideal
|
| 61 |
+
- **< 100ms**: Excelente (quase imperceptível)
|
| 62 |
+
- **100-300ms**: ✅ Bom (ideal para conversação)
|
| 63 |
+
- **300-500ms**: Aceitável
|
| 64 |
+
- **> 500ms**: Ruim (notável delay)
|
| 65 |
+
|
| 66 |
+
### FPS Esperado
|
| 67 |
+
- **30 FPS**: Ideal para vídeo suave
|
| 68 |
+
- **15-25 FPS**: Aceitável
|
| 69 |
+
- **< 15 FPS**: Problema na conexão ou servidor
|
| 70 |
+
|
| 71 |
+
## 🔍 Diagnóstico
|
| 72 |
+
|
| 73 |
+
### Vídeo não aparece
|
| 74 |
+
- Verifique se o servidor está rodando:
|
| 75 |
+
```bash
|
| 76 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 "ps aux | grep webrtc-server"
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
### Conexão falha
|
| 80 |
+
- Verifique logs do servidor:
|
| 81 |
+
```bash
|
| 82 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 "tail -f /tmp/webrtc-server.log"
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### Latência muito alta (> 500ms)
|
| 86 |
+
- Verifique sua conexão de internet
|
| 87 |
+
- Verifique carga do servidor:
|
| 88 |
+
```bash
|
| 89 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 "top -n 1 | head -20"
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
## 📋 Arquivos no Servidor
|
| 93 |
+
|
| 94 |
+
Localização: `/root/livekit-poc/`
|
| 95 |
+
- `webrtc-server.py` - Servidor WebRTC
|
| 96 |
+
- `webrtc-client.html` - Cliente web
|
| 97 |
+
- `requirements.txt` - Dependências Python
|
| 98 |
+
- `README.md` - Documentação completa
|
| 99 |
+
|
| 100 |
+
## 🛠 Comandos Úteis
|
| 101 |
+
|
| 102 |
+
### Verificar status do servidor
|
| 103 |
+
```bash
|
| 104 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 "ps aux | grep webrtc-server"
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
### Verificar logs em tempo real
|
| 108 |
+
```bash
|
| 109 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 "tail -f /tmp/webrtc-server.log"
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
### Reiniciar o servidor
|
| 113 |
+
```bash
|
| 114 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 "cd /root/livekit-poc && pkill -f webrtc-server && nohup python3 webrtc-server.py > /tmp/webrtc-server.log 2>&1 &"
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
### Parar o servidor
|
| 118 |
+
```bash
|
| 119 |
+
ssh -i ~/.ssh/id_rsa -p 43060 root@38.117.87.48 "pkill -f webrtc-server"
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## 🎨 O que você verá no vídeo
|
| 123 |
+
|
| 124 |
+
O vídeo gerado mostra:
|
| 125 |
+
- **Título**: "WebRTC Latency Test"
|
| 126 |
+
- **Timestamp**: Hora atual com milissegundos (preciso)
|
| 127 |
+
- **Frame Number**: Contador de frames
|
| 128 |
+
- **FPS**: Taxa de frames por segundo
|
| 129 |
+
- **Elapsed Time**: Tempo decorrido desde o início
|
| 130 |
+
- **Borda Colorida**: Muda de cor continuamente (visualiza atualização)
|
| 131 |
+
|
| 132 |
+
## 💡 Dicas
|
| 133 |
+
|
| 134 |
+
1. **Teste múltiplas vezes**: A latência pode variar
|
| 135 |
+
2. **Teste em diferentes momentos**: Condições de rede mudam
|
| 136 |
+
3. **Teste em diferentes dispositivos**: Desktop vs mobile
|
| 137 |
+
4. **Compare com outros serviços**: Use como baseline
|
| 138 |
+
|
| 139 |
+
## 📊 Relatório de Testes
|
| 140 |
+
|
| 141 |
+
Após testar, documente:
|
| 142 |
+
```
|
| 143 |
+
Data: ___________
|
| 144 |
+
Hora: ___________
|
| 145 |
+
Dispositivo: ___________
|
| 146 |
+
Conexão: ___________
|
| 147 |
+
|
| 148 |
+
Latência Média: _______ ms
|
| 149 |
+
Latência Mínima: _______ ms
|
| 150 |
+
Latência Máxima: _______ ms
|
| 151 |
+
FPS Recebidos: _______
|
| 152 |
+
|
| 153 |
+
Observações: ___________
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
## 🚀 Próximos Passos
|
| 157 |
+
|
| 158 |
+
Após validar a latência, você pode:
|
| 159 |
+
|
| 160 |
+
1. **Usar LiveKit Server**: Para produção com múltiplos usuários
|
| 161 |
+
2. **Adicionar Áudio**: Para conversação full-duplex
|
| 162 |
+
3. **Escalabilidade**: Testar com múltiplas conexões simultâneas
|
| 163 |
+
4. **Integrar com LLM**: Criar avatar conversacional
|
| 164 |
+
|
| 165 |
+
## 📞 Suporte
|
| 166 |
+
|
| 167 |
+
Se tiver problemas:
|
| 168 |
+
1. Verifique os logs do servidor
|
| 169 |
+
2. Verifique a conexão de rede
|
| 170 |
+
3. Use as ferramentas de debug do navegador:
|
| 171 |
+
- Chrome: `chrome://webrtc-internals`
|
| 172 |
+
- Firefox: `about:webrtc`
|
| 173 |
+
|
| 174 |
+
---
|
| 175 |
+
|
| 176 |
+
**Servidor configurado e pronto para testes!** 🎉
|
| 177 |
+
|
| 178 |
+
Acesse: http://38.117.87.48:9000
|
webrtc-latency-test/gateway/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY main.py .
|
| 9 |
+
|
| 10 |
+
EXPOSE 8080
|
| 11 |
+
|
| 12 |
+
CMD ["python", "main.py"]
|
webrtc-latency-test/gateway/main.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Gateway/Orquestrador de Microserviços
|
| 4 |
+
Integra Whisper, LLM, TTS e MuseTalk para conversação com avatar
|
| 5 |
+
"""
|
| 6 |
+
import asyncio
|
| 7 |
+
import time
|
| 8 |
+
import logging
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Dict, Any
|
| 11 |
+
from fastapi import FastAPI, HTTPException
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
import httpx
|
| 14 |
+
import uvicorn
|
| 15 |
+
|
| 16 |
+
# Configuração de logging
|
| 17 |
+
logging.basicConfig(
|
| 18 |
+
level=logging.INFO,
|
| 19 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 20 |
+
)
|
| 21 |
+
logger = logging.getLogger("gateway")
|
| 22 |
+
|
| 23 |
+
app = FastAPI(title="Avatar Conversation Gateway", version="1.0.0")
|
| 24 |
+
|
| 25 |
+
# URLs dos microserviços
|
| 26 |
+
WHISPER_URL = "http://localhost:5001"
|
| 27 |
+
LLM_URL = "http://localhost:5002"
|
| 28 |
+
TTS_URL = "http://localhost:5003"
|
| 29 |
+
MUSETALK_URL = "http://localhost:5004"
|
| 30 |
+
|
| 31 |
+
# Models
|
| 32 |
+
class ConversationRequest(BaseModel):
|
| 33 |
+
audio_data: str # Base64 encoded audio
|
| 34 |
+
sample_rate: int = 16000
|
| 35 |
+
conversation_id: str = "default"
|
| 36 |
+
|
| 37 |
+
class ConversationResponse(BaseModel):
|
| 38 |
+
video_data: str # Base64 encoded video
|
| 39 |
+
transcript: str
|
| 40 |
+
llm_response: str
|
| 41 |
+
total_latency_ms: int
|
| 42 |
+
latency_breakdown: Dict[str, int]
|
| 43 |
+
timestamp: str
|
| 44 |
+
|
| 45 |
+
class LatencyMetrics(BaseModel):
|
| 46 |
+
whisper_ms: int
|
| 47 |
+
llm_ms: int
|
| 48 |
+
tts_ms: int
|
| 49 |
+
musetalk_ms: int
|
| 50 |
+
total_ms: int
|
| 51 |
+
timestamp: str
|
| 52 |
+
|
| 53 |
+
@app.get("/")
|
| 54 |
+
async def root():
|
| 55 |
+
return {
|
| 56 |
+
"service": "Avatar Conversation Gateway",
|
| 57 |
+
"status": "running",
|
| 58 |
+
"version": "1.0.0",
|
| 59 |
+
"microservices": {
|
| 60 |
+
"whisper": WHISPER_URL,
|
| 61 |
+
"llm": LLM_URL,
|
| 62 |
+
"tts": TTS_URL,
|
| 63 |
+
"musetalk": MUSETALK_URL
|
| 64 |
+
},
|
| 65 |
+
"timestamp": datetime.now().isoformat()
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
@app.get("/health")
|
| 69 |
+
async def health():
|
| 70 |
+
"""
|
| 71 |
+
Verifica saúde de todos os microserviços
|
| 72 |
+
"""
|
| 73 |
+
services_health = {}
|
| 74 |
+
|
| 75 |
+
async with httpx.AsyncClient() as client:
|
| 76 |
+
# Whisper
|
| 77 |
+
try:
|
| 78 |
+
resp = await client.get(f"{WHISPER_URL}/health", timeout=2.0)
|
| 79 |
+
services_health["whisper"] = resp.json()
|
| 80 |
+
except Exception as e:
|
| 81 |
+
services_health["whisper"] = {"status": "unhealthy", "error": str(e)}
|
| 82 |
+
|
| 83 |
+
# LLM
|
| 84 |
+
try:
|
| 85 |
+
resp = await client.get(f"{LLM_URL}/health", timeout=2.0)
|
| 86 |
+
services_health["llm"] = resp.json()
|
| 87 |
+
except Exception as e:
|
| 88 |
+
services_health["llm"] = {"status": "unhealthy", "error": str(e)}
|
| 89 |
+
|
| 90 |
+
# TTS
|
| 91 |
+
try:
|
| 92 |
+
resp = await client.get(f"{TTS_URL}/health", timeout=2.0)
|
| 93 |
+
services_health["tts"] = resp.json()
|
| 94 |
+
except Exception as e:
|
| 95 |
+
services_health["tts"] = {"status": "unhealthy", "error": str(e)}
|
| 96 |
+
|
| 97 |
+
# MuseTalk
|
| 98 |
+
try:
|
| 99 |
+
resp = await client.get(f"{MUSETALK_URL}/health", timeout=2.0)
|
| 100 |
+
services_health["musetalk"] = resp.json()
|
| 101 |
+
except Exception as e:
|
| 102 |
+
services_health["musetalk"] = {"status": "unhealthy", "error": str(e)}
|
| 103 |
+
|
| 104 |
+
all_healthy = all(
|
| 105 |
+
s.get("status") == "healthy"
|
| 106 |
+
for s in services_health.values()
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
return {
|
| 110 |
+
"status": "healthy" if all_healthy else "degraded",
|
| 111 |
+
"services": services_health
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
@app.post("/conversation", response_model=ConversationResponse)
|
| 115 |
+
async def conversation(request: ConversationRequest):
|
| 116 |
+
"""
|
| 117 |
+
Pipeline completo: Audio -> Whisper -> LLM -> TTS -> MuseTalk -> Video
|
| 118 |
+
Mede latência de cada etapa
|
| 119 |
+
"""
|
| 120 |
+
start_time = time.time()
|
| 121 |
+
latency_breakdown = {}
|
| 122 |
+
|
| 123 |
+
logger.info(f"Starting conversation pipeline (conversation_id: {request.conversation_id})")
|
| 124 |
+
|
| 125 |
+
async with httpx.AsyncClient() as client:
|
| 126 |
+
# 1. WHISPER: Audio -> Text (STT)
|
| 127 |
+
whisper_start = time.time()
|
| 128 |
+
try:
|
| 129 |
+
whisper_response = await client.post(
|
| 130 |
+
f"{WHISPER_URL}/transcribe",
|
| 131 |
+
json={
|
| 132 |
+
"audio_data": request.audio_data,
|
| 133 |
+
"sample_rate": request.sample_rate,
|
| 134 |
+
"language": "pt"
|
| 135 |
+
},
|
| 136 |
+
timeout=5.0
|
| 137 |
+
)
|
| 138 |
+
whisper_data = whisper_response.json()
|
| 139 |
+
transcript = whisper_data["text"]
|
| 140 |
+
latency_breakdown["whisper"] = int((time.time() - whisper_start) * 1000)
|
| 141 |
+
logger.info(f"Whisper: '{transcript}' ({latency_breakdown['whisper']}ms)")
|
| 142 |
+
except Exception as e:
|
| 143 |
+
logger.error(f"Whisper error: {e}")
|
| 144 |
+
raise HTTPException(status_code=500, detail=f"Whisper service error: {e}")
|
| 145 |
+
|
| 146 |
+
# 2. LLM: Text -> Response
|
| 147 |
+
llm_start = time.time()
|
| 148 |
+
try:
|
| 149 |
+
llm_response = await client.post(
|
| 150 |
+
f"{LLM_URL}/generate",
|
| 151 |
+
json={
|
| 152 |
+
"text": transcript,
|
| 153 |
+
"conversation_id": request.conversation_id,
|
| 154 |
+
"temperature": 0.7,
|
| 155 |
+
"max_tokens": 150
|
| 156 |
+
},
|
| 157 |
+
timeout=10.0
|
| 158 |
+
)
|
| 159 |
+
llm_data = llm_response.json()
|
| 160 |
+
llm_text = llm_data["text"]
|
| 161 |
+
latency_breakdown["llm"] = int((time.time() - llm_start) * 1000)
|
| 162 |
+
logger.info(f"LLM: '{llm_text}' ({latency_breakdown['llm']}ms)")
|
| 163 |
+
except Exception as e:
|
| 164 |
+
logger.error(f"LLM error: {e}")
|
| 165 |
+
raise HTTPException(status_code=500, detail=f"LLM service error: {e}")
|
| 166 |
+
|
| 167 |
+
# 3. TTS: Text -> Audio
|
| 168 |
+
tts_start = time.time()
|
| 169 |
+
try:
|
| 170 |
+
tts_response = await client.post(
|
| 171 |
+
f"{TTS_URL}/synthesize",
|
| 172 |
+
json={
|
| 173 |
+
"text": llm_text,
|
| 174 |
+
"voice_id": "pt-BR-male",
|
| 175 |
+
"speed": 1.0,
|
| 176 |
+
"sample_rate": 16000
|
| 177 |
+
},
|
| 178 |
+
timeout=10.0
|
| 179 |
+
)
|
| 180 |
+
tts_data = tts_response.json()
|
| 181 |
+
audio_data = tts_data["audio_data"]
|
| 182 |
+
latency_breakdown["tts"] = int((time.time() - tts_start) * 1000)
|
| 183 |
+
logger.info(f"TTS: {tts_data['duration_seconds']:.2f}s audio ({latency_breakdown['tts']}ms)")
|
| 184 |
+
except Exception as e:
|
| 185 |
+
logger.error(f"TTS error: {e}")
|
| 186 |
+
raise HTTPException(status_code=500, detail=f"TTS service error: {e}")
|
| 187 |
+
|
| 188 |
+
# 4. MuseTalk: Audio -> Video
|
| 189 |
+
musetalk_start = time.time()
|
| 190 |
+
try:
|
| 191 |
+
musetalk_response = await client.post(
|
| 192 |
+
f"{MUSETALK_URL}/generate-video",
|
| 193 |
+
json={
|
| 194 |
+
"audio_data": audio_data,
|
| 195 |
+
"sample_rate": 16000,
|
| 196 |
+
"avatar_id": "default"
|
| 197 |
+
},
|
| 198 |
+
timeout=15.0
|
| 199 |
+
)
|
| 200 |
+
musetalk_data = musetalk_response.json()
|
| 201 |
+
video_data = musetalk_data["video_data"]
|
| 202 |
+
latency_breakdown["musetalk"] = int((time.time() - musetalk_start) * 1000)
|
| 203 |
+
logger.info(f"MuseTalk: {musetalk_data['fps']}fps video ({latency_breakdown['musetalk']}ms)")
|
| 204 |
+
except Exception as e:
|
| 205 |
+
logger.error(f"MuseTalk error: {e}")
|
| 206 |
+
raise HTTPException(status_code=500, detail=f"MuseTalk service error: {e}")
|
| 207 |
+
|
| 208 |
+
# Calcular latência total
|
| 209 |
+
total_latency = int((time.time() - start_time) * 1000)
|
| 210 |
+
|
| 211 |
+
logger.info(f"Pipeline complete! Total latency: {total_latency}ms")
|
| 212 |
+
logger.info(f"Breakdown: {latency_breakdown}")
|
| 213 |
+
|
| 214 |
+
return ConversationResponse(
|
| 215 |
+
video_data=video_data,
|
| 216 |
+
transcript=transcript,
|
| 217 |
+
llm_response=llm_text,
|
| 218 |
+
total_latency_ms=total_latency,
|
| 219 |
+
latency_breakdown=latency_breakdown,
|
| 220 |
+
timestamp=datetime.now().isoformat()
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
@app.get("/metrics", response_model=LatencyMetrics)
|
| 224 |
+
async def get_metrics():
|
| 225 |
+
"""
|
| 226 |
+
Executa um teste rápido e retorna métricas de latência
|
| 227 |
+
"""
|
| 228 |
+
logger.info("Running latency test...")
|
| 229 |
+
|
| 230 |
+
# Dados de teste
|
| 231 |
+
test_request = ConversationRequest(
|
| 232 |
+
audio_data="dGVzdCBhdWRpbw==", # "test audio" em base64
|
| 233 |
+
sample_rate=16000,
|
| 234 |
+
conversation_id="test"
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
# Executar pipeline
|
| 238 |
+
response = await conversation(test_request)
|
| 239 |
+
|
| 240 |
+
return LatencyMetrics(
|
| 241 |
+
whisper_ms=response.latency_breakdown["whisper"],
|
| 242 |
+
llm_ms=response.latency_breakdown["llm"],
|
| 243 |
+
tts_ms=response.latency_breakdown["tts"],
|
| 244 |
+
musetalk_ms=response.latency_breakdown["musetalk"],
|
| 245 |
+
total_ms=response.total_latency_ms,
|
| 246 |
+
timestamp=response.timestamp
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if __name__ == "__main__":
|
| 250 |
+
logger.info("Starting Gateway on port 8080")
|
| 251 |
+
uvicorn.run(app, host="0.0.0.0", port=8080, log_level="info")
|
webrtc-latency-test/gateway/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
pydantic==2.5.0
|
| 4 |
+
httpx==0.25.2
|
| 5 |
+
aiohttp==3.9.1
|
webrtc-latency-test/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiortc>=1.6.0
|
| 2 |
+
aiohttp>=3.8.0
|
| 3 |
+
opencv-python>=4.8.0
|
| 4 |
+
numpy>=1.24.0
|
| 5 |
+
av>=10.0.0
|
webrtc-latency-test/scripts/install.sh
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#
|
| 3 |
+
# WebRTC Latency Test POC - Installation Script
|
| 4 |
+
# Installs dependencies for the WebRTC latency testing system
|
| 5 |
+
#
|
| 6 |
+
|
| 7 |
+
set -e
|
| 8 |
+
|
| 9 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 10 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 11 |
+
|
| 12 |
+
GREEN='\033[0;32m'
|
| 13 |
+
CYAN='\033[0;36m'
|
| 14 |
+
YELLOW='\033[1;33m'
|
| 15 |
+
NC='\033[0m'
|
| 16 |
+
|
| 17 |
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
| 18 |
+
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
| 19 |
+
header() { echo -e "\n${CYAN}$1${NC}"; }
|
| 20 |
+
|
| 21 |
+
echo ""
|
| 22 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 23 |
+
echo "║ WEBRTC LATENCY TEST - INSTALLATION ║"
|
| 24 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 25 |
+
echo ""
|
| 26 |
+
|
| 27 |
+
# ============================================================
|
| 28 |
+
# 1. CHECK PYTHON
|
| 29 |
+
# ============================================================
|
| 30 |
+
header "[1/5] Checking Python version..."
|
| 31 |
+
|
| 32 |
+
PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}')
|
| 33 |
+
PYTHON_MAJOR=$(echo $PYTHON_VERSION | cut -d. -f1)
|
| 34 |
+
PYTHON_MINOR=$(echo $PYTHON_VERSION | cut -d. -f2)
|
| 35 |
+
|
| 36 |
+
if [ "$PYTHON_MAJOR" -ge 3 ] && [ "$PYTHON_MINOR" -ge 8 ]; then
|
| 37 |
+
success "Python $PYTHON_VERSION detected"
|
| 38 |
+
else
|
| 39 |
+
echo -e "${YELLOW}⚠${NC} Python 3.8+ required (found $PYTHON_VERSION)"
|
| 40 |
+
echo " Please install Python 3.8 or later"
|
| 41 |
+
exit 1
|
| 42 |
+
fi
|
| 43 |
+
|
| 44 |
+
# ============================================================
|
| 45 |
+
# 2. INSTALL PYTHON DEPENDENCIES
|
| 46 |
+
# ============================================================
|
| 47 |
+
header "[2/5] Installing Python dependencies..."
|
| 48 |
+
|
| 49 |
+
cd "$PROJECT_DIR"
|
| 50 |
+
|
| 51 |
+
if [ -f "requirements.txt" ]; then
|
| 52 |
+
pip3 install -r requirements.txt --quiet
|
| 53 |
+
success "Python dependencies installed"
|
| 54 |
+
else
|
| 55 |
+
# Fallback: install common dependencies
|
| 56 |
+
pip3 install --quiet \
|
| 57 |
+
aiortc>=1.6.0 \
|
| 58 |
+
aiohttp>=3.8.0 \
|
| 59 |
+
opencv-python>=4.8.0 \
|
| 60 |
+
numpy>=1.24.0 \
|
| 61 |
+
av>=10.0.0 \
|
| 62 |
+
playwright>=1.40.0
|
| 63 |
+
success "Python dependencies installed (fallback)"
|
| 64 |
+
fi
|
| 65 |
+
|
| 66 |
+
# ============================================================
|
| 67 |
+
# 3. INSTALL PLAYWRIGHT BROWSER
|
| 68 |
+
# ============================================================
|
| 69 |
+
header "[3/5] Installing Playwright and Chromium..."
|
| 70 |
+
|
| 71 |
+
pip3 install playwright --quiet
|
| 72 |
+
|
| 73 |
+
# Install Chromium (headless browser for automated tests)
|
| 74 |
+
playwright install chromium --quiet 2>/dev/null || {
|
| 75 |
+
echo " Downloading Chromium (may take a moment)..."
|
| 76 |
+
playwright install chromium
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
success "Playwright and Chromium installed"
|
| 80 |
+
|
| 81 |
+
# ============================================================
|
| 82 |
+
# 4. VERIFY DOCKER
|
| 83 |
+
# ============================================================
|
| 84 |
+
header "[4/5] Checking Docker..."
|
| 85 |
+
|
| 86 |
+
if command -v docker &> /dev/null; then
|
| 87 |
+
DOCKER_VERSION=$(docker --version | awk '{print $3}')
|
| 88 |
+
success "Docker $DOCKER_VERSION detected"
|
| 89 |
+
else
|
| 90 |
+
warn "Docker not found (optional for LiveKit)"
|
| 91 |
+
echo " To install Docker:"
|
| 92 |
+
echo " curl -fsSL https://get.docker.com -o get-docker.sh"
|
| 93 |
+
echo " sh get-docker.sh"
|
| 94 |
+
fi
|
| 95 |
+
|
| 96 |
+
# ============================================================
|
| 97 |
+
# 5. CREATE LOG DIRECTORY
|
| 98 |
+
# ============================================================
|
| 99 |
+
header "[5/5] Setting up log directories..."
|
| 100 |
+
|
| 101 |
+
mkdir -p /tmp/webrtc-logs 2>/dev/null || true
|
| 102 |
+
success "Log directories created"
|
| 103 |
+
|
| 104 |
+
# ============================================================
|
| 105 |
+
# COMPLETE
|
| 106 |
+
# ============================================================
|
| 107 |
+
echo ""
|
| 108 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 109 |
+
echo "║ INSTALLATION COMPLETE! ║"
|
| 110 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 111 |
+
echo ""
|
| 112 |
+
echo " Components installed:"
|
| 113 |
+
echo " • Python WebRTC server (aiortc)"
|
| 114 |
+
echo " • OpenCV for video processing"
|
| 115 |
+
echo " • Playwright for automated tests"
|
| 116 |
+
echo " • Chromium headless browser"
|
| 117 |
+
echo ""
|
| 118 |
+
echo " To start the server:"
|
| 119 |
+
echo " $SCRIPT_DIR/start_webrtc.sh"
|
| 120 |
+
echo ""
|
| 121 |
+
echo " To run automated tests:"
|
| 122 |
+
echo " python3 test_latency_playwright.py <users> <concurrent>"
|
| 123 |
+
echo ""
|
| 124 |
+
success "Installation completed successfully!"
|
| 125 |
+
echo ""
|
webrtc-latency-test/scripts/start.sh
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#
|
| 3 |
+
# WebRTC Latency Test POC - Start Script
|
| 4 |
+
# Starts the WebRTC server for latency testing
|
| 5 |
+
#
|
| 6 |
+
|
| 7 |
+
set -e
|
| 8 |
+
|
| 9 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 10 |
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
| 11 |
+
|
| 12 |
+
GREEN='\033[0;32m'
|
| 13 |
+
YELLOW='\033[1;33m'
|
| 14 |
+
CYAN='\033[0;36m'
|
| 15 |
+
RED='\033[0;31m'
|
| 16 |
+
NC='\033[0m'
|
| 17 |
+
|
| 18 |
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
| 19 |
+
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
| 20 |
+
error() { echo -e "${RED}✗${NC} $1"; }
|
| 21 |
+
header() { echo -e "\n${CYAN}$1${NC}"; }
|
| 22 |
+
|
| 23 |
+
# Default configuration
|
| 24 |
+
HOST="0.0.0.0"
|
| 25 |
+
PORT=9000
|
| 26 |
+
LOG_FILE="/tmp/webrtc-server.log"
|
| 27 |
+
PID_FILE="/tmp/webrtc-server.pid"
|
| 28 |
+
SERVER_SCRIPT="$PROJECT_DIR/webrtc-server-fixed.py"
|
| 29 |
+
|
| 30 |
+
# Parse arguments
|
| 31 |
+
while [[ $# -gt 0 ]]; do
|
| 32 |
+
case $1 in
|
| 33 |
+
-p|--port)
|
| 34 |
+
PORT="$2"
|
| 35 |
+
shift 2
|
| 36 |
+
;;
|
| 37 |
+
-h|--host)
|
| 38 |
+
HOST="$2"
|
| 39 |
+
shift 2
|
| 40 |
+
;;
|
| 41 |
+
--no-daemon)
|
| 42 |
+
NO_DAEMON=true
|
| 43 |
+
shift
|
| 44 |
+
;;
|
| 45 |
+
*)
|
| 46 |
+
echo "Unknown option: $1"
|
| 47 |
+
echo "Usage: $0 [--port PORT] [--host HOST] [--no-daemon]"
|
| 48 |
+
exit 1
|
| 49 |
+
;;
|
| 50 |
+
esac
|
| 51 |
+
done
|
| 52 |
+
|
| 53 |
+
echo ""
|
| 54 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 55 |
+
echo "║ WEBRTC LATENCY TEST - START SERVER ║"
|
| 56 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 57 |
+
echo ""
|
| 58 |
+
|
| 59 |
+
# ============================================================
|
| 60 |
+
# CHECK IF SERVER SCRIPT EXISTS
|
| 61 |
+
# ============================================================
|
| 62 |
+
if [ ! -f "$SERVER_SCRIPT" ]; then
|
| 63 |
+
error "Server script not found: $SERVER_SCRIPT"
|
| 64 |
+
echo " Please install the project first:"
|
| 65 |
+
echo " $SCRIPT_DIR/install_webrtc.sh"
|
| 66 |
+
exit 1
|
| 67 |
+
fi
|
| 68 |
+
|
| 69 |
+
# ============================================================
|
| 70 |
+
# CHECK IF ALREADY RUNNING
|
| 71 |
+
# ============================================================
|
| 72 |
+
if [ -f "$PID_FILE" ]; then
|
| 73 |
+
PID=$(cat "$PID_FILE")
|
| 74 |
+
if ps -p "$PID" > /dev/null 2>&1; then
|
| 75 |
+
header "Server already running"
|
| 76 |
+
echo " PID: $PID"
|
| 77 |
+
echo " Port: $PORT"
|
| 78 |
+
echo ""
|
| 79 |
+
echo " To stop: $SCRIPT_DIR/stop_webrtc.sh"
|
| 80 |
+
exit 0
|
| 81 |
+
else
|
| 82 |
+
# Stale PID file, remove it
|
| 83 |
+
rm -f "$PID_FILE"
|
| 84 |
+
fi
|
| 85 |
+
fi
|
| 86 |
+
|
| 87 |
+
# ============================================================
|
| 88 |
+
# STOP EXISTING PROCESSES
|
| 89 |
+
# ============================================================
|
| 90 |
+
header "Stopping existing WebRTC server processes..."
|
| 91 |
+
|
| 92 |
+
# Kill any existing webrtc-server processes
|
| 93 |
+
pkill -f "webrtc-server" 2>/dev/null || true
|
| 94 |
+
sleep 1
|
| 95 |
+
|
| 96 |
+
# Kill any process using the port
|
| 97 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 98 |
+
lsof -ti :$PORT | xargs kill -9 2>/dev/null || true
|
| 99 |
+
fi
|
| 100 |
+
|
| 101 |
+
success "Cleanup completed"
|
| 102 |
+
|
| 103 |
+
# ============================================================
|
| 104 |
+
# START SERVER
|
| 105 |
+
# ============================================================
|
| 106 |
+
header "Starting WebRTC server..."
|
| 107 |
+
|
| 108 |
+
cd "$PROJECT_DIR"
|
| 109 |
+
|
| 110 |
+
if [ "$NO_DAEMON" = "true" ]; then
|
| 111 |
+
# Run in foreground
|
| 112 |
+
echo " Starting in foreground mode..."
|
| 113 |
+
echo " Host: $HOST"
|
| 114 |
+
echo " Port: $PORT"
|
| 115 |
+
echo " Log: $LOG_FILE"
|
| 116 |
+
echo ""
|
| 117 |
+
python3 -u "$SERVER_SCRIPT" > "$LOG_FILE" 2>&1
|
| 118 |
+
else
|
| 119 |
+
# Run in background
|
| 120 |
+
echo " Host: $HOST"
|
| 121 |
+
echo " Port: $PORT"
|
| 122 |
+
echo " Log: $LOG_FILE"
|
| 123 |
+
echo ""
|
| 124 |
+
|
| 125 |
+
# Start in background with nohup
|
| 126 |
+
nohup python3 -u "$SERVER_SCRIPT" > "$LOG_FILE" 2>&1 &
|
| 127 |
+
SERVER_PID=$!
|
| 128 |
+
|
| 129 |
+
# Save PID
|
| 130 |
+
echo $SERVER_PID > "$PID_FILE"
|
| 131 |
+
|
| 132 |
+
# Wait a moment for startup
|
| 133 |
+
sleep 3
|
| 134 |
+
|
| 135 |
+
# Check if process is still running
|
| 136 |
+
if ps -p $SERVER_PID > /dev/null 2>&1; then
|
| 137 |
+
success "Server started successfully"
|
| 138 |
+
echo " PID: $SERVER_PID"
|
| 139 |
+
|
| 140 |
+
# Wait for port to be listening
|
| 141 |
+
for i in {1..10}; do
|
| 142 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 143 |
+
success "Server listening on port $PORT"
|
| 144 |
+
break
|
| 145 |
+
fi
|
| 146 |
+
sleep 1
|
| 147 |
+
done
|
| 148 |
+
else
|
| 149 |
+
error "Server failed to start"
|
| 150 |
+
echo " Check log: $LOG_FILE"
|
| 151 |
+
rm -f "$PID_FILE"
|
| 152 |
+
exit 1
|
| 153 |
+
fi
|
| 154 |
+
fi
|
| 155 |
+
|
| 156 |
+
# ============================================================
|
| 157 |
+
# DISPLAY STATUS
|
| 158 |
+
# ============================================================
|
| 159 |
+
header "SERVER STATUS"
|
| 160 |
+
echo ""
|
| 161 |
+
|
| 162 |
+
printf " %-15s %-10s %s\n" "Service" "Port" "Status"
|
| 163 |
+
echo " ─────────────────────────────────"
|
| 164 |
+
|
| 165 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 166 |
+
printf " %-15s %-10s ${GREEN}✓ Online${NC}\n" "WebRTC Server" "$PORT"
|
| 167 |
+
|
| 168 |
+
# Show last few log lines
|
| 169 |
+
if [ -f "$LOG_FILE" ]; then
|
| 170 |
+
echo ""
|
| 171 |
+
echo " Last log entries:"
|
| 172 |
+
tail -5 "$LOG_FILE" | sed 's/^/ /'
|
| 173 |
+
fi
|
| 174 |
+
else
|
| 175 |
+
printf " %-15s %-10s ${RED}✗ Offline${NC}\n" "WebRTC Server" "$PORT"
|
| 176 |
+
echo ""
|
| 177 |
+
error "Server is not responding"
|
| 178 |
+
exit 1
|
| 179 |
+
fi
|
| 180 |
+
|
| 181 |
+
# ============================================================
|
| 182 |
+
# ACCESS INFORMATION
|
| 183 |
+
# ============================================================
|
| 184 |
+
header "ACCESS INFORMATION"
|
| 185 |
+
echo ""
|
| 186 |
+
|
| 187 |
+
if [ "$HOST" = "0.0.0.0" ]; then
|
| 188 |
+
echo " Local access: http://localhost:$PORT"
|
| 189 |
+
echo " Network access: http://$(hostname -I | awk '{print $1}'):$PORT"
|
| 190 |
+
else
|
| 191 |
+
echo " Server URL: http://$HOST:$PORT"
|
| 192 |
+
fi
|
| 193 |
+
|
| 194 |
+
echo ""
|
| 195 |
+
echo " Web client: Open http://localhost:$PORT in browser"
|
| 196 |
+
echo " Logs: tail -f $LOG_FILE"
|
| 197 |
+
echo ""
|
| 198 |
+
|
| 199 |
+
# ============================================================
|
| 200 |
+
# QUICK TEST COMMANDS
|
| 201 |
+
# ============================================================
|
| 202 |
+
echo " Quick test commands:"
|
| 203 |
+
echo " • Manual test: Open http://localhost:$PORT in browser"
|
| 204 |
+
echo " • Auto test: python3 test_latency_playwright.py 1 true"
|
| 205 |
+
echo " • Load test: python3 test_latency_playwright.py 5 true"
|
| 206 |
+
echo ""
|
| 207 |
+
echo " To stop server: $SCRIPT_DIR/stop_webrtc.sh"
|
| 208 |
+
echo ""
|
| 209 |
+
|
| 210 |
+
success "WebRTC server started successfully!"
|
| 211 |
+
echo ""
|
webrtc-latency-test/scripts/stop.sh
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
#
|
| 3 |
+
# WebRTC Latency Test POC - Stop Script
|
| 4 |
+
# Stops the WebRTC server
|
| 5 |
+
#
|
| 6 |
+
|
| 7 |
+
set -e
|
| 8 |
+
|
| 9 |
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
| 10 |
+
|
| 11 |
+
GREEN='\033[0;32m'
|
| 12 |
+
YELLOW='\033[1;33m'
|
| 13 |
+
RED='\033[0;31m'
|
| 14 |
+
CYAN='\033[0;36m'
|
| 15 |
+
NC='\033[0m'
|
| 16 |
+
|
| 17 |
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
| 18 |
+
warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
| 19 |
+
error() { echo -e "${RED}✗${NC} $1"; }
|
| 20 |
+
header() { echo -e "\n${CYAN}$1${NC}"; }
|
| 21 |
+
|
| 22 |
+
PID_FILE="/tmp/webrtc-server.pid"
|
| 23 |
+
PORT=9000
|
| 24 |
+
|
| 25 |
+
echo ""
|
| 26 |
+
echo "╔══════════════════════════════════════════════════════════════╗"
|
| 27 |
+
echo "║ WEBRTC LATENCY TEST - STOP SERVER ║"
|
| 28 |
+
echo "╚══════════════════════════════════════════════════════════════╝"
|
| 29 |
+
echo ""
|
| 30 |
+
|
| 31 |
+
# ============================================================
|
| 32 |
+
# CHECK IF SERVER IS RUNNING
|
| 33 |
+
# ============================================================
|
| 34 |
+
header "Checking server status..."
|
| 35 |
+
|
| 36 |
+
SERVER_RUNNING=false
|
| 37 |
+
|
| 38 |
+
# Check PID file
|
| 39 |
+
if [ -f "$PID_FILE" ]; then
|
| 40 |
+
PID=$(cat "$PID_FILE")
|
| 41 |
+
if ps -p "$PID" > /dev/null 2>&1; then
|
| 42 |
+
echo " Found server process (PID: $PID)"
|
| 43 |
+
SERVER_RUNNING=true
|
| 44 |
+
else
|
| 45 |
+
warn "PID file exists but process not running (stale)"
|
| 46 |
+
rm -f "$PID_FILE"
|
| 47 |
+
fi
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
# Also check for processes using the port
|
| 51 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 52 |
+
echo " Found process listening on port $PORT"
|
| 53 |
+
SERVER_RUNNING=true
|
| 54 |
+
fi
|
| 55 |
+
|
| 56 |
+
if [ "$SERVER_RUNNING" = false ]; then
|
| 57 |
+
echo ""
|
| 58 |
+
echo " ${YELLOW}Server is not running${NC}"
|
| 59 |
+
echo ""
|
| 60 |
+
exit 0
|
| 61 |
+
fi
|
| 62 |
+
|
| 63 |
+
# ============================================================
|
| 64 |
+
# STOP SERVER
|
| 65 |
+
# ============================================================
|
| 66 |
+
header "Stopping WebRTC server..."
|
| 67 |
+
|
| 68 |
+
# Kill by PID if available
|
| 69 |
+
if [ -f "$PID_FILE" ]; then
|
| 70 |
+
PID=$(cat "$PID_FILE")
|
| 71 |
+
if ps -p "$PID" > /dev/null 2>&1; then
|
| 72 |
+
echo " Stopping server (PID: $PID)..."
|
| 73 |
+
kill $PID 2>/dev/null || true
|
| 74 |
+
|
| 75 |
+
# Wait for process to stop
|
| 76 |
+
for i in {1..5}; do
|
| 77 |
+
if ! ps -p $PID > /dev/null 2>&1; then
|
| 78 |
+
success "Server stopped"
|
| 79 |
+
break
|
| 80 |
+
fi
|
| 81 |
+
sleep 1
|
| 82 |
+
done
|
| 83 |
+
|
| 84 |
+
# Force kill if still running
|
| 85 |
+
if ps -p $PID > /dev/null 2>&1; then
|
| 86 |
+
echo " Force killing server..."
|
| 87 |
+
kill -9 $PID 2>/dev/null || true
|
| 88 |
+
fi
|
| 89 |
+
fi
|
| 90 |
+
rm -f "$PID_FILE"
|
| 91 |
+
fi
|
| 92 |
+
|
| 93 |
+
# Kill any webrtc-server processes
|
| 94 |
+
echo " Killing webrtc-server processes..."
|
| 95 |
+
pkill -f "webrtc-server" 2>/dev/null || true
|
| 96 |
+
|
| 97 |
+
# Kill any process using the port
|
| 98 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 99 |
+
echo " Killing process using port $PORT..."
|
| 100 |
+
lsof -ti :$PORT | xargs kill -9 2>/dev/null || true
|
| 101 |
+
fi
|
| 102 |
+
|
| 103 |
+
# Wait a moment
|
| 104 |
+
sleep 2
|
| 105 |
+
|
| 106 |
+
success "Server stopped"
|
| 107 |
+
|
| 108 |
+
# ============================================================
|
| 109 |
+
# VERIFY STOPPED
|
| 110 |
+
# ============================================================
|
| 111 |
+
header "Verifying..."
|
| 112 |
+
|
| 113 |
+
STILL_RUNNING=false
|
| 114 |
+
|
| 115 |
+
if lsof -Pi :$PORT -sTCP:LISTEN -t >/dev/null 2>&1; then
|
| 116 |
+
error "Something is still listening on port $PORT"
|
| 117 |
+
STILL_RUNNING=true
|
| 118 |
+
elif pgrep -f "webrtc-server" > /dev/null 2>&1; then
|
| 119 |
+
error "Some webrtc-server processes still running"
|
| 120 |
+
STILL_RUNNING=true
|
| 121 |
+
fi
|
| 122 |
+
|
| 123 |
+
if [ "$STILL_RUNNING" = false ]; then
|
| 124 |
+
success "All processes stopped"
|
| 125 |
+
fi
|
| 126 |
+
|
| 127 |
+
# ============================================================
|
| 128 |
+
# DISPLAY LOG
|
| 129 |
+
# ============================================================
|
| 130 |
+
header "Last log entries"
|
| 131 |
+
|
| 132 |
+
LOG_FILE="/tmp/webrtc-server.log"
|
| 133 |
+
if [ -f "$LOG_FILE" ]; then
|
| 134 |
+
tail -10 "$LOG_FILE" | sed 's/^/ /'
|
| 135 |
+
else
|
| 136 |
+
echo " No log file found"
|
| 137 |
+
fi
|
| 138 |
+
|
| 139 |
+
echo ""
|
| 140 |
+
success "Stop completed!"
|
| 141 |
+
echo ""
|
webrtc-latency-test/server/webrtc-client.html
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="pt-BR">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>WebRTC Latency Test</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
body {
|
| 14 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 15 |
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
| 16 |
+
min-height: 100vh;
|
| 17 |
+
display: flex;
|
| 18 |
+
flex-direction: column;
|
| 19 |
+
align-items: center;
|
| 20 |
+
padding: 20px;
|
| 21 |
+
color: #fff;
|
| 22 |
+
}
|
| 23 |
+
h1 {
|
| 24 |
+
margin-bottom: 20px;
|
| 25 |
+
text-align: center;
|
| 26 |
+
font-size: 2em;
|
| 27 |
+
color: #00d4ff;
|
| 28 |
+
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
| 29 |
+
}
|
| 30 |
+
.container {
|
| 31 |
+
max-width: 1200px;
|
| 32 |
+
width: 100%;
|
| 33 |
+
}
|
| 34 |
+
.video-container {
|
| 35 |
+
background: #0a0a0a;
|
| 36 |
+
border-radius: 10px;
|
| 37 |
+
padding: 20px;
|
| 38 |
+
margin-bottom: 20px;
|
| 39 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
| 40 |
+
}
|
| 41 |
+
video {
|
| 42 |
+
width: 100%;
|
| 43 |
+
max-width: 640px;
|
| 44 |
+
display: block;
|
| 45 |
+
margin: 0 auto;
|
| 46 |
+
border-radius: 5px;
|
| 47 |
+
background: #000;
|
| 48 |
+
}
|
| 49 |
+
.controls {
|
| 50 |
+
display: flex;
|
| 51 |
+
gap: 10px;
|
| 52 |
+
margin-top: 20px;
|
| 53 |
+
justify-content: center;
|
| 54 |
+
}
|
| 55 |
+
button {
|
| 56 |
+
padding: 12px 24px;
|
| 57 |
+
font-size: 16px;
|
| 58 |
+
font-weight: bold;
|
| 59 |
+
border: none;
|
| 60 |
+
border-radius: 5px;
|
| 61 |
+
cursor: pointer;
|
| 62 |
+
transition: all 0.3s ease;
|
| 63 |
+
text-transform: uppercase;
|
| 64 |
+
letter-spacing: 1px;
|
| 65 |
+
}
|
| 66 |
+
button:disabled {
|
| 67 |
+
opacity: 0.5;
|
| 68 |
+
cursor: not-allowed;
|
| 69 |
+
}
|
| 70 |
+
.btn-connect {
|
| 71 |
+
background: linear-gradient(135deg, #00d4ff 0%, #0099cc 100%);
|
| 72 |
+
color: #000;
|
| 73 |
+
}
|
| 74 |
+
.btn-connect:hover:not(:disabled) {
|
| 75 |
+
transform: translateY(-2px);
|
| 76 |
+
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.4);
|
| 77 |
+
}
|
| 78 |
+
.btn-disconnect {
|
| 79 |
+
background: linear-gradient(135deg, #ff4757 0%, #cc3845 100%);
|
| 80 |
+
color: #fff;
|
| 81 |
+
}
|
| 82 |
+
.btn-disconnect:hover:not(:disabled) {
|
| 83 |
+
transform: translateY(-2px);
|
| 84 |
+
box-shadow: 0 4px 15px rgba(255, 71, 87, 0.4);
|
| 85 |
+
}
|
| 86 |
+
.stats {
|
| 87 |
+
display: grid;
|
| 88 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 89 |
+
gap: 15px;
|
| 90 |
+
margin-top: 20px;
|
| 91 |
+
}
|
| 92 |
+
.stat-card {
|
| 93 |
+
background: rgba(255, 255, 255, 0.05);
|
| 94 |
+
border-radius: 8px;
|
| 95 |
+
padding: 15px;
|
| 96 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 97 |
+
}
|
| 98 |
+
.stat-label {
|
| 99 |
+
font-size: 12px;
|
| 100 |
+
color: #888;
|
| 101 |
+
margin-bottom: 5px;
|
| 102 |
+
text-transform: uppercase;
|
| 103 |
+
letter-spacing: 1px;
|
| 104 |
+
}
|
| 105 |
+
.stat-value {
|
| 106 |
+
font-size: 24px;
|
| 107 |
+
font-weight: bold;
|
| 108 |
+
color: #00d4ff;
|
| 109 |
+
}
|
| 110 |
+
.status {
|
| 111 |
+
text-align: center;
|
| 112 |
+
padding: 10px;
|
| 113 |
+
border-radius: 5px;
|
| 114 |
+
margin-bottom: 15px;
|
| 115 |
+
font-weight: bold;
|
| 116 |
+
}
|
| 117 |
+
.status.connected {
|
| 118 |
+
background: rgba(0, 212, 255, 0.2);
|
| 119 |
+
color: #00d4ff;
|
| 120 |
+
border: 1px solid #00d4ff;
|
| 121 |
+
}
|
| 122 |
+
.status.disconnected {
|
| 123 |
+
background: rgba(255, 71, 87, 0.2);
|
| 124 |
+
color: #ff4757;
|
| 125 |
+
border: 1px solid #ff4757;
|
| 126 |
+
}
|
| 127 |
+
.status.connecting {
|
| 128 |
+
background: rgba(255, 200, 0, 0.2);
|
| 129 |
+
color: #ffc800;
|
| 130 |
+
border: 1px solid #ffc800;
|
| 131 |
+
}
|
| 132 |
+
.info {
|
| 133 |
+
background: rgba(255, 200, 0, 0.1);
|
| 134 |
+
border-left: 4px solid #ffc800;
|
| 135 |
+
padding: 10px 15px;
|
| 136 |
+
margin-top: 20px;
|
| 137 |
+
border-radius: 5px;
|
| 138 |
+
}
|
| 139 |
+
.info p {
|
| 140 |
+
font-size: 14px;
|
| 141 |
+
color: #ffc800;
|
| 142 |
+
line-height: 1.6;
|
| 143 |
+
}
|
| 144 |
+
.timestamp-display {
|
| 145 |
+
font-family: 'Courier New', monospace;
|
| 146 |
+
font-size: 14px;
|
| 147 |
+
color: #0f0;
|
| 148 |
+
margin-top: 10px;
|
| 149 |
+
padding: 10px;
|
| 150 |
+
background: #000;
|
| 151 |
+
border-radius: 5px;
|
| 152 |
+
text-align: center;
|
| 153 |
+
}
|
| 154 |
+
</style>
|
| 155 |
+
</head>
|
| 156 |
+
<body>
|
| 157 |
+
<h1>🎬 WebRTC Latency Test</h1>
|
| 158 |
+
|
| 159 |
+
<div class="container">
|
| 160 |
+
<div id="status" class="status disconnected">Desconectado</div>
|
| 161 |
+
|
| 162 |
+
<div class="video-container">
|
| 163 |
+
<video id="video" autoplay playsinline muted></video>
|
| 164 |
+
<div class="timestamp-display" id="timestampDisplay">
|
| 165 |
+
Waiting for video stream...
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div class="controls">
|
| 170 |
+
<button id="connectBtn" class="btn-connect" onclick="connect()">Conectar</button>
|
| 171 |
+
<button id="disconnectBtn" class="btn-disconnect" onclick="disconnect()" disabled>Desconectar</button>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<div class="stats">
|
| 175 |
+
<div class="stat-card">
|
| 176 |
+
<div class="stat-label">FPS Recebidos</div>
|
| 177 |
+
<div class="stat-value" id="fps">0</div>
|
| 178 |
+
</div>
|
| 179 |
+
<div class="stat-card">
|
| 180 |
+
<div class="stat-label">Frames Totais</div>
|
| 181 |
+
<div class="stat-value" id="totalFrames">0</div>
|
| 182 |
+
</div>
|
| 183 |
+
<div class="stat-card">
|
| 184 |
+
<div class="stat-label">Tempo Decorrido</div>
|
| 185 |
+
<div class="stat-value" id="elapsed">0s</div>
|
| 186 |
+
</div>
|
| 187 |
+
<div class="stat-card">
|
| 188 |
+
<div class="stat-label">Latência Estimada</div>
|
| 189 |
+
<div class="stat-value" id="latency">--</div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<div class="info">
|
| 194 |
+
<p><strong>Como medir a latência:</strong></p>
|
| 195 |
+
<p>1. Conecte ao servidor WebRTC</p>
|
| 196 |
+
<p>2. Observe o timestamp no vídeo (Time: HH:MM:SS.mmm)</p>
|
| 197 |
+
<p>3. Compare com o horário local exibido abaixo</p>
|
| 198 |
+
<p>4. Latência ≈ Hora Local - Timestamp no Vídeo</p>
|
| 199 |
+
<p><strong>Latência ideal para conversação: 100-300ms</strong></p>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<script>
|
| 204 |
+
let pc = null;
|
| 205 |
+
let videoTrack = null;
|
| 206 |
+
let frameCount = 0;
|
| 207 |
+
let startTime = null;
|
| 208 |
+
let connectionStartTime = null;
|
| 209 |
+
|
| 210 |
+
async function connect() {
|
| 211 |
+
const statusDiv = document.getElementById('status');
|
| 212 |
+
const connectBtn = document.getElementById('connectBtn');
|
| 213 |
+
const disconnectBtn = document.getElementById('disconnectBtn');
|
| 214 |
+
|
| 215 |
+
statusDiv.className = 'status connecting';
|
| 216 |
+
statusDiv.textContent = 'Conectando...';
|
| 217 |
+
connectBtn.disabled = true;
|
| 218 |
+
|
| 219 |
+
try {
|
| 220 |
+
// Criar RTCPeerConnection
|
| 221 |
+
pc = new RTCPeerConnection({
|
| 222 |
+
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
| 223 |
+
});
|
| 224 |
+
|
| 225 |
+
// Lidar com incoming track
|
| 226 |
+
pc.ontrack = (event) => {
|
| 227 |
+
console.log('Track received:', event.track.kind);
|
| 228 |
+
if (event.track.kind === 'video') {
|
| 229 |
+
videoTrack = event.track;
|
| 230 |
+
const videoElement = document.getElementById('video');
|
| 231 |
+
videoElement.srcObject = event.streams[0];
|
| 232 |
+
startMeasurement();
|
| 233 |
+
}
|
| 234 |
+
};
|
| 235 |
+
|
| 236 |
+
pc.onconnectionstatechange = () => {
|
| 237 |
+
console.log('Connection state:', pc.connectionState);
|
| 238 |
+
if (pc.connectionState === 'connected') {
|
| 239 |
+
statusDiv.className = 'status connected';
|
| 240 |
+
statusDiv.textContent = 'Conectado via WebRTC';
|
| 241 |
+
disconnectBtn.disabled = false;
|
| 242 |
+
connectionStartTime = Date.now();
|
| 243 |
+
} else if (pc.connectionState === 'disconnected' ||
|
| 244 |
+
pc.connectionState === 'failed') {
|
| 245 |
+
statusDiv.className = 'status disconnected';
|
| 246 |
+
statusDiv.textContent = 'Desconectado';
|
| 247 |
+
connectBtn.disabled = false;
|
| 248 |
+
disconnectBtn.disabled = true;
|
| 249 |
+
}
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
// Criar offer
|
| 253 |
+
const offer = await pc.createOffer({ offerToReceiveVideo: true });
|
| 254 |
+
await pc.setLocalDescription(offer);
|
| 255 |
+
|
| 256 |
+
// Aguardar ICE gathering
|
| 257 |
+
await new Promise(resolve => {
|
| 258 |
+
if (pc.iceGatheringState === 'complete') {
|
| 259 |
+
resolve();
|
| 260 |
+
} else {
|
| 261 |
+
pc.onicegatheringstatechange = () => {
|
| 262 |
+
if (pc.iceGatheringState === 'complete') {
|
| 263 |
+
resolve();
|
| 264 |
+
}
|
| 265 |
+
};
|
| 266 |
+
}
|
| 267 |
+
});
|
| 268 |
+
|
| 269 |
+
// Enviar offer ao servidor
|
| 270 |
+
const response = await fetch('/offer', {
|
| 271 |
+
method: 'POST',
|
| 272 |
+
headers: { 'Content-Type': 'application/json' },
|
| 273 |
+
body: JSON.stringify({ sdp: pc.localDescription.sdp })
|
| 274 |
+
});
|
| 275 |
+
|
| 276 |
+
if (!response.ok) {
|
| 277 |
+
throw new Error('Falha ao conectar ao servidor');
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
const answer = await response.json();
|
| 281 |
+
|
| 282 |
+
// Configurar remote description
|
| 283 |
+
await pc.setRemoteDescription(new RTCSessionDescription({
|
| 284 |
+
type: answer.type,
|
| 285 |
+
sdp: answer.sdp
|
| 286 |
+
}));
|
| 287 |
+
|
| 288 |
+
console.log('WebRTC connection established');
|
| 289 |
+
|
| 290 |
+
} catch (error) {
|
| 291 |
+
console.error('Connection error:', error);
|
| 292 |
+
statusDiv.className = 'status disconnected';
|
| 293 |
+
statusDiv.textContent = 'Erro: ' + error.message;
|
| 294 |
+
connectBtn.disabled = false;
|
| 295 |
+
disconnectBtn.disabled = true;
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
async function disconnect() {
|
| 300 |
+
if (pc) {
|
| 301 |
+
pc.close();
|
| 302 |
+
pc = null;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
const videoElement = document.getElementById('video');
|
| 306 |
+
videoElement.srcObject = null;
|
| 307 |
+
|
| 308 |
+
document.getElementById('status').className = 'status disconnected';
|
| 309 |
+
document.getElementById('status').textContent = 'Desconectado';
|
| 310 |
+
document.getElementById('connectBtn').disabled = false;
|
| 311 |
+
document.getElementById('disconnectBtn').disabled = true;
|
| 312 |
+
|
| 313 |
+
stopMeasurement();
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
function startMeasurement() {
|
| 317 |
+
frameCount = 0;
|
| 318 |
+
startTime = Date.now();
|
| 319 |
+
updateDisplay();
|
| 320 |
+
|
| 321 |
+
setInterval(updateDisplay, 1000);
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function stopMeasurement() {
|
| 325 |
+
document.getElementById('timestampDisplay').textContent = 'Waiting for video stream...';
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
function updateDisplay() {
|
| 329 |
+
if (!startTime) return;
|
| 330 |
+
|
| 331 |
+
const elapsed = (Date.now() - startTime) / 1000;
|
| 332 |
+
const currentTime = new Date();
|
| 333 |
+
const timestampText = currentTime.toLocaleTimeString() + '.' +
|
| 334 |
+
currentTime.getMilliseconds().toString().padStart(3, '0');
|
| 335 |
+
|
| 336 |
+
const fps = (frameCount / elapsed).toFixed(1);
|
| 337 |
+
|
| 338 |
+
document.getElementById('fps').textContent = fps;
|
| 339 |
+
document.getElementById('totalFrames').textContent = frameCount;
|
| 340 |
+
document.getElementById('elapsed').textContent = elapsed.toFixed(1) + 's';
|
| 341 |
+
|
| 342 |
+
document.getElementById('timestampDisplay').textContent =
|
| 343 |
+
`Local Time: ${timestampText} | Frame: ${frameCount} | FPS: ${fps}`;
|
| 344 |
+
|
| 345 |
+
// Estimativa de latência (baseada em análise visual)
|
| 346 |
+
// Em produção, você faria OCR para ler o timestamp do vídeo
|
| 347 |
+
if (connectionStartTime) {
|
| 348 |
+
const connectionTime = (Date.now() - connectionStartTime) / 1000;
|
| 349 |
+
document.getElementById('latency').textContent = 'Compare manualmente';
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
// Atualizar contador de frames ( aproximado via polling do vídeo)
|
| 354 |
+
setInterval(() => {
|
| 355 |
+
const video = document.getElementById('video');
|
| 356 |
+
if (!video.paused && !video.ended && video.readyState >= 2) {
|
| 357 |
+
frameCount++;
|
| 358 |
+
}
|
| 359 |
+
}, 1000 / 30); // Aprox 30 FPS
|
| 360 |
+
|
| 361 |
+
window.onload = function() {
|
| 362 |
+
console.log('WebRTC Latency Test loaded');
|
| 363 |
+
};
|
| 364 |
+
</script>
|
| 365 |
+
</body>
|
| 366 |
+
</html>
|
webrtc-latency-test/server/webrtc-server.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
WebRTC Server Simples para Teste de Latência
|
| 4 |
+
Versão simplificada sem borda animada
|
| 5 |
+
"""
|
| 6 |
+
import asyncio
|
| 7 |
+
import logging
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import numpy as np
|
| 10 |
+
import cv2
|
| 11 |
+
from aiortc import RTCPeerConnection, RTCSessionDescription, VideoStreamTrack
|
| 12 |
+
from av import VideoFrame
|
| 13 |
+
from aiohttp import web
|
| 14 |
+
|
| 15 |
+
logging.basicConfig(level=logging.INFO)
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
class VideoGeneratorTrack(VideoStreamTrack):
|
| 19 |
+
"""Track que gera vídeo com timestamps"""
|
| 20 |
+
|
| 21 |
+
def __init__(self):
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.frame_count = 0
|
| 24 |
+
self.start_time = datetime.now()
|
| 25 |
+
self.fps = 30
|
| 26 |
+
|
| 27 |
+
async def recv(self):
|
| 28 |
+
pts, time_base = await self.next_timestamp()
|
| 29 |
+
|
| 30 |
+
# Criar frame
|
| 31 |
+
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
| 32 |
+
|
| 33 |
+
# Fundo azul escuro
|
| 34 |
+
frame[:, :] = [20, 30, 50]
|
| 35 |
+
|
| 36 |
+
# Timestamp atual
|
| 37 |
+
now = datetime.now()
|
| 38 |
+
timestamp = now.strftime("%H:%M:%S.%f")[:-3]
|
| 39 |
+
elapsed = (now - self.start_time).total_seconds()
|
| 40 |
+
|
| 41 |
+
# Adicionar textos
|
| 42 |
+
cv2.putText(frame, "WebRTC Latency Test", (200, 100),
|
| 43 |
+
cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255, 255, 255), 3)
|
| 44 |
+
|
| 45 |
+
cv2.putText(frame, f"Time: {timestamp}", (50, 220),
|
| 46 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)
|
| 47 |
+
|
| 48 |
+
cv2.putText(frame, f"Frame: #{self.frame_count}", (50, 270),
|
| 49 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 200, 0), 2)
|
| 50 |
+
|
| 51 |
+
cv2.putText(frame, f"FPS: {self.fps}", (50, 320),
|
| 52 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 200, 0), 2)
|
| 53 |
+
|
| 54 |
+
cv2.putText(frame, f"Elapsed: {elapsed:.2f}s", (50, 370),
|
| 55 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 200, 255), 2)
|
| 56 |
+
|
| 57 |
+
# Borda azul simples (sem animação)
|
| 58 |
+
cv2.rectangle(frame, (10, 10), (630, 470), (0, 212, 255), 4)
|
| 59 |
+
|
| 60 |
+
self.frame_count += 1
|
| 61 |
+
|
| 62 |
+
# Converter para VideoFrame
|
| 63 |
+
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 64 |
+
video_frame = VideoFrame.from_ndarray(frame_rgb, format="rgb24")
|
| 65 |
+
video_frame.pts = pts
|
| 66 |
+
video_frame.time_base = time_base
|
| 67 |
+
|
| 68 |
+
return video_frame
|
| 69 |
+
|
| 70 |
+
class WebRTCServer:
|
| 71 |
+
def __init__(self):
|
| 72 |
+
self.pc = RTCPeerConnection()
|
| 73 |
+
self.video_track = VideoGeneratorTrack()
|
| 74 |
+
self.pc.addTrack(self.video_track)
|
| 75 |
+
|
| 76 |
+
async def handle_offer(self, offer_sdp):
|
| 77 |
+
offer = RTCSessionDescription(sdp=offer_sdp, type="offer")
|
| 78 |
+
await self.pc.setRemoteDescription(offer)
|
| 79 |
+
|
| 80 |
+
answer = await self.pc.createAnswer()
|
| 81 |
+
await self.pc.setLocalDescription(answer)
|
| 82 |
+
|
| 83 |
+
return {
|
| 84 |
+
"sdp": self.pc.localDescription.sdp,
|
| 85 |
+
"type": self.pc.localDescription.type
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
web_rtc_server = None
|
| 89 |
+
|
| 90 |
+
async def offer(request):
|
| 91 |
+
params = await request.json()
|
| 92 |
+
offer_sdp = params.get("sdp")
|
| 93 |
+
|
| 94 |
+
if not offer_sdp:
|
| 95 |
+
return web.json_response({"error": "No SDP provided"}, status=400)
|
| 96 |
+
|
| 97 |
+
global web_rtc_server
|
| 98 |
+
web_rtc_server = WebRTCServer()
|
| 99 |
+
|
| 100 |
+
answer = await web_rtc_server.handle_offer(offer_sdp)
|
| 101 |
+
logger.info("Received WebRTC offer, sent answer")
|
| 102 |
+
|
| 103 |
+
return web.json_response(answer)
|
| 104 |
+
|
| 105 |
+
async def index(request):
|
| 106 |
+
with open('/root/livekit-poc/webrtc-client.html') as f:
|
| 107 |
+
return web.Response(text=f.read(), content_type='text/html')
|
| 108 |
+
|
| 109 |
+
async def main():
|
| 110 |
+
app = web.Application()
|
| 111 |
+
app.router.add_get('/', index)
|
| 112 |
+
app.router.add_post('/offer', offer)
|
| 113 |
+
|
| 114 |
+
runner = web.AppRunner(app)
|
| 115 |
+
await runner.setup()
|
| 116 |
+
|
| 117 |
+
site = web.TCPSite(runner, '0.0.0.0', 9000)
|
| 118 |
+
await site.start()
|
| 119 |
+
|
| 120 |
+
logger.info("WebRTC Server rodando em http://0.0.0.0:9000")
|
| 121 |
+
logger.info("Abra o navegador e acesse: http://38.117.87.48:9000")
|
| 122 |
+
|
| 123 |
+
# Manter rodando
|
| 124 |
+
await asyncio.Event().wait()
|
| 125 |
+
|
| 126 |
+
if __name__ == "__main__":
|
| 127 |
+
try:
|
| 128 |
+
asyncio.run(main())
|
| 129 |
+
except KeyboardInterrupt:
|
| 130 |
+
logger.info("Servidor interrompido")
|
webrtc-latency-test/services/llm/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY server.py .
|
| 9 |
+
|
| 10 |
+
EXPOSE 5002
|
| 11 |
+
|
| 12 |
+
CMD ["python", "server.py"]
|
webrtc-latency-test/services/llm/requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
pydantic==2.5.0
|
webrtc-latency-test/services/llm/server.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Mock LLM Service - Language Model
|
| 4 |
+
Simula o serviço LLM (Gemma/GPT) para testes de latência
|
| 5 |
+
"""
|
| 6 |
+
import asyncio
|
| 7 |
+
import time
|
| 8 |
+
import logging
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from fastapi import FastAPI
|
| 11 |
+
from pydantic import BaseModel
|
| 12 |
+
import uvicorn
|
| 13 |
+
|
| 14 |
+
# Configuração de logging
|
| 15 |
+
logging.basicConfig(
|
| 16 |
+
level=logging.INFO,
|
| 17 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 18 |
+
)
|
| 19 |
+
logger = logging.getLogger("llm-service")
|
| 20 |
+
|
| 21 |
+
app = FastAPI(title="LLM Mock Service", version="1.0.0")
|
| 22 |
+
|
| 23 |
+
# Models
|
| 24 |
+
class TextRequest(BaseModel):
|
| 25 |
+
text: str
|
| 26 |
+
conversation_id: str = "default"
|
| 27 |
+
temperature: float = 0.7
|
| 28 |
+
max_tokens: int = 150
|
| 29 |
+
|
| 30 |
+
class TextResponse(BaseModel):
|
| 31 |
+
text: str
|
| 32 |
+
processing_time_ms: int
|
| 33 |
+
tokens_generated: int
|
| 34 |
+
timestamp: str
|
| 35 |
+
|
| 36 |
+
# Mock responses baseadas na entrada
|
| 37 |
+
MOCK_RESPONSES = {
|
| 38 |
+
"olá": "Olá! Como posso ajudá-lo hoje?",
|
| 39 |
+
"como": "Estou aqui para responder suas perguntas. O que você gostaria de saber?",
|
| 40 |
+
"qual": "Meu nome é Dumont AI, um assistente virtual inteligente.",
|
| 41 |
+
"obrigado": "De nada! Fico feliz em poder ajudar.",
|
| 42 |
+
"default": "Entendo sua pergunta. Deixe-me pensar sobre isso..."
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
@app.get("/")
|
| 46 |
+
async def root():
|
| 47 |
+
return {
|
| 48 |
+
"service": "LLM Mock Service",
|
| 49 |
+
"status": "running",
|
| 50 |
+
"version": "1.0.0",
|
| 51 |
+
"model": "mock-gemma-7b",
|
| 52 |
+
"timestamp": datetime.now().isoformat()
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
@app.get("/health")
|
| 56 |
+
async def health():
|
| 57 |
+
return {"status": "healthy"}
|
| 58 |
+
|
| 59 |
+
@app.post("/generate", response_model=TextResponse)
|
| 60 |
+
async def generate(request: TextRequest):
|
| 61 |
+
"""
|
| 62 |
+
Simula geração de resposta do LLM
|
| 63 |
+
Adiciona latência artificial de 200-300ms
|
| 64 |
+
"""
|
| 65 |
+
start_time = time.time()
|
| 66 |
+
logger.info(f"Received generation request: '{request.text[:50]}...'")
|
| 67 |
+
|
| 68 |
+
# Simular latência de processamento (250ms)
|
| 69 |
+
await asyncio.sleep(0.25)
|
| 70 |
+
|
| 71 |
+
# Selecionar resposta baseada na entrada
|
| 72 |
+
text_lower = request.text.lower()
|
| 73 |
+
response_text = MOCK_RESPONSES.get("default", "Entendo.")
|
| 74 |
+
|
| 75 |
+
for key, value in MOCK_RESPONSES.items():
|
| 76 |
+
if key in text_lower:
|
| 77 |
+
response_text = value
|
| 78 |
+
break
|
| 79 |
+
|
| 80 |
+
processing_time = int((time.time() - start_time) * 1000)
|
| 81 |
+
tokens = len(response_text.split())
|
| 82 |
+
|
| 83 |
+
response = TextResponse(
|
| 84 |
+
text=response_text,
|
| 85 |
+
processing_time_ms=processing_time,
|
| 86 |
+
tokens_generated=tokens,
|
| 87 |
+
timestamp=datetime.now().isoformat()
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
logger.info(f"Generation complete: '{response_text}' ({tokens} tokens, {processing_time}ms)")
|
| 91 |
+
|
| 92 |
+
return response
|
| 93 |
+
|
| 94 |
+
@app.post("/generate-stream")
|
| 95 |
+
async def generate_stream(request: TextRequest):
|
| 96 |
+
"""
|
| 97 |
+
Simula geração em streaming (token por token)
|
| 98 |
+
"""
|
| 99 |
+
logger.info(f"Received streaming generation request: '{request.text[:50]}...'")
|
| 100 |
+
|
| 101 |
+
# Selecionar resposta
|
| 102 |
+
text_lower = request.text.lower()
|
| 103 |
+
response_text = MOCK_RESPONSES.get("default", "Entendo.")
|
| 104 |
+
|
| 105 |
+
for key, value in MOCK_RESPONSES.items():
|
| 106 |
+
if key in text_lower:
|
| 107 |
+
response_text = value
|
| 108 |
+
break
|
| 109 |
+
|
| 110 |
+
# Dividir em tokens (palavras)
|
| 111 |
+
tokens = response_text.split()
|
| 112 |
+
|
| 113 |
+
chunks = []
|
| 114 |
+
for i, token in enumerate(tokens):
|
| 115 |
+
await asyncio.sleep(0.05) # 50ms por token
|
| 116 |
+
chunks.append({
|
| 117 |
+
"token": token + " ",
|
| 118 |
+
"token_index": i,
|
| 119 |
+
"is_final": i == len(tokens) - 1
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
return {"chunks": chunks}
|
| 123 |
+
|
| 124 |
+
if __name__ == "__main__":
|
| 125 |
+
logger.info("Starting LLM Mock Service on port 5002")
|
| 126 |
+
uvicorn.run(app, host="0.0.0.0", port=5002, log_level="info")
|
webrtc-latency-test/services/musetalk/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY server.py .
|
| 9 |
+
|
| 10 |
+
EXPOSE 5004
|
| 11 |
+
|
| 12 |
+
CMD ["python", "server.py"]
|
webrtc-latency-test/services/musetalk/requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
pydantic==2.5.0
|
webrtc-latency-test/services/musetalk/server.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Mock MuseTalk Service - Avatar Video Generation
|
| 4 |
+
Simula o serviço MuseTalk para testes de latência
|
| 5 |
+
"""
|
| 6 |
+
import asyncio
|
| 7 |
+
import time
|
| 8 |
+
import logging
|
| 9 |
+
import base64
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from fastapi import FastAPI
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
import uvicorn
|
| 14 |
+
|
| 15 |
+
# Configuração de logging
|
| 16 |
+
logging.basicConfig(
|
| 17 |
+
level=logging.INFO,
|
| 18 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 19 |
+
)
|
| 20 |
+
logger = logging.getLogger("musetalk-service")
|
| 21 |
+
|
| 22 |
+
app = FastAPI(title="MuseTalk Mock Service", version="1.0.0")
|
| 23 |
+
|
| 24 |
+
# Models
|
| 25 |
+
class AudioRequest(BaseModel):
|
| 26 |
+
audio_data: str # Base64 encoded
|
| 27 |
+
sample_rate: int = 16000
|
| 28 |
+
avatar_id: str = "default"
|
| 29 |
+
|
| 30 |
+
class VideoResponse(BaseModel):
|
| 31 |
+
video_data: str # Base64 encoded
|
| 32 |
+
fps: int
|
| 33 |
+
width: int
|
| 34 |
+
height: int
|
| 35 |
+
processing_time_ms: int
|
| 36 |
+
timestamp: str
|
| 37 |
+
|
| 38 |
+
@app.get("/")
|
| 39 |
+
async def root():
|
| 40 |
+
return {
|
| 41 |
+
"service": "MuseTalk Mock Service",
|
| 42 |
+
"status": "running",
|
| 43 |
+
"version": "1.0.0",
|
| 44 |
+
"avatars": ["default", "male-1", "female-1"],
|
| 45 |
+
"timestamp": datetime.now().isoformat()
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
@app.get("/health")
|
| 49 |
+
async def health():
|
| 50 |
+
return {"status": "healthy"}
|
| 51 |
+
|
| 52 |
+
@app.post("/generate-video", response_model=VideoResponse)
|
| 53 |
+
async def generate_video(request: AudioRequest):
|
| 54 |
+
"""
|
| 55 |
+
Simula geração de vídeo a partir de áudio
|
| 56 |
+
Adiciona latência artificial de 100-150ms
|
| 57 |
+
"""
|
| 58 |
+
start_time = time.time()
|
| 59 |
+
logger.info(f"Received video generation request (avatar: {request.avatar_id})")
|
| 60 |
+
|
| 61 |
+
# Simular latência de processamento (125ms)
|
| 62 |
+
await asyncio.sleep(0.125)
|
| 63 |
+
|
| 64 |
+
# Gerar vídeo mock (bytes vazios codificados em base64)
|
| 65 |
+
# Em produção, aqui seria o vídeo real gerado pelo MuseTalk
|
| 66 |
+
fps = 30
|
| 67 |
+
duration_seconds = 2.0 # Vídeo de 2 segundos
|
| 68 |
+
num_frames = int(fps * duration_seconds)
|
| 69 |
+
|
| 70 |
+
# Mock video data (seria frames H264/VP8 em produção)
|
| 71 |
+
mock_video_bytes = b'\x00' * (num_frames * 1024) # ~1KB por frame
|
| 72 |
+
video_data_b64 = base64.b64encode(mock_video_bytes).decode('utf-8')
|
| 73 |
+
|
| 74 |
+
processing_time = int((time.time() - start_time) * 1000)
|
| 75 |
+
|
| 76 |
+
response = VideoResponse(
|
| 77 |
+
video_data=video_data_b64,
|
| 78 |
+
fps=fps,
|
| 79 |
+
width=512,
|
| 80 |
+
height=512,
|
| 81 |
+
processing_time_ms=processing_time,
|
| 82 |
+
timestamp=datetime.now().isoformat()
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
logger.info(f"Video generation complete: {num_frames} frames @{fps}fps (took {processing_time}ms)")
|
| 86 |
+
|
| 87 |
+
return response
|
| 88 |
+
|
| 89 |
+
@app.post("/generate-video-stream")
|
| 90 |
+
async def generate_video_stream(request: AudioRequest):
|
| 91 |
+
"""
|
| 92 |
+
Simula geração de vídeo em streaming (frame por frame)
|
| 93 |
+
"""
|
| 94 |
+
logger.info(f"Received streaming video generation request (avatar: {request.avatar_id})")
|
| 95 |
+
|
| 96 |
+
fps = 30
|
| 97 |
+
duration_seconds = 2.0
|
| 98 |
+
num_frames = int(fps * duration_seconds)
|
| 99 |
+
|
| 100 |
+
frames = []
|
| 101 |
+
for i in range(num_frames):
|
| 102 |
+
await asyncio.sleep(1.0 / fps) # ~33ms por frame @ 30fps
|
| 103 |
+
|
| 104 |
+
# Mock frame data
|
| 105 |
+
mock_frame = b'\x00' * 1024 # 1KB por frame
|
| 106 |
+
frame_b64 = base64.b64encode(mock_frame).decode('utf-8')
|
| 107 |
+
|
| 108 |
+
frames.append({
|
| 109 |
+
"frame_data": frame_b64,
|
| 110 |
+
"timestamp_ms": int(time.time() * 1000),
|
| 111 |
+
"frame_index": i,
|
| 112 |
+
"is_final": i == num_frames - 1
|
| 113 |
+
})
|
| 114 |
+
|
| 115 |
+
return {"frames": frames}
|
| 116 |
+
|
| 117 |
+
@app.get("/idle-animation")
|
| 118 |
+
async def idle_animation():
|
| 119 |
+
"""
|
| 120 |
+
Retorna animação idle em loop
|
| 121 |
+
"""
|
| 122 |
+
logger.info("Received idle animation request")
|
| 123 |
+
|
| 124 |
+
# Mock de 30 frames de animação idle
|
| 125 |
+
fps = 30
|
| 126 |
+
num_frames = 30
|
| 127 |
+
|
| 128 |
+
frames = []
|
| 129 |
+
for i in range(num_frames):
|
| 130 |
+
mock_frame = b'\x00' * 1024
|
| 131 |
+
frame_b64 = base64.b64encode(mock_frame).decode('utf-8')
|
| 132 |
+
|
| 133 |
+
frames.append({
|
| 134 |
+
"frame_data": frame_b64,
|
| 135 |
+
"frame_index": i
|
| 136 |
+
})
|
| 137 |
+
|
| 138 |
+
return {
|
| 139 |
+
"frames": frames,
|
| 140 |
+
"fps": fps,
|
| 141 |
+
"loop": True
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__":
|
| 145 |
+
logger.info("Starting MuseTalk Mock Service on port 5004")
|
| 146 |
+
uvicorn.run(app, host="0.0.0.0", port=5004, log_level="info")
|
webrtc-latency-test/services/tts/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY server.py .
|
| 9 |
+
|
| 10 |
+
EXPOSE 5003
|
| 11 |
+
|
| 12 |
+
CMD ["python", "server.py"]
|
webrtc-latency-test/services/tts/requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
pydantic==2.5.0
|
webrtc-latency-test/services/tts/server.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Mock TTS Service - Text to Speech
|
| 4 |
+
Simula o serviço TTS (FishAudio) para testes de latência
|
| 5 |
+
"""
|
| 6 |
+
import asyncio
|
| 7 |
+
import time
|
| 8 |
+
import logging
|
| 9 |
+
import base64
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from fastapi import FastAPI
|
| 12 |
+
from pydantic import BaseModel
|
| 13 |
+
import uvicorn
|
| 14 |
+
|
| 15 |
+
# Configuração de logging
|
| 16 |
+
logging.basicConfig(
|
| 17 |
+
level=logging.INFO,
|
| 18 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 19 |
+
)
|
| 20 |
+
logger = logging.getLogger("tts-service")
|
| 21 |
+
|
| 22 |
+
app = FastAPI(title="TTS Mock Service", version="1.0.0")
|
| 23 |
+
|
| 24 |
+
# Models
|
| 25 |
+
class TextRequest(BaseModel):
|
| 26 |
+
text: str
|
| 27 |
+
voice_id: str = "pt-BR-male"
|
| 28 |
+
speed: float = 1.0
|
| 29 |
+
sample_rate: int = 16000
|
| 30 |
+
|
| 31 |
+
class AudioResponse(BaseModel):
|
| 32 |
+
audio_data: str # Base64 encoded
|
| 33 |
+
sample_rate: int
|
| 34 |
+
processing_time_ms: int
|
| 35 |
+
duration_seconds: float
|
| 36 |
+
timestamp: str
|
| 37 |
+
|
| 38 |
+
@app.get("/")
|
| 39 |
+
async def root():
|
| 40 |
+
return {
|
| 41 |
+
"service": "TTS Mock Service",
|
| 42 |
+
"status": "running",
|
| 43 |
+
"version": "1.0.0",
|
| 44 |
+
"voices": ["pt-BR-male", "pt-BR-female"],
|
| 45 |
+
"timestamp": datetime.now().isoformat()
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
@app.get("/health")
|
| 49 |
+
async def health():
|
| 50 |
+
return {"status": "healthy"}
|
| 51 |
+
|
| 52 |
+
@app.post("/synthesize", response_model=AudioResponse)
|
| 53 |
+
async def synthesize(request: TextRequest):
|
| 54 |
+
"""
|
| 55 |
+
Simula síntese de texto para áudio
|
| 56 |
+
Adiciona latência artificial de 100-150ms
|
| 57 |
+
"""
|
| 58 |
+
start_time = time.time()
|
| 59 |
+
logger.info(f"Received synthesis request: '{request.text[:50]}...' (voice: {request.voice_id})")
|
| 60 |
+
|
| 61 |
+
# Simular latência de processamento (125ms)
|
| 62 |
+
await asyncio.sleep(0.125)
|
| 63 |
+
|
| 64 |
+
# Gerar áudio mock (bytes vazios codificados em base64)
|
| 65 |
+
# Em produção, aqui seria o áudio real sintetizado
|
| 66 |
+
text_length = len(request.text)
|
| 67 |
+
duration = text_length * 0.05 # ~50ms por caractere
|
| 68 |
+
|
| 69 |
+
# Mock audio data (seria PCM audio em produção)
|
| 70 |
+
mock_audio_bytes = b'\x00' * int(request.sample_rate * duration)
|
| 71 |
+
audio_data_b64 = base64.b64encode(mock_audio_bytes).decode('utf-8')
|
| 72 |
+
|
| 73 |
+
processing_time = int((time.time() - start_time) * 1000)
|
| 74 |
+
|
| 75 |
+
response = AudioResponse(
|
| 76 |
+
audio_data=audio_data_b64,
|
| 77 |
+
sample_rate=request.sample_rate,
|
| 78 |
+
processing_time_ms=processing_time,
|
| 79 |
+
duration_seconds=duration,
|
| 80 |
+
timestamp=datetime.now().isoformat()
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
logger.info(f"Synthesis complete: {duration:.2f}s audio (took {processing_time}ms)")
|
| 84 |
+
|
| 85 |
+
return response
|
| 86 |
+
|
| 87 |
+
@app.post("/synthesize-stream")
|
| 88 |
+
async def synthesize_stream(request: TextRequest):
|
| 89 |
+
"""
|
| 90 |
+
Simula síntese em streaming (chunks de áudio)
|
| 91 |
+
"""
|
| 92 |
+
logger.info(f"Received streaming synthesis request: '{request.text[:50]}...'")
|
| 93 |
+
|
| 94 |
+
# Dividir texto em sentenças
|
| 95 |
+
sentences = request.text.split('.')
|
| 96 |
+
|
| 97 |
+
chunks = []
|
| 98 |
+
for i, sentence in enumerate(sentences):
|
| 99 |
+
if not sentence.strip():
|
| 100 |
+
continue
|
| 101 |
+
|
| 102 |
+
await asyncio.sleep(0.05) # 50ms por chunk
|
| 103 |
+
|
| 104 |
+
# Mock audio chunk
|
| 105 |
+
chunk_duration = len(sentence) * 0.05
|
| 106 |
+
mock_audio = b'\x00' * int(request.sample_rate * chunk_duration)
|
| 107 |
+
audio_b64 = base64.b64encode(mock_audio).decode('utf-8')
|
| 108 |
+
|
| 109 |
+
chunks.append({
|
| 110 |
+
"audio_data": audio_b64,
|
| 111 |
+
"chunk_index": i,
|
| 112 |
+
"is_final": i == len(sentences) - 1
|
| 113 |
+
})
|
| 114 |
+
|
| 115 |
+
return {"chunks": chunks}
|
| 116 |
+
|
| 117 |
+
if __name__ == "__main__":
|
| 118 |
+
logger.info("Starting TTS Mock Service on port 5003")
|
| 119 |
+
uvicorn.run(app, host="0.0.0.0", port=5003, log_level="info")
|
webrtc-latency-test/services/whisper/Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Instalar dependências
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Copiar código
|
| 10 |
+
COPY server.py .
|
| 11 |
+
|
| 12 |
+
# Expor porta
|
| 13 |
+
EXPOSE 5001
|
| 14 |
+
|
| 15 |
+
# Rodar servidor
|
| 16 |
+
CMD ["python", "server.py"]
|
webrtc-latency-test/services/whisper/requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
pydantic==2.5.0
|
webrtc-latency-test/services/whisper/server.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Mock Whisper Service - Speech to Text
|
| 4 |
+
Simula o serviço Whisper para testes de latência
|
| 5 |
+
"""
|
| 6 |
+
import asyncio
|
| 7 |
+
import time
|
| 8 |
+
import logging
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from fastapi import FastAPI, HTTPException
|
| 11 |
+
from pydantic import BaseModel
|
| 12 |
+
import uvicorn
|
| 13 |
+
|
| 14 |
+
# Configuração de logging
|
| 15 |
+
logging.basicConfig(
|
| 16 |
+
level=logging.INFO,
|
| 17 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 18 |
+
)
|
| 19 |
+
logger = logging.getLogger("whisper-service")
|
| 20 |
+
|
| 21 |
+
app = FastAPI(title="Whisper Mock Service", version="1.0.0")
|
| 22 |
+
|
| 23 |
+
# Models
|
| 24 |
+
class AudioRequest(BaseModel):
|
| 25 |
+
audio_data: str # Base64 encoded
|
| 26 |
+
sample_rate: int = 16000
|
| 27 |
+
language: str = "pt"
|
| 28 |
+
|
| 29 |
+
class TextResponse(BaseModel):
|
| 30 |
+
text: str
|
| 31 |
+
confidence: float
|
| 32 |
+
processing_time_ms: int
|
| 33 |
+
timestamp: str
|
| 34 |
+
|
| 35 |
+
# Mock responses
|
| 36 |
+
MOCK_RESPONSES = [
|
| 37 |
+
"Olá, como você está?",
|
| 38 |
+
"Qual é o seu nome?",
|
| 39 |
+
"Como posso ajudar você hoje?",
|
| 40 |
+
"Obrigado pela pergunta",
|
| 41 |
+
"Isso é muito interessante",
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
response_counter = 0
|
| 45 |
+
|
| 46 |
+
@app.get("/")
|
| 47 |
+
async def root():
|
| 48 |
+
return {
|
| 49 |
+
"service": "Whisper Mock Service",
|
| 50 |
+
"status": "running",
|
| 51 |
+
"version": "1.0.0",
|
| 52 |
+
"timestamp": datetime.now().isoformat()
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
@app.get("/health")
|
| 56 |
+
async def health():
|
| 57 |
+
return {"status": "healthy"}
|
| 58 |
+
|
| 59 |
+
@app.post("/transcribe", response_model=TextResponse)
|
| 60 |
+
async def transcribe(request: AudioRequest):
|
| 61 |
+
"""
|
| 62 |
+
Simula transcrição de áudio para texto
|
| 63 |
+
Adiciona latência artificial de 50-100ms
|
| 64 |
+
"""
|
| 65 |
+
global response_counter
|
| 66 |
+
|
| 67 |
+
start_time = time.time()
|
| 68 |
+
logger.info(f"Received transcription request (sample_rate: {request.sample_rate})")
|
| 69 |
+
|
| 70 |
+
# Simular latência de processamento (50-100ms)
|
| 71 |
+
await asyncio.sleep(0.075) # 75ms
|
| 72 |
+
|
| 73 |
+
# Selecionar resposta mock
|
| 74 |
+
text = MOCK_RESPONSES[response_counter % len(MOCK_RESPONSES)]
|
| 75 |
+
response_counter += 1
|
| 76 |
+
|
| 77 |
+
processing_time = int((time.time() - start_time) * 1000)
|
| 78 |
+
|
| 79 |
+
response = TextResponse(
|
| 80 |
+
text=text,
|
| 81 |
+
confidence=0.95,
|
| 82 |
+
processing_time_ms=processing_time,
|
| 83 |
+
timestamp=datetime.now().isoformat()
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
logger.info(f"Transcription complete: '{text}' (took {processing_time}ms)")
|
| 87 |
+
|
| 88 |
+
return response
|
| 89 |
+
|
| 90 |
+
@app.post("/transcribe-stream")
|
| 91 |
+
async def transcribe_stream(request: AudioRequest):
|
| 92 |
+
"""
|
| 93 |
+
Simula transcrição em streaming
|
| 94 |
+
Retorna chunks de texto progressivamente
|
| 95 |
+
"""
|
| 96 |
+
logger.info("Received streaming transcription request")
|
| 97 |
+
|
| 98 |
+
# Dividir resposta em palavras
|
| 99 |
+
text = MOCK_RESPONSES[response_counter % len(MOCK_RESPONSES)]
|
| 100 |
+
words = text.split()
|
| 101 |
+
|
| 102 |
+
chunks = []
|
| 103 |
+
for i, word in enumerate(words):
|
| 104 |
+
await asyncio.sleep(0.02) # 20ms por palavra
|
| 105 |
+
chunks.append({
|
| 106 |
+
"text": word,
|
| 107 |
+
"is_final": i == len(words) - 1,
|
| 108 |
+
"chunk_index": i
|
| 109 |
+
})
|
| 110 |
+
|
| 111 |
+
return {"chunks": chunks}
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
logger.info("Starting Whisper Mock Service on port 5001")
|
| 115 |
+
uvicorn.run(app, host="0.0.0.0", port=5001, log_level="info")
|
webrtc-latency-test/shared/proto/llm.proto
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package llm;
|
| 4 |
+
|
| 5 |
+
service LLMService {
|
| 6 |
+
// Gera resposta a partir de texto
|
| 7 |
+
rpc Generate(TextRequest) returns (TextResponse) {}
|
| 8 |
+
|
| 9 |
+
// Gera resposta com streaming de tokens
|
| 10 |
+
rpc GenerateStream(TextRequest) returns (stream TokenChunk) {}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
message TextRequest {
|
| 14 |
+
string text = 1;
|
| 15 |
+
string conversation_id = 2;
|
| 16 |
+
float temperature = 3;
|
| 17 |
+
int32 max_tokens = 4;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
message TextResponse {
|
| 21 |
+
string text = 1;
|
| 22 |
+
int64 processing_time_ms = 2;
|
| 23 |
+
int32 tokens_generated = 3;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
message TokenChunk {
|
| 27 |
+
string token = 1;
|
| 28 |
+
int32 token_index = 2;
|
| 29 |
+
bool is_final = 3;
|
| 30 |
+
}
|
webrtc-latency-test/shared/proto/musetalk.proto
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package musetalk;
|
| 4 |
+
|
| 5 |
+
service MuseTalkService {
|
| 6 |
+
// Gera vídeo a partir de áudio
|
| 7 |
+
rpc GenerateVideo(AudioRequest) returns (VideoResponse) {}
|
| 8 |
+
|
| 9 |
+
// Gera vídeo com streaming de frames
|
| 10 |
+
rpc GenerateVideoStream(stream AudioChunk) returns (stream VideoFrame) {}
|
| 11 |
+
|
| 12 |
+
// Obtém animação idle em loop
|
| 13 |
+
rpc GetIdleAnimation(IdleRequest) returns (stream VideoFrame) {}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
message AudioRequest {
|
| 17 |
+
bytes audio_data = 1;
|
| 18 |
+
int32 sample_rate = 2;
|
| 19 |
+
string avatar_id = 3;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
message AudioChunk {
|
| 23 |
+
bytes audio_data = 1;
|
| 24 |
+
int32 chunk_index = 2;
|
| 25 |
+
int32 sample_rate = 3;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
message VideoResponse {
|
| 29 |
+
bytes video_data = 1;
|
| 30 |
+
int32 fps = 2;
|
| 31 |
+
int32 width = 3;
|
| 32 |
+
int32 height = 4;
|
| 33 |
+
int64 processing_time_ms = 5;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
message VideoFrame {
|
| 37 |
+
bytes frame_data = 1;
|
| 38 |
+
int64 timestamp_ms = 2;
|
| 39 |
+
int32 frame_index = 3;
|
| 40 |
+
bool is_final = 4;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
message IdleRequest {
|
| 44 |
+
string avatar_id = 1;
|
| 45 |
+
int32 fps = 2;
|
| 46 |
+
}
|
webrtc-latency-test/shared/proto/tts.proto
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package tts;
|
| 4 |
+
|
| 5 |
+
service TTSService {
|
| 6 |
+
// Sintetiza texto para áudio
|
| 7 |
+
rpc Synthesize(TextRequest) returns (AudioResponse) {}
|
| 8 |
+
|
| 9 |
+
// Sintetiza com streaming de áudio
|
| 10 |
+
rpc SynthesizeStream(TextRequest) returns (stream AudioChunk) {}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
message TextRequest {
|
| 14 |
+
string text = 1;
|
| 15 |
+
string voice_id = 2;
|
| 16 |
+
float speed = 3;
|
| 17 |
+
int32 sample_rate = 4;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
message AudioResponse {
|
| 21 |
+
bytes audio_data = 1;
|
| 22 |
+
int32 sample_rate = 2;
|
| 23 |
+
int64 processing_time_ms = 3;
|
| 24 |
+
float duration_seconds = 4;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
message AudioChunk {
|
| 28 |
+
bytes audio_data = 1;
|
| 29 |
+
int32 chunk_index = 2;
|
| 30 |
+
bool is_final = 3;
|
| 31 |
+
}
|
webrtc-latency-test/shared/proto/whisper.proto
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package whisper;
|
| 4 |
+
|
| 5 |
+
service WhisperService {
|
| 6 |
+
// Transcreve áudio para texto
|
| 7 |
+
rpc Transcribe(AudioRequest) returns (TextResponse) {}
|
| 8 |
+
|
| 9 |
+
// Transcreve com streaming
|
| 10 |
+
rpc TranscribeStream(stream AudioChunk) returns (stream TextChunk) {}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
message AudioRequest {
|
| 14 |
+
bytes audio_data = 1;
|
| 15 |
+
int32 sample_rate = 2;
|
| 16 |
+
string language = 3;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
message AudioChunk {
|
| 20 |
+
bytes audio_data = 1;
|
| 21 |
+
int32 chunk_index = 2;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
message TextResponse {
|
| 25 |
+
string text = 1;
|
| 26 |
+
float confidence = 2;
|
| 27 |
+
int64 processing_time_ms = 3;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
message TextChunk {
|
| 31 |
+
string text = 1;
|
| 32 |
+
bool is_final = 2;
|
| 33 |
+
int32 chunk_index = 3;
|
| 34 |
+
}
|
webrtc-latency-test/start-all.sh
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
echo "🚀 Iniciando todos os microserviços..."
|
| 4 |
+
echo ""
|
| 5 |
+
|
| 6 |
+
# Diretório base
|
| 7 |
+
BASE_DIR="$(pwd)"
|
| 8 |
+
|
| 9 |
+
# Função para verificar se a porta está em uso
|
| 10 |
+
check_port() {
|
| 11 |
+
lsof -ti:$1 > /dev/null 2>&1
|
| 12 |
+
return $?
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
# Função para iniciar serviço
|
| 16 |
+
start_service() {
|
| 17 |
+
local name=$1
|
| 18 |
+
local dir=$2
|
| 19 |
+
local port=$3
|
| 20 |
+
|
| 21 |
+
echo "Starting $name on port $port..."
|
| 22 |
+
|
| 23 |
+
# Verificar se porta está em uso
|
| 24 |
+
if check_port $port; then
|
| 25 |
+
echo " ⚠️ Port $port already in use, skipping $name"
|
| 26 |
+
return
|
| 27 |
+
fi
|
| 28 |
+
|
| 29 |
+
cd "$BASE_DIR/$dir"
|
| 30 |
+
|
| 31 |
+
# Instalar dependências se necessário
|
| 32 |
+
if [ ! -d "venv" ]; then
|
| 33 |
+
python3 -m venv venv
|
| 34 |
+
source venv/bin/activate
|
| 35 |
+
pip install -q -r requirements.txt
|
| 36 |
+
deactivate
|
| 37 |
+
fi
|
| 38 |
+
|
| 39 |
+
# Iniciar serviço
|
| 40 |
+
source venv/bin/activate
|
| 41 |
+
nohup python3 *.py > /tmp/$name.log 2>&1 &
|
| 42 |
+
local PID=$!
|
| 43 |
+
deactivate
|
| 44 |
+
|
| 45 |
+
echo " ✅ $name started (PID: $PID)"
|
| 46 |
+
sleep 1
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
# Iniciar serviços
|
| 50 |
+
echo "1️⃣ Starting Whisper Service..."
|
| 51 |
+
start_service "whisper" "services/whisper" 5001
|
| 52 |
+
|
| 53 |
+
echo "2️⃣ Starting LLM Service..."
|
| 54 |
+
start_service "llm" "services/llm" 5002
|
| 55 |
+
|
| 56 |
+
echo "3️⃣ Starting TTS Service..."
|
| 57 |
+
start_service "tts" "services/tts" 5003
|
| 58 |
+
|
| 59 |
+
echo "4️⃣ Starting MuseTalk Service..."
|
| 60 |
+
start_service "musetalk" "services/musetalk" 5004
|
| 61 |
+
|
| 62 |
+
echo "5️⃣ Starting Gateway..."
|
| 63 |
+
start_service "gateway" "gateway" 8080
|
| 64 |
+
|
| 65 |
+
echo ""
|
| 66 |
+
echo "✅ All services started!"
|
| 67 |
+
echo ""
|
| 68 |
+
echo "📊 Service URLs:"
|
| 69 |
+
echo " Whisper: http://localhost:5001"
|
| 70 |
+
echo " LLM: http://localhost:5002"
|
| 71 |
+
echo " TTS: http://localhost:5003"
|
| 72 |
+
echo " MuseTalk: http://localhost:5004"
|
| 73 |
+
echo " Gateway: http://localhost:8080"
|
| 74 |
+
echo ""
|
| 75 |
+
echo "🧪 Run tests:"
|
| 76 |
+
echo " python3 test_latency.py"
|
| 77 |
+
echo ""
|
| 78 |
+
echo "🛑 Stop all services:"
|
| 79 |
+
echo " ./stop-all.sh"
|
webrtc-latency-test/stop-all.sh
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
echo "🛑 Stopping all microserviços..."
|
| 4 |
+
echo ""
|
| 5 |
+
|
| 6 |
+
# Matar processos nas portas
|
| 7 |
+
for port in 5001 5002 5003 5004 8080; do
|
| 8 |
+
PID=$(lsof -ti:$port)
|
| 9 |
+
if [ ! -z "$PID" ]; then
|
| 10 |
+
echo "Stopping service on port $port (PID: $PID)..."
|
| 11 |
+
kill $PID
|
| 12 |
+
fi
|
| 13 |
+
done
|
| 14 |
+
|
| 15 |
+
echo ""
|
| 16 |
+
echo "✅ All services stopped!"
|
webrtc-latency-test/test_latency.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script de Teste de Latência
|
| 4 |
+
Testa a comunicação entre microserviços e mede latência
|
| 5 |
+
"""
|
| 6 |
+
import requests
|
| 7 |
+
import time
|
| 8 |
+
import json
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
|
| 11 |
+
GATEWAY_URL = "http://localhost:8080"
|
| 12 |
+
|
| 13 |
+
def test_health():
|
| 14 |
+
"""Verifica saúde de todos os serviços"""
|
| 15 |
+
print("\n🏥 Verificando saúde dos serviços...")
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
response = requests.get(f"{GATEWAY_URL}/health", timeout=5)
|
| 19 |
+
health = response.json()
|
| 20 |
+
|
| 21 |
+
print(f"\nStatus do Gateway: {health['status']}")
|
| 22 |
+
print("\nServiços:")
|
| 23 |
+
for service, status in health['services'].items():
|
| 24 |
+
emoji = "✅" if status.get('status') == 'healthy' else "❌"
|
| 25 |
+
print(f" {emoji} {service}: {status.get('status', 'unknown')}")
|
| 26 |
+
|
| 27 |
+
return health['status'] == 'healthy'
|
| 28 |
+
except Exception as e:
|
| 29 |
+
print(f"❌ Erro ao verificar saúde: {e}")
|
| 30 |
+
return False
|
| 31 |
+
|
| 32 |
+
def test_latency(num_tests=5):
|
| 33 |
+
"""Testa latência da pipeline completa"""
|
| 34 |
+
print(f"\n⏱️ Testando latência ({num_tests} requisições)...\n")
|
| 35 |
+
|
| 36 |
+
results = []
|
| 37 |
+
|
| 38 |
+
for i in range(num_tests):
|
| 39 |
+
print(f"Teste {i+1}/{num_tests}...", end=" ")
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
start = time.time()
|
| 43 |
+
response = requests.get(f"{GATEWAY_URL}/metrics", timeout=30)
|
| 44 |
+
total_time = int((time.time() - start) * 1000)
|
| 45 |
+
|
| 46 |
+
metrics = response.json()
|
| 47 |
+
|
| 48 |
+
print(f"✅ {metrics['total_ms']}ms")
|
| 49 |
+
results.append(metrics)
|
| 50 |
+
|
| 51 |
+
time.sleep(0.5) # Pequena pausa entre testes
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"❌ Erro: {e}")
|
| 54 |
+
|
| 55 |
+
if not results:
|
| 56 |
+
print("\n❌ Nenhum teste bem-sucedido!")
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
# Calcular estatísticas
|
| 60 |
+
whisper_avg = sum(r['whisper_ms'] for r in results) / len(results)
|
| 61 |
+
llm_avg = sum(r['llm_ms'] for r in results) / len(results)
|
| 62 |
+
tts_avg = sum(r['tts_ms'] for r in results) / len(results)
|
| 63 |
+
musetalk_avg = sum(r['musetalk_ms'] for r in results) / len(results)
|
| 64 |
+
total_avg = sum(r['total_ms'] for r in results) / len(results)
|
| 65 |
+
|
| 66 |
+
total_min = min(r['total_ms'] for r in results)
|
| 67 |
+
total_max = max(r['total_ms'] for r in results)
|
| 68 |
+
|
| 69 |
+
# Exibir resultados
|
| 70 |
+
print("\n" + "="*60)
|
| 71 |
+
print("📊 RESULTADOS DA ANÁLISE DE LATÊNCIA")
|
| 72 |
+
print("="*60)
|
| 73 |
+
|
| 74 |
+
print(f"\n🎯 Latência Média por Serviço:")
|
| 75 |
+
print(f" Whisper (STT): {whisper_avg:6.1f}ms")
|
| 76 |
+
print(f" LLM: {llm_avg:6.1f}ms")
|
| 77 |
+
print(f" TTS: {tts_avg:6.1f}ms")
|
| 78 |
+
print(f" MuseTalk: {musetalk_avg:6.1f}ms")
|
| 79 |
+
print(f" ─────────────────────────────")
|
| 80 |
+
print(f" TOTAL: {total_avg:6.1f}ms")
|
| 81 |
+
|
| 82 |
+
print(f"\n📈 Estatísticas Totais:")
|
| 83 |
+
print(f" Mínima: {total_min}ms")
|
| 84 |
+
print(f" Máxima: {total_max}ms")
|
| 85 |
+
print(f" Média: {total_avg:.1f}ms")
|
| 86 |
+
print(f" Testes: {len(results)}")
|
| 87 |
+
|
| 88 |
+
# Classificação
|
| 89 |
+
if total_avg < 300:
|
| 90 |
+
classification = "✅ EXCELENTE"
|
| 91 |
+
elif total_avg < 500:
|
| 92 |
+
classification = "✅ BOM"
|
| 93 |
+
elif total_avg < 800:
|
| 94 |
+
classification = "⚠️ ACEITÁVEL"
|
| 95 |
+
else:
|
| 96 |
+
classification = "❌ ALTO"
|
| 97 |
+
|
| 98 |
+
print(f"\n🎯 Classificação: {classification}")
|
| 99 |
+
|
| 100 |
+
# Comparação com alvos
|
| 101 |
+
print(f"\n🎯 Comparação com Alvos:")
|
| 102 |
+
targets = {
|
| 103 |
+
'Whisper': (whisper_avg, 100),
|
| 104 |
+
'LLM': (llm_avg, 300),
|
| 105 |
+
'TTS': (tts_avg, 150),
|
| 106 |
+
'MuseTalk': (musetalk_avg, 150),
|
| 107 |
+
'Total': (total_avg, 500)
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
for name, (actual, target) in targets.items():
|
| 111 |
+
diff = actual - target
|
| 112 |
+
if diff <= 0:
|
| 113 |
+
status = "✅"
|
| 114 |
+
elif diff < 50:
|
| 115 |
+
status = "⚠️ "
|
| 116 |
+
else:
|
| 117 |
+
status = "❌"
|
| 118 |
+
print(f" {status} {name:10s}: {actual:6.1f}ms (alvo: {target}ms)")
|
| 119 |
+
|
| 120 |
+
print("\n" + "="*60)
|
| 121 |
+
|
| 122 |
+
# Salvar resultados
|
| 123 |
+
filename = f"latency_test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 124 |
+
with open(filename, 'w') as f:
|
| 125 |
+
json.dump({
|
| 126 |
+
'timestamp': datetime.now().isoformat(),
|
| 127 |
+
'num_tests': len(results),
|
| 128 |
+
'results': results,
|
| 129 |
+
'averages': {
|
| 130 |
+
'whisper_ms': whisper_avg,
|
| 131 |
+
'llm_ms': llm_avg,
|
| 132 |
+
'tts_ms': tts_avg,
|
| 133 |
+
'musetalk_ms': musetalk_avg,
|
| 134 |
+
'total_ms': total_avg
|
| 135 |
+
},
|
| 136 |
+
'min_ms': total_min,
|
| 137 |
+
'max_ms': total_max
|
| 138 |
+
}, f, indent=2)
|
| 139 |
+
|
| 140 |
+
print(f"\n💾 Resultados salvos em: {filename}")
|
| 141 |
+
|
| 142 |
+
def main():
|
| 143 |
+
print("="*60)
|
| 144 |
+
print("🧪 TESTE DE LATÊNCIA - MICROSERVIÇOS")
|
| 145 |
+
print("="*60)
|
| 146 |
+
|
| 147 |
+
# 1. Verificar saúde
|
| 148 |
+
if not test_health():
|
| 149 |
+
print("\n❌ Alguns serviços não estão saudáveis!")
|
| 150 |
+
print(" Execute: docker-compose up -d")
|
| 151 |
+
print(" Ou: ./start-all.sh")
|
| 152 |
+
return
|
| 153 |
+
|
| 154 |
+
# 2. Testar latência
|
| 155 |
+
test_latency(num_tests=5)
|
| 156 |
+
|
| 157 |
+
print("\n✨ Teste concluído!")
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
main()
|
webrtc-latency-test/tests/test_latency_playwright.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Teste Automatizado de Latência com Playwright
|
| 4 |
+
Testa a latência do servidor WebRTC com múltiplos usuários simultâneos
|
| 5 |
+
"""
|
| 6 |
+
import asyncio
|
| 7 |
+
import time
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from playwright.async_api import async_playwright
|
| 10 |
+
import json
|
| 11 |
+
|
| 12 |
+
SERVER_URL = "http://38.117.87.48:9000"
|
| 13 |
+
|
| 14 |
+
async def test_single_user(user_id):
|
| 15 |
+
"""Testa latência com um único usuário"""
|
| 16 |
+
results = {
|
| 17 |
+
"user_id": user_id,
|
| 18 |
+
"connected": False,
|
| 19 |
+
"connection_time": 0,
|
| 20 |
+
"latency_measurements": [],
|
| 21 |
+
"fps": 0,
|
| 22 |
+
"errors": []
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
async with async_playwright() as p:
|
| 26 |
+
try:
|
| 27 |
+
# Iniciar browser headless
|
| 28 |
+
browser = await p.chromium.launch(headless=True)
|
| 29 |
+
context = await browser.new_context()
|
| 30 |
+
page = await context.new_page()
|
| 31 |
+
|
| 32 |
+
# Navegar para a página
|
| 33 |
+
start_time = time.time()
|
| 34 |
+
await page.goto(SERVER_URL)
|
| 35 |
+
navigation_time = (time.time() - start_time) * 1000
|
| 36 |
+
results["navigation_time"] = navigation_time
|
| 37 |
+
|
| 38 |
+
# Clicar no botão de conectar
|
| 39 |
+
connect_start = time.time()
|
| 40 |
+
await page.click("#connectBtn")
|
| 41 |
+
|
| 42 |
+
# Aguardar conexão
|
| 43 |
+
try:
|
| 44 |
+
await page.wait_for_selector('.status.connected', timeout=10000)
|
| 45 |
+
connection_time = (time.time() - connect_start) * 1000
|
| 46 |
+
results["connection_time"] = connection_time
|
| 47 |
+
results["connected"] = True
|
| 48 |
+
print(f"Usuário {user_id}: Conectado em {connection_time:.2f}ms")
|
| 49 |
+
except Exception as e:
|
| 50 |
+
results["errors"].append(f"Erro ao conectar: {str(e)}")
|
| 51 |
+
await browser.close()
|
| 52 |
+
return results
|
| 53 |
+
|
| 54 |
+
# Aguardar estabilização do vídeo
|
| 55 |
+
await asyncio.sleep(3)
|
| 56 |
+
|
| 57 |
+
# Capturar 10 medições de latência
|
| 58 |
+
for i in range(10):
|
| 59 |
+
try:
|
| 60 |
+
# Capturar timestamp local
|
| 61 |
+
local_time = datetime.now()
|
| 62 |
+
|
| 63 |
+
# Capturar timestamp do vídeo (do elemento de texto)
|
| 64 |
+
timestamp_text = await page.locator("#timestampDisplay").text_content()
|
| 65 |
+
|
| 66 |
+
# Extrair timestamp do vídeo (formato: "Local Time: HH:MM:SS.mmm | Frame: X | FPS: Y")
|
| 67 |
+
if "Local Time:" in timestamp_text:
|
| 68 |
+
parts = timestamp_text.split("Local Time: ")[1].split(" |")[0]
|
| 69 |
+
video_timestamp = datetime.strptime(parts, "%H:%M:%S.%f")
|
| 70 |
+
|
| 71 |
+
# Calcular diferença (se mesmo minuto, caso contrário ignora)
|
| 72 |
+
if local_time.hour == video_timestamp.hour and \
|
| 73 |
+
local_time.minute == video_timestamp.minute:
|
| 74 |
+
diff_ms = (local_time - video_timestamp).total_seconds() * 1000
|
| 75 |
+
results["latency_measurements"].append(diff_ms)
|
| 76 |
+
|
| 77 |
+
await asyncio.sleep(0.5) # Esperar 500ms entre medições
|
| 78 |
+
except Exception as e:
|
| 79 |
+
results["errors"].append(f"Erro na medição {i}: {str(e)}")
|
| 80 |
+
|
| 81 |
+
# Capturar FPS final
|
| 82 |
+
try:
|
| 83 |
+
fps_text = await page.locator("#fps").text_content()
|
| 84 |
+
results["fps"] = float(fps_text) if fps_text else 0
|
| 85 |
+
except:
|
| 86 |
+
pass
|
| 87 |
+
|
| 88 |
+
# Fechar browser
|
| 89 |
+
await browser.close()
|
| 90 |
+
|
| 91 |
+
except Exception as e:
|
| 92 |
+
results["errors"].append(f"Erro geral: {str(e)}")
|
| 93 |
+
|
| 94 |
+
# Calcular estatísticas de latência
|
| 95 |
+
if results["latency_measurements"]:
|
| 96 |
+
results["avg_latency"] = sum(results["latency_measurements"]) / len(results["latency_measurements"])
|
| 97 |
+
results["min_latency"] = min(results["latency_measurements"])
|
| 98 |
+
results["max_latency"] = max(results["latency_measurements"])
|
| 99 |
+
results["num_measurements"] = len(results["latency_measurements"])
|
| 100 |
+
|
| 101 |
+
return results
|
| 102 |
+
|
| 103 |
+
async def test_multiple_users(num_users, concurrent=True):
|
| 104 |
+
"""Testa latência com múltiplos usuários"""
|
| 105 |
+
print(f"\n🧪 Iniciando teste com {num_users} usuário(s)...")
|
| 106 |
+
print(f"Modo: {'Simultâneo' if concurrent else 'Sequencial'}")
|
| 107 |
+
print(f"Servidor: {SERVER_URL}\n")
|
| 108 |
+
|
| 109 |
+
start_time = time.time()
|
| 110 |
+
|
| 111 |
+
if concurrent:
|
| 112 |
+
# Executar todos os testes simultaneamente
|
| 113 |
+
tasks = [test_single_user(i) for i in range(num_users)]
|
| 114 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 115 |
+
else:
|
| 116 |
+
# Executar testes sequencialmente
|
| 117 |
+
results = []
|
| 118 |
+
for i in range(num_users):
|
| 119 |
+
result = await test_single_user(i)
|
| 120 |
+
results.append(result)
|
| 121 |
+
|
| 122 |
+
total_time = time.time() - start_time
|
| 123 |
+
|
| 124 |
+
# Processar resultados
|
| 125 |
+
all_latencies = []
|
| 126 |
+
connection_times = []
|
| 127 |
+
fps_values = []
|
| 128 |
+
successful = 0
|
| 129 |
+
|
| 130 |
+
for result in results:
|
| 131 |
+
if isinstance(result, Exception):
|
| 132 |
+
print(f"❌ Usuário com erro: {result}")
|
| 133 |
+
continue
|
| 134 |
+
|
| 135 |
+
if result["connected"]:
|
| 136 |
+
successful += 1
|
| 137 |
+
if "latency_measurements" in result and result["latency_measurements"]:
|
| 138 |
+
all_latencies.extend(result["latency_measurements"])
|
| 139 |
+
if "connection_time" in result:
|
| 140 |
+
connection_times.append(result["connection_time"])
|
| 141 |
+
if "fps" in result and result["fps"] > 0:
|
| 142 |
+
fps_values.append(result["fps"])
|
| 143 |
+
|
| 144 |
+
latency_str = f"{result['avg_latency']:.2f}ms" if "avg_latency" in result else "N/A"
|
| 145 |
+
print(f"✅ Usuário {result['user_id']}: Latência={latency_str}, FPS={result['fps']:.1f}")
|
| 146 |
+
else:
|
| 147 |
+
print(f"❌ Usuário {result['user_id']}: Falha na conexão")
|
| 148 |
+
if result["errors"]:
|
| 149 |
+
print(f" Erros: {result['errors']}")
|
| 150 |
+
|
| 151 |
+
# Estatísticas globais
|
| 152 |
+
print("\n" + "="*60)
|
| 153 |
+
print("📊 RESULTADOS AGREGADOS")
|
| 154 |
+
print("="*60)
|
| 155 |
+
print(f"Usuários testados: {num_users}")
|
| 156 |
+
print(f"Conexões bem-sucedidas: {successful}/{num_users}")
|
| 157 |
+
print(f"Taxa de sucesso: {(successful/num_users)*100:.1f}%")
|
| 158 |
+
print(f"Tempo total do teste: {total_time:.2f}s")
|
| 159 |
+
|
| 160 |
+
if all_latencies:
|
| 161 |
+
print(f"\n📈 LATÊNCIA")
|
| 162 |
+
print(f" Média: {sum(all_latencies)/len(all_latencies):.2f}ms")
|
| 163 |
+
print(f" Mínima: {min(all_latencies):.2f}ms")
|
| 164 |
+
print(f" Máxima: {max(all_latencies):.2f}ms")
|
| 165 |
+
print(f" Medidas totais: {len(all_latencies)}")
|
| 166 |
+
|
| 167 |
+
if connection_times:
|
| 168 |
+
print(f"\n⏱️ TEMPO DE CONEXÃO")
|
| 169 |
+
print(f" Média: {sum(connection_times)/len(connection_times):.2f}ms")
|
| 170 |
+
print(f" Mínima: {min(connection_times):.2f}ms")
|
| 171 |
+
print(f" Máxima: {max(connection_times):.2f}ms")
|
| 172 |
+
|
| 173 |
+
if fps_values:
|
| 174 |
+
print(f"\n🎬 FPS")
|
| 175 |
+
print(f" Média: {sum(fps_values)/len(fps_values):.1f}")
|
| 176 |
+
print(f" Mínimo: {min(fps_values):.1f}")
|
| 177 |
+
print(f" Máximo: {max(fps_values):.1f}")
|
| 178 |
+
|
| 179 |
+
# Classificação da latência
|
| 180 |
+
if all_latencies:
|
| 181 |
+
avg_latency = sum(all_latencies)/len(all_latencies)
|
| 182 |
+
print(f"\n🎯 CLASSIFICAÇÃO")
|
| 183 |
+
if avg_latency < 100:
|
| 184 |
+
print(f" ✅ EXCELENTE ({avg_latency:.2f}ms) - Quase imperceptível")
|
| 185 |
+
elif avg_latency < 300:
|
| 186 |
+
print(f" ✅ BOM ({avg_latency:.2f}ms) - Ideal para conversação")
|
| 187 |
+
elif avg_latency < 500:
|
| 188 |
+
print(f" ⚠️ ACEITÁVEL ({avg_latency:.2f}ms) - Pequenos delays possíveis")
|
| 189 |
+
else:
|
| 190 |
+
print(f" ❌ RUIM ({avg_latency:.2f}ms) - Latência muito alta")
|
| 191 |
+
|
| 192 |
+
print("="*60)
|
| 193 |
+
|
| 194 |
+
# Salvar resultados em JSON
|
| 195 |
+
output = {
|
| 196 |
+
"test_config": {
|
| 197 |
+
"num_users": num_users,
|
| 198 |
+
"concurrent": concurrent,
|
| 199 |
+
"server_url": SERVER_URL,
|
| 200 |
+
"test_date": datetime.now().isoformat()
|
| 201 |
+
},
|
| 202 |
+
"results": {
|
| 203 |
+
"successful_connections": successful,
|
| 204 |
+
"success_rate": (successful/num_users)*100 if num_users > 0 else 0,
|
| 205 |
+
"total_test_time": total_time,
|
| 206 |
+
"latency": {
|
| 207 |
+
"all_measurements": all_latencies,
|
| 208 |
+
"avg": sum(all_latencies)/len(all_latencies) if all_latencies else None,
|
| 209 |
+
"min": min(all_latencies) if all_latencies else None,
|
| 210 |
+
"max": max(all_latencies) if all_latencies else None,
|
| 211 |
+
"num_measurements": len(all_latencies)
|
| 212 |
+
},
|
| 213 |
+
"connection_time": {
|
| 214 |
+
"all": connection_times,
|
| 215 |
+
"avg": sum(connection_times)/len(connection_times) if connection_times else None,
|
| 216 |
+
"min": min(connection_times) if connection_times else None,
|
| 217 |
+
"max": max(connection_times) if connection_times else None
|
| 218 |
+
},
|
| 219 |
+
"fps": {
|
| 220 |
+
"all": fps_values,
|
| 221 |
+
"avg": sum(fps_values)/len(fps_values) if fps_values else None,
|
| 222 |
+
"min": min(fps_values) if fps_values else None,
|
| 223 |
+
"max": max(fps_values) if fps_values else None
|
| 224 |
+
}
|
| 225 |
+
},
|
| 226 |
+
"user_results": results
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
# Salvar em arquivo
|
| 230 |
+
filename = f"/root/livekit-poc/latency_test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 231 |
+
with open(filename, 'w') as f:
|
| 232 |
+
json.dump(output, f, indent=2)
|
| 233 |
+
print(f"\n💾 Resultados salvos em: {filename}")
|
| 234 |
+
|
| 235 |
+
return output
|
| 236 |
+
|
| 237 |
+
async def main():
|
| 238 |
+
"""Função principal"""
|
| 239 |
+
import sys
|
| 240 |
+
|
| 241 |
+
# Parâmetros padrão
|
| 242 |
+
num_users = 1
|
| 243 |
+
concurrent = True
|
| 244 |
+
|
| 245 |
+
# Ler argumentos da linha de comando
|
| 246 |
+
if len(sys.argv) > 1:
|
| 247 |
+
num_users = int(sys.argv[1])
|
| 248 |
+
if len(sys.argv) > 2:
|
| 249 |
+
concurrent = sys.argv[2].lower() == 'true'
|
| 250 |
+
|
| 251 |
+
print("🎬 Teste Automatizado de Latência WebRTC")
|
| 252 |
+
print("="*60)
|
| 253 |
+
|
| 254 |
+
await test_multiple_users(num_users, concurrent)
|
| 255 |
+
|
| 256 |
+
print("\n✅ Teste concluído!")
|
| 257 |
+
|
| 258 |
+
if __name__ == "__main__":
|
| 259 |
+
asyncio.run(main())
|