marcosremar2 Claude Opus 4.5 commited on
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>

Files changed (2) hide show
  1. interface/CLAUDE.md +228 -323
  2. interface/index_streaming.html +25 -1
interface/CLAUDE.md CHANGED
@@ -1,384 +1,243 @@
1
- # Avatar Interface - Sistema de Streaming de Video com Lip Sync
2
 
3
- ## ARQUITETURA PRINCIPAL
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
- ## REGRAS IMPORTANTES
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 de Streaming Progressivo
78
-
79
- ```
80
- ┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
81
- │ Frontend │◄─────►│ Interface Server │◄─────►│ TTS (Orpheus) │
82
- (Browser) WS │ (porta 8080) │ WS │ (porta 8081) │
83
- │ │ │ │ PCM 24kHz
84
- Renderiza │ MONTA CHUNKS: │ │ (audio final) │
85
- chunks │ │ audio Orpheus + └─────────────────┘
86
- conforme │ │ frames Wav2Lip
87
- chegam │ │ ┌─────────────────┐
88
- └─────────────────┘ │ DESCARTA audio │◄─────►│ Wav2Lip │
89
- robotico! │ WS │ (porta 8082) │
90
- └──────────────────────┘ JPEG 25fps │
91
- (so frames!)
92
- └─────────────────┘
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  ```
94
 
95
- ## Fluxo de Streaming Progressivo
96
 
97
- ```
98
- 1. Frontend envia: {"action": "generate", "text": "Hello", "voice": "tara"}
99
-
100
-
101
- 2. Interface conecta ao TTS (8081) e Wav2Lip (8082) EM PARALELO
102
-
103
- ├──────────────────────────────────────────────────────────┐
104
-
105
- ▼ ▼
106
- 3a. Orpheus TTS gera audio 3b. Wav2Lip gera frames
107
- em chunks (~100ms cada) baseado em audio interno
108
- (eSpeak robotico - DESCARTAR)
109
-
110
- ▼ ▼
111
- 4. Interface Server acumula em buffers:
112
- - audio_buffer: chunks do Orpheus
113
- - frame_buffer: frames JPEG do Wav2Lip
114
-
115
-
116
- 5. Quando tiver dados suficientes (~100ms):
117
- a. Pegar audio do Orpheus (ex: 4800 bytes = 100ms)
118
- b. Pegar frames do Wav2Lip (ex: 2-3 frames = 80-120ms)
119
- c. Montar chunk: [audio_orpheus + frames]
120
- d. Enviar para frontend IMEDIATAMENTE
121
-
122
-
123
- 6. Frontend recebe chunk e renderiza:
124
- - Decodifica audio e adiciona ao buffer de playback
125
- - Decodifica frames JPEG e adiciona a fila de exibicao
126
- - Inicia playback assim que tiver ~200ms de dados
127
-
128
-
129
- 7. Repetir ate receber "done" de ambos os servicos
 
 
 
 
 
 
 
 
130
  ```
131
 
132
- ## Protocolos WebSocket
133
 
134
- ### TTS Server (porta 8081) - Orpheus
135
 
136
- **Conexao:** `ws://localhost:8081/ws`
 
137
 
138
- **Requisicao:**
139
  ```json
140
  {
141
- "action": "synthesize",
142
- "text": "Hello world",
143
- "voice": "tara",
144
- "stream": true
145
  }
146
  ```
147
 
148
- **Respostas (streaming):**
149
  ```json
150
  {
151
- "type": "audio_chunk",
152
- "audio": "<base64 PCM>",
153
- "chunk_index": 1,
154
- "bytes": 4800,
155
- "sample_rate": 24000,
156
- "channels": 1,
157
- "bits_per_sample": 16
158
  }
159
  ```
160
 
161
- ```json
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
- **Arquivo:** `/home/marcosavatar/realtimeWav2lip/websocket_server.py`
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
- "action": "process_audio",
186
- "audio": "<base64 PCM>",
187
- "sample_rate": 16000
188
  }
189
  ```
