dumont-talker / server /sync_tts.py
marcosremar2
Initial commit: dumont-talker speech-to-speech avatar
71e110b
Raw
History Blame
5.97 kB
#!/usr/bin/env python3
"""
TTS Synchronization Tool
Compares word timestamps between espeak and ElevenLabs
"""
import subprocess
import asyncio
import json
import os
import sys
import wave
WHISPER_CLI = "/workspace/MuseTalk1.5/vendor/whisper/build/bin/whisper-cli"
WHISPER_MODEL = "/workspace/MuseTalk1.5/vendor/whisper/models/ggml-large-v3-turbo-q5_0.bin"
def get_audio_duration(filepath):
"""Get audio duration in seconds"""
if filepath.endswith('.wav'):
with wave.open(filepath, 'r') as f:
return f.getnframes() / float(f.getframerate())
else:
result = subprocess.run([
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', filepath
], capture_output=True, text=True)
return float(result.stdout.strip())
def extract_word_timestamps(audio_path):
"""Extract word timestamps using local Whisper"""
# Convert to wav if needed
if not audio_path.endswith('.wav'):
wav_path = audio_path.replace('.mp3', '.wav')
subprocess.run([
'ffmpeg', '-y', '-v', 'quiet', '-i', audio_path, '-ar', '16000', '-ac', '1', wav_path
], capture_output=True)
audio_path = wav_path
# Run whisper-cli
result = subprocess.run([
WHISPER_CLI, '-m', WHISPER_MODEL, '-f', audio_path,
'--output-json', '--max-len', '1', '-l', 'pt', '--no-prints'
], capture_output=True, text=True)
# Read JSON output
json_path = audio_path + '.json'
if os.path.exists(json_path):
with open(json_path) as f:
data = json.load(f)
os.remove(json_path)
words = []
for item in data.get('transcription', []):
text = item['text'].strip()
if text and text not in [',', '.', '?', '!']:
words.append({
'text': text,
'start': item['offsets']['from'],
'end': item['offsets']['to']
})
return words
return []
def generate_espeak(text, output_path, speed=175):
"""Generate espeak audio"""
subprocess.run([
"espeak-ng", "-v", "pt-br", "-s", str(speed), "-w", output_path, text
], capture_output=True)
return get_audio_duration(output_path)
async def generate_elevenlabs(text, output_path):
"""Generate ElevenLabs audio"""
import httpx
ELEVENLABS_API_KEY = "sk_857e9e6f2412ddf3ff5334b736e4b571641d26225c0d8d62"
ELEVENLABS_VOICE_ID = "21m00Tcm4TlvDq8ikWAM"
async with httpx.AsyncClient(timeout=60.0) as client:
response = await client.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{ELEVENLABS_VOICE_ID}/stream",
headers={"xi-api-key": ELEVENLABS_API_KEY, "Content-Type": "application/json"},
json={"text": text, "model_id": "eleven_flash_v2_5", "voice_settings": {"stability": 0.5, "similarity_boost": 0.75}}
)
with open(output_path, "wb") as f:
f.write(response.content)
return get_audio_duration(output_path)
def generate_espeak_synced(text, output_path, target_duration, base_speed=175):
"""Generate espeak with adjusted speed to match target duration"""
# Get base duration
temp_path = "/tmp/espeak_temp.wav"
subprocess.run([
"espeak-ng", "-v", "pt-br", "-s", str(base_speed), "-w", temp_path, text
], capture_output=True)
base_duration = get_audio_duration(temp_path)
# Calculate required speed
required_speed = int(base_speed * (base_duration / target_duration))
required_speed = max(80, min(450, required_speed))
# Generate with adjusted speed
subprocess.run([
"espeak-ng", "-v", "pt-br", "-s", str(required_speed), "-w", output_path, text
], capture_output=True)
return get_audio_duration(output_path), required_speed
async def compare_timestamps(text):
"""Compare word timestamps between espeak and ElevenLabs"""
print(f"\n{'='*70}")
print(f"COMPARAÇÃO DE TIMESTAMPS: \"{text}\"")
print('='*70)
# Generate ElevenLabs
eleven_path = "/tmp/compare_eleven.mp3"
print("\n1. Gerando ElevenLabs...")
eleven_dur = await generate_elevenlabs(text, eleven_path)
print(f" Duração: {eleven_dur:.2f}s")
# Generate synced espeak
espeak_path = "/tmp/compare_espeak.wav"
print("\n2. Gerando espeak sincronizado...")
espeak_dur, speed = generate_espeak_synced(text, espeak_path, eleven_dur)
print(f" Duração: {espeak_dur:.2f}s (speed={speed})")
# Extract timestamps
print("\n3. Extraindo timestamps com Whisper...")
eleven_words = extract_word_timestamps(eleven_path)
espeak_words = extract_word_timestamps(espeak_path)
# Compare
print("\n" + "-"*70)
print(f"{'PALAVRA':<15} {'ELEVENLABS':<20} {'ESPEAK':<20} {'DIFF':<10}")
print("-"*70)
total_diff = 0
count = 0
for i, ew in enumerate(eleven_words):
if i < len(espeak_words):
sw = espeak_words[i]
diff = abs(ew['start'] - sw['start'])
total_diff += diff
count += 1
print(f"{ew['text']:<15} {ew['start']:>5}-{ew['end']:<5} ms {sw['start']:>5}-{sw['end']:<5} ms {diff:>5} ms")
print("-"*70)
if count > 0:
avg_diff = total_diff / count
print(f"\nMÉDIA DE DIFERENÇA: {avg_diff:.0f} ms")
if avg_diff < 50:
print("✓ EXCELENTE SINCRONIZAÇÃO!")
elif avg_diff < 100:
print("✓ BOA SINCRONIZAÇÃO")
elif avg_diff < 200:
print("⚠ SINCRONIZAÇÃO ACEITÁVEL")
else:
print("✗ SINCRONIZAÇÃO RUIM")
return eleven_words, espeak_words
if __name__ == "__main__":
text = sys.argv[1] if len(sys.argv) > 1 else "Olá, tudo bem? Como posso te ajudar?"
asyncio.run(compare_timestamps(text))