| |
| """ |
| 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""" |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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""" |
| |
| 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) |
| |
| |
| required_speed = int(base_speed * (base_duration / target_duration)) |
| required_speed = max(80, min(450, required_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) |
| |
| |
| 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") |
| |
| |
| 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})") |
| |
| |
| print("\n3. Extraindo timestamps com Whisper...") |
| eleven_words = extract_word_timestamps(eleven_path) |
| espeak_words = extract_word_timestamps(espeak_path) |
| |
| |
| 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)) |
|
|