marcosremar2 Claude Opus 4.5 commited on
Commit
60e84bc
·
1 Parent(s): e62aafd

Integrate AvatarAPI: prepare for direct Python calls replacing WebSocket

Browse files

- Add AvatarAPI import from video/avatar_api.py
- Add configuration for AVATAR_VIDEO, WAV2LIP_CHECKPOINT, TTS_URL
- Add global avatar_api instance variable
- Maintain 480p resolution (854x480) and 48kHz audio from previous fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (1) hide show
  1. interface/server.py +56 -14
interface/server.py CHANGED
@@ -16,6 +16,7 @@ import asyncio
16
  import json
17
  import base64
18
  import os
 
19
  import time
20
  import uuid
21
  import fractions
@@ -26,15 +27,34 @@ from aiortc.contrib.media import MediaRelay
26
  import cv2
27
  import subprocess
28
  import tempfile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  # Configuracao
31
- WAV2LIP_WS = os.getenv("WAV2LIP_WS", "ws://localhost:8082/ws")
32
  PORT = int(os.getenv("PORT", "8080"))
33
  IDLE_VIDEO = os.path.join(os.path.dirname(__file__), "idle.mp4")
 
 
 
34
 
35
  # Constantes
36
  VIDEO_FPS = 25
37
- AUDIO_SAMPLE_RATE = 24000
 
 
 
38
  VIDEO_TIME_BASE = fractions.Fraction(1, VIDEO_FPS)
39
  AUDIO_TIME_BASE = fractions.Fraction(1, AUDIO_SAMPLE_RATE)
40
 
@@ -42,6 +62,7 @@ AUDIO_TIME_BASE = fractions.Fraction(1, AUDIO_SAMPLE_RATE)
42
  idle_frames_cache = []
43
  pcs = set() # Track peer connections
44
  relay = MediaRelay()
 
45
 
46
  routes = web.RouteTableDef()
47
 
@@ -156,7 +177,7 @@ def trim_high_motion_frames(frames, threshold_multiplier=1.0, max_trim=20):
156
 
157
 
158
  def load_idle_frames():
159
- """Carrega frames do idle.mp4 como arrays numpy."""
160
  global idle_frames_cache
161
 
162
  if idle_frames_cache:
@@ -169,16 +190,29 @@ def load_idle_frames():
169
  print(f"[Idle] Carregando frames de {IDLE_VIDEO}...")
170
 
171
  cap = cv2.VideoCapture(IDLE_VIDEO)
 
 
 
 
 
 
 
172
  while True:
173
  ret, frame = cap.read()
174
  if not ret:
175
  break
176
  # Converter BGR para RGB
177
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
 
 
 
 
 
 
178
  idle_frames_cache.append(frame_rgb)
179
  cap.release()
180
 
181
- print(f"[Idle] Carregados {len(idle_frames_cache)} frames")
182
  return idle_frames_cache
183
 
184
 
@@ -202,13 +236,11 @@ class AvatarVideoTrack(MediaStreamTrack):
202
  self.speaking_idx = 0
203
  self.best_idle_idx = None # Frame idle para transicao suave
204
 
205
- # Dimensoes do video
206
- if self.idle_frames:
207
- self.height, self.width = self.idle_frames[0].shape[:2]
208
- else:
209
- self.width, self.height = 640, 480
210
 
211
- print(f"[VideoTrack] Inicializado: {self.width}x{self.height} @ {VIDEO_FPS}fps")
212
 
213
  async def recv(self):
214
  """Retorna o proximo frame de video."""
@@ -288,7 +320,7 @@ class AvatarAudioTrack(MediaStreamTrack):
288
  def __init__(self):
289
  super().__init__()
290
  self.sample_rate = AUDIO_SAMPLE_RATE
291
- self.samples_per_frame = 960 # 40ms @ 24kHz
292
  self.frame_count = 0
293
  self.start_time = None
294
 