190
 
191
- **Resposta (frames) - USAR APENAS OS FRAMES:**
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
- "type": "done",
207
- "frames": 25,
208
- "audio_duration_ms": 1000,
209
- "total_time_ms": 500
210
  }
211
  ```
212
 
213
- **IMPORTANTE:** Wav2Lip espera audio em **16kHz**. O TTS envia em **24kHz**. O server faz resample automaticamente.
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
- "type": "done",
250
- "total_duration_ms": 5000,
251
- "total_frames": 125,
252
- "total_chunks": 50,
253
- "elapsed_ms": 3500
254
  }
255
  ```
256
 
257
- ```json
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
- ## Algoritmo de Montagem de Chunks
275
-
276
- ```python
277
- CHUNK_DURATION_MS = 100 # Tamanho alvo de cada chunk
278
- AUDIO_SAMPLE_RATE = 24000 # Orpheus
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
- ## Sincronizacao Audio/Video
 
 
 
 
317
 
318
- ### Calculos
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
- ### Exemplo de Chunk
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
- Total por chunk: ~50-80 KB (depende da compressao JPEG)
338
- ```
339
 
340
- ## Estrutura do Projeto
341
 
 
342
  ```
343
- /workspace/interface/
344
- ├── CLAUDE.md # Esta documentacao
345
- ├── server.py # Servidor WebSocket com montagem de chunks
346
- ├── index.html # Frontend com playback progressivo
347
- ├── idle.mp4 # Video de idle loop
348
- └── static/ # Arquivos estaticos
349
  ```
350
 
351
- ## Configuracao
352
-
353
  ```bash
354
- # Variaveis de ambiente (opcional)
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 - Streaming Progressivo
372
  ==================================================
373
  Porta: 8080
374
- TTS (Orpheus): ws://localhost:8081/ws (24kHz)
375
- Wav2Lip: ws://localhost:8082/ws (16kHz, apenas frames)
376
- Chunk duration: 100ms
377
- Video: 25fps (40ms/frame)
 
 
 
 
 
378
  ==================================================
379
  ```
380
 
381
- ## Vozes Disponiveis (TTS Orpheus)
 
 
382
 
383
  | Voice | Genero |
384
  |-------|-----------|
@@ -388,26 +247,72 @@ Video: 25fps (40ms/frame)
388
  | leo | Masculino |
389
  | dan | Masculino |
390
 
391
- ## Resumo das Portas
392
 
393
- | Servico | Porta | Audio | Video |
394
- |------------------|-------|--------------|------------|
395
- | Interface Server | 8080 | Monta chunks | - |
396
- | TTS (Orpheus) | 8081 | PCM 24kHz | - |
397
- | Wav2Lip | 8082 | DESCARTAR! | JPEG 25fps |
398
 
399
- ## Dependencias Python
 
 
 
 
400
 
401
- ```
402
- aiohttp>=3.9.0
403
- ```
 
 
 
 
 
 
 
 
 
 
404
 
405
- ## Notas Importantes
406
 
407
- 1. **NUNCA usar audio do Wav2Lip no frontend** - O Wav2Lip usa eSpeak internamente para gerar lip sync. Este audio e robotico e deve ser DESCARTADO. Usar APENAS os frames JPEG.
 
 
 
 
408
 
409
- 2. **SEMPRE usar audio do Orpheus** - O audio final enviado ao frontend deve vir do Orpheus TTS, que gera voz de alta qualidade.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
- 3. **Streaming progressivo e obrigatorio** - Nao esperar todo o audio/video ficar pronto. Montar e enviar chunks de ~100ms conforme os dados chegam.
412
 
413
- 4. **Frontend deve iniciar playback cedo** - Assim que receber ~200ms de dados (2 chunks), iniciar reproducao enquanto continua recebendo.
 
 
 
 
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