Spaces:
Sleeping
Sleeping
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() |