File size: 5,890 Bytes
d9af7ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""
Visualization generation for BLT patching explorer.
"""

import numpy as np
import librosa
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from typing import List

from .audio import load_audio_file, TOKENS_PER_SECOND


def compute_spectrogram(audio_path: str, sr: int | None = None):
    """
    Compute mel-spectrogram from audio file.

    Args:
        audio_path: Path to audio file
        sr: Optional sample rate override. When None, uses the file's native rate.

    Returns:
        times: Time points (seconds)
        frequencies: Frequency bins (Hz)
        magnitudes: 2D array (freq x time) in dB
    """
    file_sr, audio = load_audio_file(audio_path)
    sr = sr or file_sr

    n_fft = 1024
    hop_length = 64
    n_mels = 256

    S = librosa.feature.melspectrogram(
        y=audio,
        sr=sr,
        n_fft=n_fft,
        hop_length=hop_length,
        n_mels=n_mels,
        fmax=8000
    )
    S_db = librosa.power_to_db(S, ref=np.max)

    times = librosa.frames_to_time(
        np.arange(S.shape[1]), sr=sr, hop_length=hop_length
    )
    frequencies = librosa.mel_frequencies(n_mels=n_mels, fmax=8000)

    return times, frequencies, S_db


def create_visualization(
    data: dict,
    patches: List[List[int]],
    metric_name: str,
    threshold: float
):
    """
    Create 4-panel synchronized visualization.

    Args:
        data: Precomputed data dict
        patches: Patch boundaries
        metric_name: "entropy" or "perplexity"
        threshold: Threshold value

    Returns:
        Plotly figure
    """
    import time
    start_time = time.time()
    print(f"\n[Visualization] Starting for {data['file_id']}")
    print(f"  Patches: {len(patches)}, Metric: {metric_name}, Threshold: {threshold}")

    fig = make_subplots(
        rows=3, cols=1,
        subplot_titles=(
            'Spectrogram',
            f'{metric_name.capitalize()} + Patch Boundaries',
            'Patch Size Distribution'
        ),
        specs=[
            [{"type": "heatmap"}],
            [{"type": "scatter"}],
            [{"type": "bar"}]
        ],
        vertical_spacing=0.1,
        row_heights=[0.4, 0.4, 0.2]
    )

    # Get metric data
    metric = np.array(
        data['entropies'] if metric_name == 'entropy' else data['perplexities']
    )

    # Calculate patch boundary times
    boundary_times = []
    for patch in patches:
        if patch:
            boundary_times.append(patch[0] / TOKENS_PER_SECOND)

    duration = data['audio_duration_sec']

    # Limit vlines to avoid slowdown
    MAX_VLINES = 200
    draw_boundaries = len(boundary_times) <= MAX_VLINES

    if draw_boundaries:
        print(f"  Will draw {len(boundary_times)} patch boundaries")
    else:
        print(f"  Skipping {len(boundary_times)} boundaries (too many, would be slow)")

    # 1. Spectrogram
    spec_start = time.time()
    times, freqs, mags = compute_spectrogram(data['audio_path'])
    print(f"  Spectrogram computed in {time.time() - spec_start:.2f}s")

    fig.add_trace(
        go.Heatmap(
            x=times,
            y=freqs,
            z=mags,
            colorscale='Viridis',
            showscale=True,
            colorbar=dict(title="dB", x=1.05)
        ),
        row=1, col=1
    )

    # Add patch boundaries to spectrogram
    if draw_boundaries:
        for t in boundary_times:
            fig.add_vline(
                x=t, line_width=2, line_dash="solid",
                line_color="red", opacity=0.7,
                row=1, col=1
            )

    fig.update_xaxes(title_text="Time (s)", row=1, col=1, range=[0, duration])
    fig.update_yaxes(title_text="Frequency (Hz)", row=1, col=1)

    # 2. Entropy/Perplexity plot
    token_positions = np.arange(len(metric))

    fig.add_trace(
        go.Scatter(
            x=token_positions,
            y=metric,
            mode='lines+markers',
            name=metric_name.capitalize(),
            line=dict(color='blue', width=2),
            marker=dict(size=3)
        ),
        row=2, col=1
    )

    # Add threshold line
    fig.add_hline(
        y=threshold, line_width=2, line_dash="dash",
        line_color="red", opacity=0.7,
        annotation_text=f"Threshold: {threshold:.2f}",
        annotation_position="right",
        row=2, col=1
    )

    # Add patch boundaries
    if draw_boundaries:
        for patch in patches:
            if patch:
                fig.add_vline(
                    x=patch[0], line_width=1, line_dash="solid",
                    line_color="gray", opacity=0.4,
                    row=2, col=1
                )

    fig.update_xaxes(title_text="Token Position", row=2, col=1, range=[0, len(metric)])
    fig.update_yaxes(title_text=metric_name.capitalize(), row=2, col=1)

    # 3. Patch size distribution
    sizes = [len(p) for p in patches]
    size_counts = {}
    for size in sizes:
        size_counts[size] = size_counts.get(size, 0) + 1

    sorted_sizes = sorted(size_counts.keys())
    sorted_counts = [size_counts[s] for s in sorted_sizes]

    fig.add_trace(
        go.Bar(
            x=sorted_sizes,
            y=sorted_counts,
            name='Patch Sizes',
            marker_color='steelblue',
            hovertemplate='Size: %{x} tokens<br>Count: %{y}<extra></extra>'
        ),
        row=3, col=1
    )

    fig.update_xaxes(title_text="Patch Size (tokens)", row=3, col=1)
    fig.update_yaxes(title_text="Count", row=3, col=1)

    # Update layout
    fig.update_layout(
        height=1200,
        showlegend=False,
        hovermode='closest',
        title_text=f"BLT Patching Visualization - {data['file_id']}"
    )

    total_time = time.time() - start_time
    print(f"  Total visualization time: {total_time:.2f}s")
    if not draw_boundaries:
        print(f"  ⚠️  Note: Patch boundaries not drawn (>{MAX_VLINES} patches)")

    return fig