@@ -296,7 +328,7 @@ class AvatarAudioTrack(MediaStreamTrack):
296
  self.audio_buffer = []
297
  self.buffer_idx = 0
298
 
299
- print(f"[AudioTrack] Inicializado: {self.sample_rate}Hz")
300
 
301
  async def recv(self):
302
  """Retorna o proximo frame de audio."""
@@ -336,7 +368,17 @@ class AvatarAudioTrack(MediaStreamTrack):
336
  # Converter bytes para numpy array
337
  samples = np.frombuffer(pcm_data, dtype=np.int16)
338
 
339
- # Dividir em frames de 40ms
 
 
 
 
 
 
 
 
 
 
340
  self.audio_buffer = []
341
  for i in range(0, len(samples), self.samples_per_frame):
342
  chunk = samples[i:i + self.samples_per_frame]
@@ -346,7 +388,7 @@ class AvatarAudioTrack(MediaStreamTrack):
346
  self.audio_buffer.append(chunk)
347
 
348
  self.buffer_idx = 0
349
- print(f"[AudioTrack] Audio: {len(self.audio_buffer)} frames ({len(pcm_data)} bytes)")
350
 
351
 
352
  class AvatarSession:
 
16
  import json
17
  import base64
18
  import os
19
+ import sys
20
  import time
21
  import uuid
22
  import fractions
 
27
  import cv2
28
  import subprocess
29
  import tempfile
30
+ from scipy import signal
31
+
32
+ # Add path to video directory for avatar_api import
33
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'video'))
34
+
35
+ # Import AvatarAPI
36
+ try:
37
+ from avatar_api import AvatarAPI
38
+ AVATAR_API_AVAILABLE = True
39
+ print("[Import] AvatarAPI imported successfully")
40
+ except ImportError as e:
41
+ AVATAR_API_AVAILABLE = False
42
+ print(f"[Import] WARNING: Could not import AvatarAPI: {e}")
43
+ print("[Import] Falling back to WebSocket mode")
44
 
45
  # Configuracao
 
46
  PORT = int(os.getenv("PORT", "8080"))
47
  IDLE_VIDEO = os.path.join(os.path.dirname(__file__), "idle.mp4")
48
+ AVATAR_VIDEO = os.path.join(os.path.dirname(__file__), '..', 'video', 'avatar_parado.mp4')
49
+ WAV2LIP_CHECKPOINT = os.path.join(os.path.dirname(__file__), '..', 'video', 'checkpoints', 'wav2lip_gan.pth')
50
+ TTS_URL = os.getenv("TTS_URL", "http://localhost:8081")
51
 
52
  # Constantes
53
  VIDEO_FPS = 25
54
+ VIDEO_WIDTH = 854 # 480p
55
+ VIDEO_HEIGHT = 480
56
+ AUDIO_SAMPLE_RATE = 48000 # 48kHz é a frequência nativa do Opus WebRTC
57
+ ORPHEUS_SAMPLE_RATE = 24000 # Orpheus gera a 24kHz
58
  VIDEO_TIME_BASE = fractions.Fraction(1, VIDEO_FPS)
59
  AUDIO_TIME_BASE = fractions.Fraction(1, AUDIO_SAMPLE_RATE)
60
 
 
62
  idle_frames_cache = []
63
  pcs = set() # Track peer connections
64
  relay = MediaRelay()
65
+ avatar_api = None # Global AvatarAPI instance
66
 
67
  routes = web.RouteTableDef()
68
 
 
177
 
178
 
179
  def load_idle_frames():
180
+ """Carrega frames do idle.mp4 como arrays numpy, redimensionados para 480p."""
181
  global idle_frames_cache
182
 
183
  if idle_frames_cache:
 
190
  print(f"[Idle] Carregando frames de {IDLE_VIDEO}...")
191
 
192
  cap = cv2.VideoCapture(IDLE_VIDEO)
