Commit ·
64b0a86
1
Parent(s): 0e82ee5
Fix audio pop/click by skipping WAV header
Browse files- Detect and skip 44-byte WAV header from Orpheus TTS audio
- The header was being interpreted as PCM data causing noise
- Add fade-in (50ms quadratic) and fade-out (30ms) for smoother audio
- Document fix in CLAUDE.md for future reference
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- interface/CLAUDE.md +228 -323
- interface/index_streaming.html +25 -1
interface/CLAUDE.md
CHANGED
|
@@ -1,384 +1,243 @@
|
|
| 1 |
-
# Avatar Interface -
|
| 2 |
|
| 3 |
-
##
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
│ Frontend │◄────►│ Interface Server │◄────►│ Orpheus TTS │
|
| 8 |
-
│ (Browser) │ WS │ (porta 8080) │ WS │ (porta 8081) │
|
| 9 |
-
└─────────────┘ │ │ │ chunks audio │
|
| 10 |
-
│ 1. Recebe texto do frontend │ └─────────────────┘
|
| 11 |
-
│ 2. Conecta Orpheus + Wav2Lip │
|
| 12 |
-
│ EM PARALELO │ ┌─────────────────┐
|
| 13 |
-
│ 3. Recebe chunks conforme │◄────►│ Wav2Lip │
|
| 14 |
-
│ chegam de ambos │ WS │ (porta 8082) │
|
| 15 |
-
│ 4. Monta: audio Orpheus + │ │ frames JPEG │
|
| 16 |
-
│ frames Wav2Lip │ │ (NAO MODIFICAR)│
|
| 17 |
-
│ 5. Envia chunk IMEDIATAMENTE │ └─────────────────┘
|
| 18 |
-
└─────────────────────────────────┘
|
| 19 |
-
```
|
| 20 |
-
|
| 21 |
-
## FLUXO DE STREAMING
|
| 22 |
-
|
| 23 |
-
```
|
| 24 |
-
1. Frontend envia: {"action": "generate", "text": "Hello", "voice": "tara"}
|
| 25 |
-
│
|
| 26 |
-
▼
|
| 27 |
-
2. Interface conecta EM PARALELO:
|
| 28 |
-
├── Orpheus WS (8081) → recebe chunks de audio PCM 24kHz
|
| 29 |
-
└── Wav2Lip WS (8082) → recebe frames JPEG (lip sync com eSpeak interno)
|
| 30 |
-
│
|
| 31 |
-
▼
|
| 32 |
-
3. Conforme dados chegam, acumula em buffers:
|
| 33 |
-
- audio_buffer: chunks PCM do Orpheus
|
| 34 |
-
- frame_buffer: frames JPEG do Wav2Lip
|
| 35 |
-
│
|
| 36 |
-
▼
|
| 37 |
-
4. Quando tem 1 frame + audio correspondente (~1920 bytes = 40ms):
|
| 38 |
-
- Monta chunk binario: [audio_orpheus + frame_wav2lip]
|
| 39 |
-
- Envia IMEDIATAMENTE para frontend
|
| 40 |
-
│
|
| 41 |
-
▼
|
| 42 |
-
5. Frontend reproduz em tempo real (nao espera tudo chegar)
|
| 43 |
-
```
|
| 44 |
-
|
| 45 |
-
## REGRAS CRITICAS
|
| 46 |
-
|
| 47 |
-
1. **NAO MODIFICAR O WAV2LIP** - Ele ja gera lip sync com eSpeak interno. So usar os frames JPEG.
|
| 48 |
-
|
| 49 |
-
2. **AUDIO VEM DO ORPHEUS** - O audio robotico do Wav2Lip e DESCARTADO. Audio final = Orpheus.
|
| 50 |
-
|
| 51 |
-
3. **CONEXOES EM PARALELO** - Orpheus e Wav2Lip devem ser chamados ao mesmo tempo.
|
| 52 |
-
|
| 53 |
-
4. **STREAMING IMEDIATO** - Montar e enviar chunks conforme dados chegam, nao esperar tudo.
|
| 54 |
-
|
| 55 |
-
5. **SINCRONIZACAO** - Calcular audio_por_frame = total_audio / total_frames para alinhar.
|
| 56 |
-
|
| 57 |
-
---
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
1. **NAO ALTERAR ARQUIVOS FORA DE `/workspace/interface`** - Este projeto esta isolado no diretorio `/workspace/interface`. Nao modifique, crie ou delete arquivos em outros diretorios do sistema.
|
| 62 |
-
|
| 63 |
-
2. **Sempre ler CLAUDE.md antes de fazer alteracoes** - Este arquivo contem a arquitetura e regras do projeto. Consultar antes de modificar qualquer codigo.
|
| 64 |
-
|
| 65 |
-
3. **Manter as portas fixas** - TTS=8081, Wav2Lip=8082, Interface=8080. Nunca alterar essas portas sem confirmacao explicita do usuario.
|
| 66 |
-
|
| 67 |
-
4. **Nao refatorar sem pedir** - Focar apenas no que foi solicitado. Nao reorganizar codigo, renomear variaveis ou "melhorar" coisas que nao foram pedidas.
|
| 68 |
-
|
| 69 |
-
5. **Testar apos cada mudanca** - Apos modificar codigo, verificar se o servidor ainda inicia e responde no `/health`.
|
| 70 |
-
|
| 71 |
-
6. **Manter estrutura de arquivos simples** - Apenas `server.py`, `index.html`, `CLAUDE.md`. Nao criar novos arquivos sem aprovacao.
|
| 72 |
-
|
| 73 |
-
7. **Commits pequenos e descritivos** - Se for fazer commit, fazer um por funcionalidade, nao acumular varias mudancas.
|
| 74 |
|
| 75 |
---
|
| 76 |
|
| 77 |
-
## Arquitetura
|
| 78 |
-
|
| 79 |
-
```
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
│
|
| 83 |
-
│
|
| 84 |
-
│
|
| 85 |
-
│
|
| 86 |
-
│
|
| 87 |
-
│
|
| 88 |
-
└─────────────────
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
```
|
| 94 |
|
| 95 |
-
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
│
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
│
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
│
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
│
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
```
|
| 131 |
|
| 132 |
-
|
| 133 |
|
| 134 |
-
##
|
| 135 |
|
| 136 |
-
|
|
|
|
| 137 |
|
| 138 |
-
**
|
| 139 |
```json
|
| 140 |
{
|
| 141 |
-
"
|
| 142 |
-
"
|
| 143 |
-
"voice": "tara",
|
| 144 |
-
"stream": true
|
| 145 |
}
|
| 146 |
```
|
| 147 |
|
| 148 |
-
**
|
| 149 |
```json
|
| 150 |
{
|
| 151 |
-
"
|
| 152 |
-
"
|
| 153 |
-
"
|
| 154 |
-
"bytes": 4800,
|
| 155 |
-
"sample_rate": 24000,
|
| 156 |
-
"channels": 1,
|
| 157 |
-
"bits_per_sample": 16
|
| 158 |
}
|
| 159 |
```
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
"type": "done",
|
| 164 |
-
"total_chunks": 5,
|
| 165 |
-
"total_bytes": 24000
|
| 166 |
-
}
|
| 167 |
-
```
|
| 168 |
-
|
| 169 |
-
**Formato do audio:** PCM 24kHz, 16-bit signed little-endian, mono
|
| 170 |
-
|
| 171 |
-
**Vozes:** tara, leo, leah, jess, dan, mia, zac, zoe
|
| 172 |
-
|
| 173 |
-
---
|
| 174 |
-
|
| 175 |
-
### Wav2Lip Server (porta 8082)
|
| 176 |
|
| 177 |
-
**
|
| 178 |
-
|
| 179 |
-
**IMPORTANTE:** O Wav2Lip usa audio interno (eSpeak) para gerar lip sync.
|
| 180 |
-
Este audio robotico DEVE SER DESCARTADO - usar APENAS os frames JPEG!
|
| 181 |
-
|
| 182 |
-
**Enviar audio (chunk por chunk):**
|
| 183 |
```json
|
| 184 |
{
|
| 185 |
-
"
|
| 186 |
-
"
|
| 187 |
-
"
|
| 188 |
}
|
| 189 |
```
|
| 190 |
|
| 191 |
-
**
|
| 192 |
-
```json
|
| 193 |
-
{"type": "frame", "frame": "<base64 JPEG>", "index": 0}
|
| 194 |
-
{"type": "frame", "frame": "<base64 JPEG>", "index": 1}
|
| 195 |
-
...
|
| 196 |
-
```
|
| 197 |
-
|
| 198 |
-
**Finalizar:**
|
| 199 |
-
```json
|
| 200 |
-
{"action": "end"}
|
| 201 |
-
```
|
| 202 |
-
|
| 203 |
-
**Resposta final:**
|
| 204 |
```json
|
| 205 |
{
|
| 206 |
-
"
|
| 207 |
-
"frames": 25,
|
| 208 |
-
"audio_duration_ms": 1000,
|
| 209 |
-
"total_time_ms": 500
|
| 210 |
}
|
| 211 |
```
|
| 212 |
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
---
|
| 216 |
-
|
| 217 |
-
### Interface Server (porta 8080) - Frontend
|
| 218 |
-
|
| 219 |
-
**Requisicao do frontend:**
|
| 220 |
-
```json
|
| 221 |
-
{"action": "generate", "text": "Hello world", "voice": "tara"}
|
| 222 |
-
{"action": "stop"}
|
| 223 |
-
{"action": "ping"}
|
| 224 |
-
```
|
| 225 |
-
|
| 226 |
-
**Respostas para o frontend (STREAMING PROGRESSIVO):**
|
| 227 |
-
|
| 228 |
-
```json
|
| 229 |
-
{"type": "status", "message": "Conectando aos servicos..."}
|
| 230 |
-
```
|
| 231 |
-
|
| 232 |
-
```json
|
| 233 |
-
{"type": "stream_start", "ttfb_ms": 150}
|
| 234 |
-
```
|
| 235 |
-
|
| 236 |
-
```json
|
| 237 |
-
{
|
| 238 |
-
"type": "chunk",
|
| 239 |
-
"chunk_index": 1,
|
| 240 |
-
"audio_size": 4800,
|
| 241 |
-
"audio_duration_ms": 100,
|
| 242 |
-
"num_frames": 2,
|
| 243 |
-
"data": "<base64 do chunk montado>"
|
| 244 |
-
}
|
| 245 |
-
```
|
| 246 |
|
|
|
|
| 247 |
```json
|
| 248 |
{
|
| 249 |
-
"
|
| 250 |
-
"
|
| 251 |
-
"
|
| 252 |
-
"total_chunks": 50,
|
| 253 |
-
"elapsed_ms": 3500
|
| 254 |
}
|
| 255 |
```
|
| 256 |
|
| 257 |
-
|
| 258 |
-
{"type": "error", "message": "Descricao do erro"}
|
| 259 |
-
```
|
| 260 |
-
|
| 261 |
-
### Formato do Chunk Montado (binario em base64)
|
| 262 |
|
| 263 |
-
|
| 264 |
-
[audio_size: 4 bytes big-endian]
|
| 265 |
-
[audio_data: PCM 24kHz do ORPHEUS, 16-bit, mono]
|
| 266 |
-
[num_frames: 4 bytes big-endian]
|
| 267 |
-
[frame_1_size: 4 bytes big-endian]
|
| 268 |
-
[frame_1_data: JPEG bytes do WAV2LIP]
|
| 269 |
-
[frame_2_size: 4 bytes big-endian]
|
| 270 |
-
[frame_2_data: JPEG bytes do WAV2LIP]
|
| 271 |
-
...
|
| 272 |
-
```
|
| 273 |
|
| 274 |
-
##
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
VIDEO_FPS = 25 # Wav2Lip
|
| 280 |
-
BYTES_PER_SAMPLE = 2 # 16-bit
|
| 281 |
-
|
| 282 |
-
# Buffers
|
| 283 |
-
audio_buffer = bytearray() # Audio do Orpheus
|
| 284 |
-
frame_buffer = [] # Frames do Wav2Lip
|
| 285 |
-
|
| 286 |
-
async def process_streaming():
|
| 287 |
-
# Conectar em PARALELO
|
| 288 |
-
orpheus_task = asyncio.create_task(connect_orpheus())
|
| 289 |
-
wav2lip_task = asyncio.create_task(connect_wav2lip())
|
| 290 |
-
|
| 291 |
-
while not done:
|
| 292 |
-
# Verificar se tem dados suficientes para montar chunk
|
| 293 |
-
audio_bytes_needed = int(CHUNK_DURATION_MS * AUDIO_SAMPLE_RATE * BYTES_PER_SAMPLE / 1000)
|
| 294 |
-
frames_needed = int(CHUNK_DURATION_MS * VIDEO_FPS / 1000)
|
| 295 |
-
|
| 296 |
-
if len(audio_buffer) >= audio_bytes_needed and len(frame_buffer) >= frames_needed:
|
| 297 |
-
# Extrair dados dos buffers
|
| 298 |
-
audio_chunk = audio_buffer[:audio_bytes_needed]
|
| 299 |
-
del audio_buffer[:audio_bytes_needed]
|
| 300 |
-
|
| 301 |
-
frames_chunk = frame_buffer[:frames_needed]
|
| 302 |
-
del frame_buffer[:frames_needed]
|
| 303 |
-
|
| 304 |
-
# Montar e enviar chunk
|
| 305 |
-
chunk_data = build_chunk(audio_chunk, frames_chunk)
|
| 306 |
-
await ws.send_json({
|
| 307 |
-
"type": "chunk",
|
| 308 |
-
"chunk_index": chunk_index,
|
| 309 |
-
"audio_duration_ms": CHUNK_DURATION_MS,
|
| 310 |
-
"num_frames": len(frames_chunk),
|
| 311 |
-
"data": base64.b64encode(chunk_data).decode()
|
| 312 |
-
})
|
| 313 |
-
chunk_index += 1
|
| 314 |
-
```
|
| 315 |
|
| 316 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
-
|
| 319 |
|
| 320 |
-
|
| 321 |
-
TTS Orpheus: 24000 Hz (24000 samples/segundo)
|
| 322 |
-
Wav2Lip: 16000 Hz (espera 16000 samples/segundo para lip sync)
|
| 323 |
-
Video: 25 fps (1 frame a cada 40ms)
|
| 324 |
|
| 325 |
-
Para chunk de 100ms:
|
| 326 |
-
- Audio Orpheus: 100 * 24000 / 1000 * 2 = 4800 bytes
|
| 327 |
-
- Frames Wav2Lip: 100 / 40 = 2.5 ≈ 2-3 frames
|
| 328 |
```
|
| 329 |
-
|
| 330 |
-
#
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
| 332 |
```
|
| 333 |
-
Chunk de 100ms:
|
| 334 |
-
- Audio: 4800 bytes PCM 24kHz do Orpheus (voz de alta qualidade)
|
| 335 |
-
- Frames: 2-3 JPEGs do Wav2Lip (lip sync)
|
| 336 |
|
| 337 |
-
|
| 338 |
-
```
|
| 339 |
|
| 340 |
-
##
|
| 341 |
|
|
|
|
| 342 |
```
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
└── static/ # Arquivos estaticos
|
| 349 |
```
|
| 350 |
|
| 351 |
-
##
|
| 352 |
-
|
| 353 |
```bash
|
| 354 |
-
|
| 355 |
-
TTS_WS=ws://localhost:8081/ws
|
| 356 |
-
WAV2LIP_WS=ws://localhost:8082/ws
|
| 357 |
-
PORT=8080
|
| 358 |
-
CHUNK_DURATION_MS=100
|
| 359 |
```
|
| 360 |
|
|
|
|
|
|
|
| 361 |
## Como Executar
|
| 362 |
|
| 363 |
```bash
|
| 364 |
cd /workspace/interface
|
|
|
|
| 365 |
python3 server.py
|
| 366 |
```
|
| 367 |
|
| 368 |
**Output esperado:**
|
| 369 |
```
|
| 370 |
==================================================
|
| 371 |
-
Interface Server -
|
| 372 |
==================================================
|
| 373 |
Porta: 8080
|
| 374 |
-
|
| 375 |
-
Wav2Lip: ws://localhost:8082/ws
|
| 376 |
-
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
==================================================
|
| 379 |
```
|
| 380 |
|
| 381 |
-
|
|
|
|
|
|
|
| 382 |
|
| 383 |
| Voice | Genero |
|
| 384 |
|-------|-----------|
|
|
@@ -388,26 +247,72 @@ Video: 25fps (40ms/frame)
|
|
| 388 |
| leo | Masculino |
|
| 389 |
| dan | Masculino |
|
| 390 |
|
| 391 |
-
|
| 392 |
|
| 393 |
-
|
| 394 |
-
|------------------|-------|--------------|------------|
|
| 395 |
-
| Interface Server | 8080 | Monta chunks | - |
|
| 396 |
-
| TTS (Orpheus) | 8081 | PCM 24kHz | - |
|
| 397 |
-
| Wav2Lip | 8082 | DESCARTAR! | JPEG 25fps |
|
| 398 |
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
-
##
|
| 406 |
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
-
|
| 412 |
|
| 413 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Avatar Interface - WebRTC Streaming com VP9
|
| 2 |
|
| 3 |
+
## Visao Geral
|
| 4 |
|
| 5 |
+
Sistema de avatar em tempo real usando WebRTC para streaming de video com baixa latencia.
|
| 6 |
+
O backend faz toda a fusao de video (idle + lip-sync) e envia um stream unificado para o frontend.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
**Framework WebRTC:** [aiortc](https://github.com/aiortc/aiortc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
---
|
| 11 |
|
| 12 |
+
## Arquitetura
|
| 13 |
+
|
| 14 |
+
```
|
| 15 |
+
FRONTEND (Browser)
|
| 16 |
+
┌─────────────────────────────────────┐
|
| 17 |
+
│ │
|
| 18 |
+
│ <video autoplay> │
|
| 19 |
+
│ │
|
| 20 |
+
│ Apenas renderiza o stream │
|
| 21 |
+
│ WebRTC (VP9 + Opus) │
|
| 22 |
+
│ │
|
| 23 |
+
└──────────────────▲──────────────────┘
|
| 24 |
+
│
|
| 25 |
+
│ WebRTC
|
| 26 |
+
│ (VP9 video + Opus audio)
|
| 27 |
+
│
|
| 28 |
+
═══════════════════════════════════════╧════════════════════════════════════
|
| 29 |
+
|
| 30 |
+
BACKEND (Python + aiortc)
|
| 31 |
+
┌───────────────────────────────────────────────────────────────────────────┐
|
| 32 |
+
│ │
|
| 33 |
+
│ INTERFACE SERVER (8080) │
|
| 34 |
+
│ │
|
| 35 |
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │
|
| 36 |
+
│ │ idle.mp4 │────►│ MIXER │────►│ WebRTC Tracks │ │
|
| 37 |
+
│ │ (frames) │ │ │ │ │ │
|
| 38 |
+
│ └─────────────┘ │ Alterna │ │ AvatarVideoTrack (VP9) │──┼──► WebRTC
|
| 39 |
+
│ │ idle/speak │ │ AvatarAudioTrack (Opus) │ │
|
| 40 |
+
│ ┌─────────────┐ │ │ │ │ │
|
| 41 |
+
│ │ Wav2Lip │────►│ │ │ 25fps, baixa latencia │ │
|
| 42 |
+
│ │ (frames) │ └─────────────┘ └─────────────────────────────┘ │
|
| 43 |
+
│ └─────────────┘ │
|
| 44 |
+
│ │ │
|
| 45 |
+
│ ┌─────▼───────┐ │
|
| 46 |
+
│ │ Audio │ │
|
| 47 |
+
│ │ Orpheus │ │
|
| 48 |
+
│ └─────────────┘ │
|
| 49 |
+
│ │
|
| 50 |
+
└───────────────────────────────────────────────────────────────────────────┘
|
| 51 |
+
│
|
| 52 |
+
│ WebSocket
|
| 53 |
+
▼
|
| 54 |
+
┌─────────────────────────────────────┐
|
| 55 |
+
│ WAV2LIP SERVER (8082) │
|
| 56 |
+
│ │
|
| 57 |
+
│ Gera frames de lip-sync │
|
| 58 |
+
│ Chama Orpheus TTS internamente │
|
| 59 |
+
│ │
|
| 60 |
+
└─────────────────────────────────────┘
|
| 61 |
```
|
| 62 |
|
| 63 |
+
---
|
| 64 |
|
| 65 |
+
## Fluxo de Funcionamento
|
| 66 |
+
|
| 67 |
+
### 1. Conexao WebRTC
|
| 68 |
+
|
| 69 |
+
```
|
| 70 |
+
Cliente Servidor
|
| 71 |
+
│ │
|
| 72 |
+
│ POST /offer (SDP offer) │
|
| 73 |
+
│ ─────────────────────────────► │
|
| 74 |
+
│ │ Cria RTCPeerConnection
|
| 75 |
+
│ │ Cria VideoTrack + AudioTrack
|
| 76 |
+
│ │
|
| 77 |
+
│ SDP answer + session_id │
|
| 78 |
+
│ ◄───────────────────────────── │
|
| 79 |
+
│ │
|
| 80 |
+
│ WebRTC conectado │
|
| 81 |
+
│ ◄════════════════════════════► │ Stream de video comeca
|
| 82 |
+
│ │ (idle frames em loop)
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### 2. Geracao de Fala
|
| 86 |
+
|
| 87 |
+
```
|
| 88 |
+
Cliente Servidor Wav2Lip
|
| 89 |
+
│ │ │
|
| 90 |
+
│ POST /generate │ │
|
| 91 |
+
│ {text, voice, session_id} │ │
|
| 92 |
+
│ ─────────────────────────────► │ │
|
| 93 |
+
│ │ │
|
| 94 |
+
│ │ WS: generate │
|
| 95 |
+
│ │ ────────────────────────► │
|
| 96 |
+
│ │ │
|
| 97 |
+
│ │ frames + audio │
|
| 98 |
+
│ │ ◄──────────────────────── │
|
| 99 |
+
│ │ │
|
| 100 |
+
│ Stream muda para lip-sync │ │
|
| 101 |
+
│ ◄════════════════════════════► │ │
|
| 102 |
+
│ (video + audio sincronizado) │ │
|
| 103 |
+
│ │ │
|
| 104 |
+
│ Volta ao idle automaticamente │ │
|
| 105 |
+
│ ◄════════════════════════════► │ │
|
| 106 |
```
|
| 107 |
|
| 108 |
+
---
|
| 109 |
|
| 110 |
+
## Endpoints da API
|
| 111 |
|
| 112 |
+
### POST /offer
|
| 113 |
+
Inicia conexao WebRTC (signaling).
|
| 114 |
|
| 115 |
+
**Request:**
|
| 116 |
```json
|
| 117 |
{
|
| 118 |
+
"sdp": "v=0\r\no=- ...",
|
| 119 |
+
"type": "offer"
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
```
|
| 122 |
|
| 123 |
+
**Response:**
|
| 124 |
```json
|
| 125 |
{
|
| 126 |
+
"sdp": "v=0\r\no=- ...",
|
| 127 |
+
"type": "answer",
|
| 128 |
+
"session_id": "uuid-da-sessao"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
```
|
| 131 |
|
| 132 |
+
### POST /generate
|
| 133 |
+
Gera fala com lip-sync.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
**Request:**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
```json
|
| 137 |
{
|
| 138 |
+
"session_id": "uuid-da-sessao",
|
| 139 |
+
"text": "Hello, I am an avatar!",
|
| 140 |
+
"voice": "tara"
|
| 141 |
}
|
| 142 |
```
|
| 143 |
|
| 144 |
+
**Response:**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
```json
|
| 146 |
{
|
| 147 |
+
"status": "generating"
|
|
|
|
|
|
|
|
|
|
| 148 |
}
|
| 149 |
```
|
| 150 |
|
| 151 |
+
### GET /health
|
| 152 |
+
Status do servidor.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
+
**Response:**
|
| 155 |
```json
|
| 156 |
{
|
| 157 |
+
"status": "ok",
|
| 158 |
+
"mode": "webrtc",
|
| 159 |
+
"connections": 2
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
```
|
| 162 |
|
| 163 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
+
## Configuracao de Codec
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
+
### Video (VP9)
|
| 168 |
+
- **Codec:** libvpx-vp9
|
| 169 |
+
- **FPS:** 25
|
| 170 |
+
- **Latencia:** ~50-100ms
|
| 171 |
+
- **Qualidade:** Alta (compressao temporal)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
+
### Audio (Opus)
|
| 174 |
+
- **Codec:** Opus
|
| 175 |
+
- **Sample Rate:** 24000 Hz (resampled para 48000 pelo WebRTC)
|
| 176 |
+
- **Canais:** Mono
|
| 177 |
+
- **Modo:** Low delay
|
| 178 |
|
| 179 |
+
---
|
| 180 |
|
| 181 |
+
## Estrutura de Arquivos
|
|
|
|
|
|
|
|
|
|
| 182 |
|
|
|
|
|
|
|
|
|
|
| 183 |
```
|
| 184 |
+
/workspace/interface/
|
| 185 |
+
├── CLAUDE.md # Esta documentacao
|
| 186 |
+
├── server.py # Servidor WebRTC com aiortc
|
| 187 |
+
├── index.html # Frontend WebRTC
|
| 188 |
+
├── idle.mp4 # Video de idle loop
|
| 189 |
+
└── requirements.txt # Dependencias Python
|
| 190 |
```
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
+
---
|
|
|
|
| 193 |
|
| 194 |
+
## Dependencias
|
| 195 |
|
| 196 |
+
### Python
|
| 197 |
```
|
| 198 |
+
aiohttp>=3.9.0
|
| 199 |
+
aiortc>=1.6.0
|
| 200 |
+
opencv-python>=4.8.0
|
| 201 |
+
numpy>=1.24.0
|
| 202 |
+
av>=10.0.0
|
|
|
|
| 203 |
```
|
| 204 |
|
| 205 |
+
### Sistema (Ubuntu)
|
|
|
|
| 206 |
```bash
|
| 207 |
+
apt install -y libavdevice-dev libavfilter-dev libopus-dev libvpx-dev libsrtp2-dev
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
```
|
| 209 |
|
| 210 |
+
---
|
| 211 |
+
|
| 212 |
## Como Executar
|
| 213 |
|
| 214 |
```bash
|
| 215 |
cd /workspace/interface
|
| 216 |
+
pip install -r requirements.txt
|
| 217 |
python3 server.py
|
| 218 |
```
|
| 219 |
|
| 220 |
**Output esperado:**
|
| 221 |
```
|
| 222 |
==================================================
|
| 223 |
+
Interface Server - WebRTC VP9 Streaming
|
| 224 |
==================================================
|
| 225 |
Porta: 8080
|
| 226 |
+
Idle Video: /workspace/interface/idle.mp4
|
| 227 |
+
Wav2Lip: ws://localhost:8082/ws
|
| 228 |
+
==================================================
|
| 229 |
+
Endpoints:
|
| 230 |
+
POST /offer - WebRTC signaling
|
| 231 |
+
POST /generate - Gerar fala
|
| 232 |
+
==================================================
|
| 233 |
+
Carregando idle frames...
|
| 234 |
+
[Idle] Carregados 1368 frames
|
| 235 |
==================================================
|
| 236 |
```
|
| 237 |
|
| 238 |
+
---
|
| 239 |
+
|
| 240 |
+
## Vozes Disponiveis (Orpheus TTS)
|
| 241 |
|
| 242 |
| Voice | Genero |
|
| 243 |
|-------|-----------|
|
|
|
|
| 247 |
| leo | Masculino |
|
| 248 |
| dan | Masculino |
|
| 249 |
|
| 250 |
+
---
|
| 251 |
|
| 252 |
+
## Portas
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
+
| Servico | Porta |
|
| 255 |
+
|------------------|-------|
|
| 256 |
+
| Interface Server | 8080 |
|
| 257 |
+
| Orpheus TTS | 8081 |
|
| 258 |
+
| Wav2Lip | 8082 |
|
| 259 |
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
## Vantagens do WebRTC sobre WebSocket+JPEG
|
| 263 |
+
|
| 264 |
+
| Aspecto | WebSocket+JPEG | WebRTC+VP9 |
|
| 265 |
+
|-----------------|-------------------|---------------------|
|
| 266 |
+
| Bandwidth | ~1.25 MB/s | ~200 KB/s (6x menos)|
|
| 267 |
+
| Latencia | ~50ms | ~50-100ms |
|
| 268 |
+
| CPU Browser | Alta (JS decode) | Baixa (GPU decode) |
|
| 269 |
+
| Audio/Video | Separados | Sincronizados |
|
| 270 |
+
| Qualidade | Boa | Excelente |
|
| 271 |
+
|
| 272 |
+
---
|
| 273 |
|
| 274 |
+
## Frontend Simplificado
|
| 275 |
|
| 276 |
+
O frontend apenas:
|
| 277 |
+
1. Envia offer SDP
|
| 278 |
+
2. Recebe answer SDP
|
| 279 |
+
3. Renderiza `<video>`
|
| 280 |
+
4. Envia texto para /generate
|
| 281 |
|
| 282 |
+
Toda a logica de fusao, encoding e timing esta no backend.
|
| 283 |
+
|
| 284 |
+
---
|
| 285 |
+
|
| 286 |
+
## Fixes Importantes
|
| 287 |
+
|
| 288 |
+
### Audio Pop/Click no Inicio (WAV Header)
|
| 289 |
+
|
| 290 |
+
**Problema:** O audio do Orpheus TTS vem com um header WAV de 44 bytes. Quando o frontend interpreta esses bytes como dados PCM, causa um ruido/estalo no inicio da reproducao.
|
| 291 |
+
|
| 292 |
+
**Solucao:** Detectar o header WAV (bytes `RIFF`) e pular os primeiros 44 bytes antes de processar o PCM:
|
| 293 |
+
|
| 294 |
+
```javascript
|
| 295 |
+
// Verificar se tem header WAV (RIFF) e pular se existir
|
| 296 |
+
let pcmOffset = 0;
|
| 297 |
+
if (bytes.length > 44 &&
|
| 298 |
+
bytes[0] === 0x52 && bytes[1] === 0x49 &&
|
| 299 |
+
bytes[2] === 0x46 && bytes[3] === 0x46) { // "RIFF"
|
| 300 |
+
console.log('WAV header detected, skipping 44 bytes');
|
| 301 |
+
pcmOffset = 44;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
const pcmData = new Int16Array(bytes.buffer, pcmOffset);
|
| 305 |
+
```
|
| 306 |
+
|
| 307 |
+
**Adicional:** Aplicar fade-in/fade-out suave para evitar qualquer descontinuidade restante:
|
| 308 |
+
- Fade-in: 50ms com curva quadratica
|
| 309 |
+
- Fade-out: 30ms linear
|
| 310 |
+
|
| 311 |
+
---
|
| 312 |
|
| 313 |
+
## Regras Importantes
|
| 314 |
|
| 315 |
+
1. **NAO ALTERAR ARQUIVOS FORA DE `/workspace/interface`**
|
| 316 |
+
2. **Backend faz toda a fusao** - Frontend so renderiza
|
| 317 |
+
3. **Manter portas fixas** - 8080, 8081, 8082
|
| 318 |
+
4. **Testar apos cada mudanca** - Verificar /health
|
interface/index_streaming.html
CHANGED
|
@@ -235,13 +235,37 @@ async function startSyncedPlayback(base64Audio, durationMs) {
|
|
| 235 |
bytes[i] = binaryString.charCodeAt(i);
|
| 236 |
}
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
// PCM 16-bit mono 24kHz -> AudioBuffer
|
| 239 |
-
const pcmData = new Int16Array(bytes.buffer);
|
| 240 |
const floatData = new Float32Array(pcmData.length);
|
| 241 |
for (let i = 0; i < pcmData.length; i++) {
|
| 242 |
floatData[i] = pcmData[i] / 32768.0;
|
| 243 |
}
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
audioBuffer = audioContext.createBuffer(1, floatData.length, 24000);
|
| 246 |
audioBuffer.getChannelData(0).set(floatData);
|
| 247 |
|
|
|
|
| 235 |
bytes[i] = binaryString.charCodeAt(i);
|
| 236 |
}
|
| 237 |
|
| 238 |
+
// Verificar se tem header WAV (RIFF) e pular se existir
|
| 239 |
+
let pcmOffset = 0;
|
| 240 |
+
if (bytes.length > 44 &&
|
| 241 |
+
bytes[0] === 0x52 && bytes[1] === 0x49 &&
|
| 242 |
+
bytes[2] === 0x46 && bytes[3] === 0x46) { // "RIFF"
|
| 243 |
+
console.log('WAV header detected, skipping 44 bytes');
|
| 244 |
+
pcmOffset = 44;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
// PCM 16-bit mono 24kHz -> AudioBuffer
|
| 248 |
+
const pcmData = new Int16Array(bytes.buffer, pcmOffset);
|
| 249 |
const floatData = new Float32Array(pcmData.length);
|
| 250 |
for (let i = 0; i < pcmData.length; i++) {
|
| 251 |
floatData[i] = pcmData[i] / 32768.0;
|
| 252 |
}
|
| 253 |
|
| 254 |
+
// Aplicar fade-in suave para evitar estalo no inicio (50ms @ 24kHz = 1200 samples)
|
| 255 |
+
const fadeInSamples = 1200;
|
| 256 |
+
for (let i = 0; i < Math.min(fadeInSamples, floatData.length); i++) {
|
| 257 |
+
// Usar curva exponencial para fade mais suave
|
| 258 |
+
const t = i / fadeInSamples;
|
| 259 |
+
floatData[i] *= t * t; // Curva quadratica (mais suave que linear)
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// Aplicar fade-out suave para evitar estalo no fim (30ms @ 24kHz = 720 samples)
|
| 263 |
+
const fadeOutSamples = 720;
|
| 264 |
+
const fadeOutStart = floatData.length - fadeOutSamples;
|
| 265 |
+
for (let i = 0; i < fadeOutSamples && fadeOutStart + i < floatData.length; i++) {
|
| 266 |
+
floatData[fadeOutStart + i] *= (fadeOutSamples - i) / fadeOutSamples;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
audioBuffer = audioContext.createBuffer(1, floatData.length, 24000);
|
| 270 |
audioBuffer.getChannelData(0).set(floatData);
|
| 271 |
|