Skip to content

Commit 6823d47

Browse files
satraclaude
andauthored
Lab cleanup: voice conversion, linear spec, line plots, single recording (#487)
* Lab cleanup: remove voice conversion, linear spectrogram, line plots - Removed voice conversion section (tangential to lab hypothesis) - Changed spectrograms from mel to linear (better frequency readability) - Removed duplicate PPG timeline plot (was plotted standalone + in aligned panel) - Added "style": "line" option to plot_aligned_panels features panel - Used line style for pitch, loudness, and EMA in comparison plots - Simplified recording to single button (lab needs one recording, not two) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * plot_ppg_phoneme_timeline: add waveform panel aligned with segments When given audio + PPG, the function now plots a two-panel figure: - Top: waveform (amplitude over time) - Bottom: phoneme segments (colored bars) Both share the time axis via sharex=True. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Document style param in docstring, fix incomplete intro text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 448adf4 commit 6823d47

3 files changed

Lines changed: 156 additions & 125 deletions

File tree

src/senselab/audio/tasks/features_extraction/ppg.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -408,14 +408,15 @@ def plot_ppg_phoneme_timeline(
408408
title: str = "PPG phoneme timeline",
409409
show: bool = True,
410410
) -> Figure:
411-
"""Plot a horizontal-bar timeline of PPG phoneme segments.
411+
"""Plot a waveform aligned with a horizontal-bar timeline of PPG phoneme segments.
412412
413-
Each contiguous run of a dominant phoneme is drawn as a coloured
414-
horizontal bar. Only phonemes that actually appear are shown on the
415-
y-axis.
413+
The top panel shows the audio waveform, and the bottom panel shows
414+
each contiguous run of a dominant phoneme as a coloured horizontal bar.
415+
Both panels share the same time axis.
416416
417417
Args:
418-
audio: The source audio used to derive real-time durations.
418+
audio: The source audio used to derive real-time durations and
419+
the waveform display.
419420
posteriorgram: PPG tensor (see :func:`extract_mean_phoneme_durations`
420421
for accepted shapes).
421422
title: Figure title.
@@ -428,6 +429,7 @@ def plot_ppg_phoneme_timeline(
428429
ValueError: If the posteriorgram is empty or contains NaN values.
429430
"""
430431
import matplotlib.pyplot as plt
432+
import numpy as np
431433

432434
if posteriorgram.numel() == 0 or torch.isnan(posteriorgram).any():
433435
raise ValueError("Cannot plot an empty or NaN posteriorgram.")
@@ -448,26 +450,44 @@ def plot_ppg_phoneme_timeline(
448450
cmap = plt.get_cmap("tab20")
449451
num_colors = 20 # tab20 has 20 colours
450452

451-
fig, ax = plt.subplots(figsize=(14, max(3, 0.35 * len(phoneme_order))))
453+
duration = audio.waveform.shape[1] / audio.sampling_rate
454+
seg_height = max(3, 0.35 * len(phoneme_order))
455+
456+
fig, (ax_wav, ax_seg) = plt.subplots(
457+
2,
458+
1,
459+
figsize=(14, 3 + seg_height),
460+
sharex=True,
461+
gridspec_kw={"height_ratios": [1, max(1, seg_height / 3)]},
462+
)
463+
464+
# Waveform panel
465+
waveform_np = audio.waveform.squeeze().cpu().numpy()
466+
time_axis = np.linspace(0, duration, len(waveform_np), endpoint=False)
467+
ax_wav.plot(time_axis, waveform_np, linewidth=0.3, color="steelblue")
468+
ax_wav.set_ylabel("Amplitude")
469+
ax_wav.set_title(title)
470+
ax_wav.grid(True, alpha=0.2)
452471

472+
# Phoneme segment panel
453473
for seg in segments:
454474
y_idx = seen[seg["phoneme"]]
455475
color = cmap(seg["phoneme_index"] % num_colors)
456476
start = seg["start_seconds"]
457477
width = seg["duration_seconds"]
458478

459-
ax.barh(y_idx, width, left=start, height=0.7, color=color, edgecolor="none")
479+
ax_seg.barh(y_idx, width, left=start, height=0.7, color=color, edgecolor="none")
460480

461481
# Onset/offset markers
462-
ax.plot(start, y_idx, "|", color="black", markersize=6, markeredgewidth=0.5)
463-
ax.plot(start + width, y_idx, "|", color="black", markersize=6, markeredgewidth=0.5)
464-
465-
ax.set_yticks(range(len(phoneme_order)))
466-
ax.set_yticklabels(phoneme_order)
467-
ax.set_xlabel("Time (seconds)")
468-
ax.set_ylabel("Phoneme")
469-
ax.set_title(title)
470-
ax.invert_yaxis()
482+
ax_seg.plot(start, y_idx, "|", color="black", markersize=6, markeredgewidth=0.5)
483+
ax_seg.plot(start + width, y_idx, "|", color="black", markersize=6, markeredgewidth=0.5)
484+
485+
ax_seg.set_yticks(range(len(phoneme_order)))
486+
ax_seg.set_yticklabels(phoneme_order, fontsize=7)
487+
ax_seg.set_xlabel("Time (seconds)")
488+
ax_seg.set_ylabel("Phoneme")
489+
ax_seg.set_xlim(0, duration)
490+
ax_seg.invert_yaxis()
471491
fig.tight_layout()
472492

473493
if show:

src/senselab/audio/tasks/plotting/plotting.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,10 +608,14 @@ def _get_spec(mel: bool) -> Tuple[np.ndarray, float, float]:
608608

609609
elif ptype == "features":
610610
data = panel.get("data", [])
611+
style = panel.get("style", "scatter")
611612
for times, values, label, color in data:
612613
t_np = times.cpu().numpy() if torch.is_tensor(times) else np.asarray(times)
613614
v_np = values.cpu().numpy() if torch.is_tensor(values) else np.asarray(values)
614-
ax.scatter(t_np, v_np, s=3, c=color, label=label, alpha=0.7)
615+
if style == "line":
616+
ax.plot(t_np, v_np, color=color, label=label, linewidth=0.8, alpha=0.8)
617+
else:
618+
ax.scatter(t_np, v_np, s=3, c=color, label=label, alpha=0.7)
615619
ax.set_ylabel("Value")
616620
ax.legend(loc="upper right", fontsize=7)
617621
ax.grid(True, alpha=0.3)

0 commit comments

Comments
 (0)