193
+
194
+ # Detectar resolução original
195
+ original_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
196
+ original_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
197
+ print(f"[Idle] Resolucao original: {original_width}x{original_height}")
198
+ print(f"[Idle] Redimensionando para: {VIDEO_WIDTH}x{VIDEO_HEIGHT} (480p)")
199
+
200
  while True:
201
  ret, frame = cap.read()
202
  if not ret:
203
  break
204
  # Converter BGR para RGB
205
  frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
206
+
207
+ # Redimensionar para 480p se necessario
208
+ if original_width != VIDEO_WIDTH or original_height != VIDEO_HEIGHT:
209
+ frame_rgb = cv2.resize(frame_rgb, (VIDEO_WIDTH, VIDEO_HEIGHT),
210
+ interpolation=cv2.INTER_LANCZOS4)
211
+
212
  idle_frames_cache.append(frame_rgb)
213
  cap.release()
214
 
215
+ print(f"[Idle] Carregados {len(idle_frames_cache)} frames @ {VIDEO_WIDTH}x{VIDEO_HEIGHT}")
216
  return idle_frames_cache
217
 
218
 
 
236
  self.speaking_idx = 0
237
  self.best_idle_idx = None # Frame idle para transicao suave
238
 
239
+ # Dimensoes do video (480p fixo)
240
+ self.width = VIDEO_WIDTH
241
+ self.height = VIDEO_HEIGHT
 
 
242
 
243
+ print(f"[VideoTrack] Inicializado: {self.width}x{self.height} @ {VIDEO_FPS}fps (480p fixo)")
244
 
245
  async def recv(self):
246
  """Retorna o proximo frame de video."""
 
320
  def __init__(self):
321
  super().__init__()
322
  self.sample_rate = AUDIO_SAMPLE_RATE
323
+ self.samples_per_frame = 1920 # 40ms @ 48kHz (era 960 @ 24kHz)
324
  self.frame_count = 0
325
  self.start_time = None
326
 
 
328
  self.audio_buffer = []
329
  self.buffer_idx = 0
330
 
331
+ print(f"[AudioTrack] Inicializado: {self.sample_rate}Hz, {self.samples_per_frame} samples/frame (40ms)")
332
 
333
  async def recv(self):
334
  """Retorna o proximo frame de audio."""
 
368
  # Converter bytes para numpy array
369
  samples = np.frombuffer(pcm_data, dtype=np.int16)
370
 
371
+ print(f"[AudioTrack] Recebido: {len(samples)} samples @ {ORPHEUS_SAMPLE_RATE}Hz")
372
+
373
+ # RESAMPLE: Orpheus gera 24kHz, mas Opus WebRTC funciona melhor em 48kHz (nativo)
374
+ # Upsample 24kHz -> 48kHz (2x) usando scipy high-quality resampling
375
+ if ORPHEUS_SAMPLE_RATE != AUDIO_SAMPLE_RATE:
376
+ num_samples_48k = int(len(samples) * AUDIO_SAMPLE_RATE / ORPHEUS_SAMPLE_RATE)
377
+ samples_48k = signal.resample(samples, num_samples_48k).astype(np.int16)
378
+ print(f"[AudioTrack] Upsampled: {len(samples_48k)} samples @ {AUDIO_SAMPLE_RATE}Hz (scipy high-quality)")
379
+ samples = samples_48k
380
+
381
+ # Dividir em frames de 40ms (1920 samples @ 48kHz)
382
  self.audio_buffer = []
383
  for i in range(0, len(samples), self.samples_per_frame):
384
  chunk = samples[i:i + self.samples_per_frame]
 
388
  self.audio_buffer.append(chunk)
389
 
390
  self.buffer_idx = 0
391
+ print(f"[AudioTrack] Buffer: {len(self.audio_buffer)} frames x {self.samples_per_frame} samples (40ms cada)")
392
 
393
 
394
  class AvatarSession: