marcosremar2's picture
Add temporal analysis with phoneme timestamps, word boundaries and speaker diarization + visualizations
93a3819
import gradio as gr
import torch
import numpy as np
from transformers import AutoModel, Wav2Vec2Processor
import librosa
import scipy.stats as stats
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from sklearn.cluster import KMeans
from scipy.signal import find_peaks
import warnings
warnings.filterwarnings('ignore')
# Configurar o modelo
MODEL_NAME = "marcosremar2/wavlm-large-deploy"
print("Carregando modelo...")
try:
processor = Wav2Vec2Processor.from_pretrained("facebook/wav2vec2-base-960h")
model = AutoModel.from_pretrained(MODEL_NAME)
model.eval()
print("Modelo carregado com sucesso!")
except Exception as e:
print(f"Erro ao carregar modelo: {e}")
processor = None
model = None
def analyze_prosodic_features(features):
"""Analisa características prosódicas nas features"""
# Features relacionadas à prosódia geralmente estão nas primeiras dimensões
prosodic_dims = features[:, :128] # Primeiras 128 dimensões
# Análise de variabilidade (relacionada ao ritmo e stress)
variance = np.var(prosodic_dims, axis=0)
high_variance_dims = np.where(variance > np.percentile(variance, 90))[0]
# Análise de tendências temporais (relacionada à entonação)
trends = []
for dim in range(min(32, prosodic_dims.shape[1])):
slope, _, r_value, _, _ = stats.linregress(range(len(prosodic_dims)), prosodic_dims[:, dim])
trends.append((dim, slope, r_value**2))
# Ordenar por força da tendência
trends.sort(key=lambda x: abs(x[1]), reverse=True)
return {
'high_variance_dims': high_variance_dims[:10],
'variance_stats': {
'mean': np.mean(variance),
'std': np.std(variance),
'max': np.max(variance)
},
'strongest_trends': trends[:5]
}
def analyze_phonetic_features(features):
"""Analisa características fonéticas"""
# Features fonéticas geralmente estão nas dimensões médias
phonetic_dims = features[:, 128:512] # Dimensões 128-512
# Análise de padrões de ativação
activation_patterns = np.mean(phonetic_dims, axis=0)
highly_active = np.where(activation_patterns > np.percentile(activation_patterns, 85))[0]
# Análise de correlações entre dimensões (indicativo de co-articulação)
correlations = np.corrcoef(phonetic_dims.T)
high_corr_pairs = []
for i in range(min(50, correlations.shape[0])):
for j in range(i+1, min(50, correlations.shape[1])):
if abs(correlations[i, j]) > 0.7:
high_corr_pairs.append((i, j, correlations[i, j]))
return {
'highly_active_dims': highly_active[:15],
'activation_stats': {
'mean': np.mean(activation_patterns),
'std': np.std(activation_patterns),
'active_ratio': len(highly_active) / len(activation_patterns)
},
'high_correlations': sorted(high_corr_pairs, key=lambda x: abs(x[2]), reverse=True)[:5]
}
def analyze_temporal_features(features):
"""Analisa características temporais e de duração"""
# Análise da evolução temporal das features
frame_energy = np.mean(features**2, axis=1) # Energia por frame
# Detectar segmentos de alta/baixa energia (aproximação de segmentação)
energy_threshold = np.mean(frame_energy)
high_energy_frames = np.where(frame_energy > energy_threshold)[0]
# Análise de transições (mudanças abruptas indicam fronteiras fonéticas)
feature_diff = np.diff(features, axis=0)
transition_strength = np.mean(feature_diff**2, axis=1)
strong_transitions = np.where(transition_strength > np.percentile(transition_strength, 80))[0]
return {
'energy_analysis': {
'mean_energy': np.mean(frame_energy),
'energy_variance': np.var(frame_energy),
'high_energy_ratio': len(high_energy_frames) / len(frame_energy)
},
'transition_analysis': {
'num_strong_transitions': len(strong_transitions),
'avg_transition_strength': np.mean(transition_strength),
'transition_density': len(strong_transitions) / len(features)
}
}
def analyze_speaker_features(features):
"""Analisa características do falante"""
# Features de speaker geralmente estão nas últimas dimensões
speaker_dims = features[:, 512:] # Dimensões finais
# Consistência das features de speaker (devem ser relativamente estáveis)
speaker_stability = np.std(speaker_dims, axis=0)
stable_dims = np.where(speaker_stability < np.percentile(speaker_stability, 30))[0]
# Média geral das características do speaker
speaker_profile = np.mean(speaker_dims, axis=0)
return {
'stability_analysis': {
'num_stable_dims': len(stable_dims),
'mean_stability': np.mean(speaker_stability),
'stability_ratio': len(stable_dims) / speaker_dims.shape[1]
},
'speaker_profile_stats': {
'profile_mean': np.mean(speaker_profile),
'profile_std': np.std(speaker_profile),
'dominant_features': np.where(speaker_profile > np.percentile(speaker_profile, 75))[0][:10]
}
}
def detect_phoneme_boundaries(features, audio_duration):
"""Detecta fronteiras de fonemas usando mudanças nas features"""
# Calcular diferenças entre frames consecutivos
feature_diff = np.diff(features, axis=0)
change_magnitude = np.mean(feature_diff**2, axis=1)
# Encontrar picos (possíveis fronteiras de fonemas)
peaks, _ = find_peaks(change_magnitude,
height=np.percentile(change_magnitude, 70),
distance=3) # Mínimo 3 frames entre picos
# Converter frames para timestamps
frame_duration = audio_duration / len(features)
phoneme_boundaries = peaks * frame_duration
# Estimar fonemas (simplificado baseado em energia e mudanças espectrais)
phoneme_types = []
for i in range(len(phoneme_boundaries)):
if i < len(peaks):
frame_idx = peaks[i]
# Análise simplificada das características espectrais
spectral_features = features[frame_idx, 128:256] # Features fonéticas
energy = np.mean(features[frame_idx]**2)
# Classificação básica baseada em energia e padrões espectrais
if energy > np.percentile([np.mean(features[j]**2) for j in range(len(features))], 80):
if np.mean(spectral_features[:32]) > np.mean(spectral_features[32:]):
phoneme_types.append("VOGAL")
else:
phoneme_types.append("CONSOANTE_FORTE")
else:
phoneme_types.append("CONSOANTE_FRACA")
return phoneme_boundaries, phoneme_types
def detect_word_boundaries(features, audio_duration):
"""Detecta fronteiras de palavras usando pausas e mudanças prosódicas"""
# Energia por frame
frame_energy = np.mean(features**2, axis=1)
# Detectar pausas (baixa energia)
energy_threshold = np.percentile(frame_energy, 20)
low_energy_frames = frame_energy < energy_threshold
# Encontrar segmentos contínuos de baixa energia (pausas)
pause_starts = []
pause_ends = []
in_pause = False
for i, is_low in enumerate(low_energy_frames):
if is_low and not in_pause:
pause_starts.append(i)
in_pause = True
elif not is_low and in_pause:
pause_ends.append(i)
in_pause = False
# Converter para timestamps
frame_duration = audio_duration / len(features)
word_boundaries = []
for start, end in zip(pause_starts, pause_ends):
if (end - start) * frame_duration > 0.1: # Pausas > 100ms
word_boundaries.append((start * frame_duration, end * frame_duration))
return word_boundaries
def analyze_speaker_changes(features, audio_duration):
"""Analisa mudanças de falante usando clustering das features de speaker"""
speaker_features = features[:, 512:] # Features de falante
if speaker_features.shape[1] < 10:
return [], []
# Suavizar features para reduzir ruído
window_size = 5
smoothed_features = np.array([
np.mean(speaker_features[max(0, i-window_size):i+window_size+1], axis=0)
for i in range(len(speaker_features))
])
# Clustering para identificar diferentes falantes
try:
n_speakers = min(3, len(smoothed_features) // 10) # Máximo 3 falantes
if n_speakers < 2:
return [], []
kmeans = KMeans(n_clusters=n_speakers, random_state=42, n_init=10)
speaker_labels = kmeans.fit_predict(smoothed_features)
# Detectar mudanças de falante
speaker_changes = []
current_speaker = speaker_labels[0]
for i, label in enumerate(speaker_labels[1:], 1):
if label != current_speaker:
timestamp = i * (audio_duration / len(features))
speaker_changes.append((timestamp, current_speaker, label))
current_speaker = label
return speaker_changes, speaker_labels
except:
return [], []
def create_temporal_visualization(features, audio, sr, audio_duration):
"""Cria visualizações temporais das features"""
fig, axes = plt.subplots(4, 1, figsize=(15, 12))
# 1. Waveform com energia
time_axis = np.linspace(0, audio_duration, len(audio))
axes[0].plot(time_axis, audio, alpha=0.7, color='blue', linewidth=0.5)
axes[0].set_title('Forma de Onda do Áudio', fontsize=12, fontweight='bold')
axes[0].set_ylabel('Amplitude')
axes[0].grid(True, alpha=0.3)
# 2. Energia por frame
frame_energy = np.mean(features**2, axis=1)
frame_time = np.linspace(0, audio_duration, len(frame_energy))
axes[1].plot(frame_time, frame_energy, color='red', linewidth=2)
axes[1].set_title('Energia por Frame (Detecção de Pausas)', fontsize=12, fontweight='bold')
axes[1].set_ylabel('Energia')
axes[1].grid(True, alpha=0.3)
# 3. Features Prosódicas (primeiras 16 dimensões)
prosodic_features = features[:, :16]
im1 = axes[2].imshow(prosodic_features.T, aspect='auto', cmap='viridis',
extent=[0, audio_duration, 0, 16])
axes[2].set_title('Features Prosódicas (Entonação, Ritmo)', fontsize=12, fontweight='bold')
axes[2].set_ylabel('Dimensão')
plt.colorbar(im1, ax=axes[2], label='Valor da Feature')
# 4. Features Fonéticas (dimensões 128-144)
phonetic_features = features[:, 128:144]
im2 = axes[3].imshow(phonetic_features.T, aspect='auto', cmap='plasma',
extent=[0, audio_duration, 0, 16])
axes[3].set_title('Features Fonéticas (Fonemas)', fontsize=12, fontweight='bold')
axes[3].set_xlabel('Tempo (s)')
axes[3].set_ylabel('Dimensão')
plt.colorbar(im2, ax=axes[3], label='Valor da Feature')
plt.tight_layout()
return fig
def create_segmentation_visualization(features, audio_duration, phoneme_boundaries,
word_boundaries, speaker_changes):
"""Cria visualização da segmentação temporal"""
fig, ax = plt.subplots(1, 1, figsize=(15, 8))
# Features de energia como base
frame_energy = np.mean(features**2, axis=1)
frame_time = np.linspace(0, audio_duration, len(frame_energy))
ax.plot(frame_time, frame_energy, color='black', alpha=0.7, linewidth=1)
# Marcar fronteiras de fonemas
for i, boundary in enumerate(phoneme_boundaries):
ax.axvline(x=boundary, color='blue', linestyle='--', alpha=0.7)
if i < len(phoneme_boundaries):
ax.text(boundary, max(frame_energy) * 0.9, f'F{i+1}',
rotation=90, fontsize=8, color='blue')
# Marcar pausas (fronteiras de palavras)
for start, end in word_boundaries:
rect = patches.Rectangle((start, 0), end-start, max(frame_energy),
linewidth=0, facecolor='yellow', alpha=0.3)
ax.add_patch(rect)
# Marcar mudanças de falante
for timestamp, old_speaker, new_speaker in speaker_changes:
ax.axvline(x=timestamp, color='red', linestyle='-', linewidth=3, alpha=0.8)
ax.text(timestamp, max(frame_energy) * 0.7, f'S{old_speaker}→S{new_speaker}',
rotation=90, fontsize=10, color='red', fontweight='bold')
ax.set_title('Segmentação Temporal: Fonemas, Palavras e Falantes',
fontsize=14, fontweight='bold')
ax.set_xlabel('Tempo (s)')
ax.set_ylabel('Energia')
ax.grid(True, alpha=0.3)
# Legenda
from matplotlib.lines import Line2D
legend_elements = [
Line2D([0], [0], color='blue', linestyle='--', label='Fronteiras de Fonemas'),
Line2D([0], [0], color='yellow', linewidth=10, alpha=0.3, label='Pausas (Palavras)'),
Line2D([0], [0], color='red', linewidth=3, label='Mudanças de Falante')
]
ax.legend(handles=legend_elements, loc='upper right')
plt.tight_layout()
return fig
def process_audio(audio_file):
"""Processa o arquivo de áudio com análise completa e visualizações"""
if audio_file is None:
return "❌ Por favor, carregue um arquivo de áudio.", None, None
if processor is None or model is None:
return "❌ Erro: Modelo não foi carregado corretamente.", None, None
try:
# Carregar áudio
audio, sr = librosa.load(audio_file, sr=16000)
# Limitar duração
max_duration = 30 # segundos
if len(audio) > max_duration * sr:
audio = audio[:max_duration * sr]
# Processar com o modelo
inputs = processor(audio, sampling_rate=16000, return_tensors="pt", padding=True)
with torch.no_grad():
outputs = model(inputs.input_values)
hidden_states = outputs.last_hidden_state
# Converter para numpy para análise
features = hidden_states.squeeze(0).cpu().numpy()
duration = len(audio) / sr
# Análises temporais detalhadas
phoneme_boundaries, phoneme_types = detect_phoneme_boundaries(features, duration)
word_boundaries = detect_word_boundaries(features, duration)
speaker_changes, speaker_labels = analyze_speaker_changes(features, duration)
# Criar visualizações
temporal_viz = create_temporal_visualization(features, audio, sr, duration)
segmentation_viz = create_segmentation_visualization(
features, duration, phoneme_boundaries, word_boundaries, speaker_changes)
# Análise detalhada de fonemas
phoneme_analysis = ""
if len(phoneme_boundaries) > 0:
phoneme_analysis = "\n## 🗣️ **Análise Temporal de Fonemas**\n"
for i, (boundary, ptype) in enumerate(zip(phoneme_boundaries, phoneme_types)):
phoneme_analysis += f"- **Fonema {i+1}**: {ptype} aos {boundary:.2f}s\n"
# Análise de palavras/pausas
word_analysis = ""
if len(word_boundaries) > 0:
word_analysis = "\n## 📝 **Análise de Palavras/Pausas**\n"
for i, (start, end) in enumerate(word_boundaries):
duration_pause = end - start
word_analysis += f"- **Pausa {i+1}**: {start:.2f}s - {end:.2f}s (duração: {duration_pause:.2f}s)\n"
# Análise de falantes
speaker_analysis = ""
if len(speaker_changes) > 0:
speaker_analysis = "\n## 👥 **Diarização de Falantes**\n"
current_speaker = 0
speaker_analysis += f"- **Início**: Falante {current_speaker}\n"
for timestamp, old_speaker, new_speaker in speaker_changes:
speaker_analysis += f"- **{timestamp:.2f}s**: Mudança de Falante {old_speaker} → Falante {new_speaker}\n"
else:
speaker_analysis = "\n## 👤 **Análise de Falante**\n- Apenas um falante detectado no áudio\n"
# Estatísticas gerais
num_frames = features.shape[0]
frame_rate = num_frames / duration
result = f"""
# 🎵 Análise Completa WavLM-Large com Visualizações
## 📊 **Informações Básicas**
- **Duração**: {duration:.2f} segundos
- **Frames extraídos**: {num_frames}
- **Taxa de frames**: {frame_rate:.1f} frames/segundo
- **Resolução temporal**: {duration/num_frames:.3f}s por frame
## 🔍 **Resumo da Segmentação**
- **Fonemas detectados**: {len(phoneme_boundaries)}
- **Pausas detectadas**: {len(word_boundaries)}
- **Mudanças de falante**: {len(speaker_changes)}
- **Qualidade da análise**: {'✅ Excelente' if num_frames > 100 else '⚠️ Limitada (áudio curto)'}
{phoneme_analysis}
{word_analysis}
{speaker_analysis}
## 📈 **Interpretação das Visualizações**
**Gráfico 1 - Temporal Features:**
- Mostra evolução das características ao longo do tempo
- Prosódia (entonação) e fonemas em tempo real
**Gráfico 2 - Segmentação:**
- Linhas azuis: fronteiras de fonemas
- Áreas amarelas: pausas entre palavras
- Linhas vermelhas: mudanças de falante
## 🎯 **Aplicações Práticas**
- **Transcrição temporal**: Use os timestamps para sincronizar texto
- **Análise prosódica**: Veja padrões de entonação
- **Diarização**: Identifique quem fala quando
- **Segmentação**: Encontre fronteiras naturais da fala
"""
return result, temporal_viz, segmentation_viz
except Exception as e:
return f"❌ Erro ao processar: {str(e)}", None, None
# Interface Gradio com visualizações
iface = gr.Interface(
fn=process_audio,
inputs=gr.Audio(type="filepath", label="📁 Carregar Arquivo de Áudio"),
outputs=[
gr.Textbox(label="📊 Análise Temporal Detalhada", lines=25),
gr.Plot(label="📈 Visualização Temporal das Features"),
gr.Plot(label="🎯 Segmentação: Fonemas, Palavras e Falantes")
],
title="🎵 WavLM-Large: Análise Temporal Avançada",
description="""
**Análise completa com timestamps e visualizações**
✨ **Novidades desta versão:**
• 🗣️ **Timestamps de fonemas** - Quando cada som foi pronunciado
• 📝 **Detecção de pausas** - Fronteiras de palavras/frases
• 👥 **Diarização de falantes** - Quem fala quando
• 📈 **Visualizações temporais** - Gráficos das características
• 🎯 **Segmentação visual** - Mapa temporal completo
Carregue um arquivo de áudio para análise temporal completa!
""",
examples=None,
allow_flagging="never"
)
if __name__ == "__main__":
iface.launch()