marcosremar2 Claude Opus 4.5 commited on
Commit
4d63615
·
1 Parent(s): 14b6b3e

Add Orpheus TTS integration with fallback to Wav2Lip audio

Browse files

- Add Orpheus TTS WebSocket client (ws://localhost:8081/ws)
- Fetch high-quality audio from Orpheus in parallel with Wav2Lip frames
- Fallback to Wav2Lip eSpeak audio when Orpheus is unavailable
- Audio is sent as binary PCM for efficient streaming

🤖 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_optimized.py +92 -16
interface/server_optimized.py CHANGED
@@ -25,8 +25,10 @@ import struct
25
 
26
  # Configuracao
27
  WAV2LIP_WS = os.getenv("WAV2LIP_WS", "ws://localhost:8082/ws")
 
28
  PORT = int(os.getenv("PORT", "8080"))
29
  VIDEO_FPS = 25
 
30
 
31
  routes = web.RouteTableDef()
32
 
@@ -73,8 +75,54 @@ class OptimizedSession:
73
  except Exception as e:
74
  print(f"Erro enviando audio binário: {e}")
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  async def run(self, text: str, voice: str):
77
- """Streaming otimizado com binário."""
78
  self.is_running = True
79
  self.start_time = time.time()
80
  self.total_bytes = 0
@@ -83,6 +131,11 @@ class OptimizedSession:
83
 
84
  try:
85
  async with aiohttp.ClientSession() as session:
 
 
 
 
 
86
  wav2lip_ws = await session.ws_connect(
87
  WAV2LIP_WS,
88
  timeout=aiohttp.ClientWSTimeout(ws_close=300),
@@ -91,7 +144,7 @@ class OptimizedSession:
91
 
92
  await self.send_json("status", message="Gerando...")
93
 
94
- # Enviar texto para Wav2Lip
95
  await wav2lip_ws.send_json({
96
  "action": "speak",
97
  "text": text,
@@ -102,8 +155,9 @@ class OptimizedSession:
102
  print(f"[Optimized] Texto: '{text[:50]}...'")
103
 
104
  first_frame_time = None
 
105
 
106
- # Receber e repassar
107
  async for msg in wav2lip_ws:
108
  if not self.is_running:
109
  break
@@ -118,7 +172,7 @@ class OptimizedSession:
118
  frame_bytes = base64.b64decode(frame_b64)
119
  self.total_frames += 1
120
 
121
- # Enviar como binário puro (sem re-encodar para base64)
122
  await self.send_binary_frame(frame_bytes, self.total_frames)
123
 
124
  if first_frame_time is None:
@@ -127,20 +181,39 @@ class OptimizedSession:
127
  print(f"[Optimized] Primeiro frame: {latency_ms}ms")
128
  await self.send_json("first_frame", latency_ms=latency_ms)
129
 
130
- elif msg_type == "full_audio":
131
- # Decodificar e enviar como binário
132
- audio_b64 = data.get("audio", "")
133
- audio_bytes = base64.b64decode(audio_b64)
134
- sample_rate = 24000
 
 
 
 
 
135
 
136
- # Extrair PCM se for WAV
137
- if len(audio_bytes) > 44 and audio_bytes[:4] == b'RIFF':
138
- audio_bytes = audio_bytes[44:]
139
-
140
- await self.send_binary_audio(audio_bytes, sample_rate)
141
- print(f"[Optimized] Audio: {len(audio_bytes)} bytes")
 
 
 
 
 
 
142
 
143
  elif msg_type == "done":
 
 
 
 
 
 
 
 
144
  print(f"[Optimized] Concluído: {self.total_frames} frames")
145
  break
146
 
@@ -159,6 +232,10 @@ class OptimizedSession:
159
 
160
  await wav2lip_ws.close()
161
 
 
 
 
 
162
  except Exception as e:
163
  print(f"Erro: {e}")
164
  import traceback
@@ -168,7 +245,6 @@ class OptimizedSession:
168
 
169
  # Finalizar
170
  elapsed = time.time() - self.start_time
171
- savings = self.total_bytes # vs base64 seria ~33% maior
172
 
173
  print(f"Sessao: {elapsed:.2f}s, {self.total_frames} frames, {self.total_bytes/1024:.1f}KB binário")
174
 
 
25
 
26
  # Configuracao
27
  WAV2LIP_WS = os.getenv("WAV2LIP_WS", "ws://localhost:8082/ws")
28
+ ORPHEUS_WS = os.getenv("ORPHEUS_WS", "ws://localhost:8081/ws")
29
  PORT = int(os.getenv("PORT", "8080"))
30
  VIDEO_FPS = 25
31
+ AUDIO_SAMPLE_RATE = 24000
32
 
33
  routes = web.RouteTableDef()
34
 
 
75
  except Exception as e:
76
  print(f"Erro enviando audio binário: {e}")
77
 
78
+ async def fetch_audio_from_orpheus(self, session, text: str, voice: str):
79
+ """Busca áudio do Orpheus TTS via WebSocket."""
80
+ audio_chunks = []
81
+ try:
82
+ orpheus_ws = await session.ws_connect(
83
+ ORPHEUS_WS,
84
+ timeout=aiohttp.ClientWSTimeout(ws_close=60),
85
+ max_msg_size=10*1024*1024
86
+ )
87
+
88
+ # Enviar requisição para Orpheus (action: synthesize)
89
+ await orpheus_ws.send_json({
90
+ "action": "synthesize",
91
+ "text": text,
92
+ "voice": voice,
93
+ "stream": True
94
+ })
95
+
96
+ print(f"[Orpheus] Requisição enviada: '{text[:30]}...'")
97
+
98
+ # Receber chunks de áudio
99
+ async for msg in orpheus_ws:
100
+ if msg.type == aiohttp.WSMsgType.BINARY:
101
+ audio_chunks.append(msg.data)
102
+ elif msg.type == aiohttp.WSMsgType.TEXT:
103
+ data = json.loads(msg.data)
104
+ if data.get("type") == "done" or data.get("done"):
105
+ break
106
+ elif data.get("type") == "error":
107
+ print(f"[Orpheus] Erro: {data.get('message')}")
108
+ break
109
+ elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
110
+ break
111
+
112
+ await orpheus_ws.close()
113
+
114
+ except Exception as e:
115
+ print(f"[Orpheus] Erro: {e}")
116
+
117
+ # Concatenar chunks
118
+ if audio_chunks:
119
+ full_audio = b''.join(audio_chunks)
120
+ print(f"[Orpheus] Audio recebido: {len(full_audio)} bytes")
121
+ return full_audio
122
+ return None
123
+
124
  async def run(self, text: str, voice: str):
125
+ """Streaming otimizado com binário - audio do Orpheus TTS."""
126
  self.is_running = True
127
  self.start_time = time.time()
128
  self.total_bytes = 0
 
131
 
132
  try:
133
  async with aiohttp.ClientSession() as session:
134
+ # Iniciar busca de áudio do Orpheus em paralelo
135
+ audio_task = asyncio.create_task(
136
+ self.fetch_audio_from_orpheus(session, text, voice)
137
+ )
138
+
139
  wav2lip_ws = await session.ws_connect(
140
  WAV2LIP_WS,
141
  timeout=aiohttp.ClientWSTimeout(ws_close=300),
 
144
 
145
  await self.send_json("status", message="Gerando...")
146
 
147
+ # Enviar texto para Wav2Lip (ainda usa eSpeak para lip sync)
148
  await wav2lip_ws.send_json({
149
  "action": "speak",
150
  "text": text,
 
155
  print(f"[Optimized] Texto: '{text[:50]}...'")
156
 
157
  first_frame_time = None
158
+ audio_sent = False
159
 
160
+ # Receber frames do Wav2Lip
161
  async for msg in wav2lip_ws:
162
  if not self.is_running:
163
  break
 
172
  frame_bytes = base64.b64decode(frame_b64)
173
  self.total_frames += 1
174
 
175
+ # Enviar como binário puro
176
  await self.send_binary_frame(frame_bytes, self.total_frames)
177
 
178
  if first_frame_time is None:
 
181
  print(f"[Optimized] Primeiro frame: {latency_ms}ms")
182
  await self.send_json("first_frame", latency_ms=latency_ms)
183
 
184
+ # Enviar áudio do Orpheus quando disponível (apenas uma vez)
185
+ if not audio_sent and audio_task.done():
186
+ audio_bytes = audio_task.result()
187
+ if audio_bytes:
188
+ # Extrair PCM se for WAV
189
+ if len(audio_bytes) > 44 and audio_bytes[:4] == b'RIFF':
190
+ audio_bytes = audio_bytes[44:]
191
+ await self.send_binary_audio(audio_bytes, AUDIO_SAMPLE_RATE)
192
+ print(f"[Optimized] Audio Orpheus: {len(audio_bytes)} bytes")
193
+ audio_sent = True
194
 
195
+ elif msg_type == "full_audio":
196
+ # Usar áudio do Wav2Lip como fallback se Orpheus não enviou
197
+ if not audio_sent:
198
+ wav_audio = data.get("audio", "")
199
+ if wav_audio:
200
+ audio_bytes = base64.b64decode(wav_audio)
201
+ # Extrair PCM se for WAV
202
+ if len(audio_bytes) > 44 and audio_bytes[:4] == b'RIFF':
203
+ audio_bytes = audio_bytes[44:]
204
+ await self.send_binary_audio(audio_bytes, AUDIO_SAMPLE_RATE)
205
+ print(f"[Optimized] Audio Wav2Lip fallback: {len(audio_bytes)} bytes")
206
+ audio_sent = True
207
 
208
  elif msg_type == "done":
209
+ # Garantir que audio foi enviado
210
+ if not audio_sent:
211
+ audio_bytes = await audio_task
212
+ if audio_bytes:
213
+ if len(audio_bytes) > 44 and audio_bytes[:4] == b'RIFF':
214
+ audio_bytes = audio_bytes[44:]
215
+ await self.send_binary_audio(audio_bytes, AUDIO_SAMPLE_RATE)
216
+ print(f"[Optimized] Audio Orpheus (final): {len(audio_bytes)} bytes")
217
  print(f"[Optimized] Concluído: {self.total_frames} frames")
218
  break
219
 
 
232
 
233
  await wav2lip_ws.close()
234
 
235
+ # Cancelar task de audio se ainda estiver rodando
236
+ if not audio_task.done():
237
+ audio_task.cancel()
238
+
239
  except Exception as e:
240
  print(f"Erro: {e}")
241
  import traceback
 
245
 
246
  # Finalizar
247
  elapsed = time.time() - self.start_time
 
248
 
249
  print(f"Sessao: {elapsed:.2f}s, {self.total_frames} frames, {self.total_bytes/1024:.1f}KB binário")
250