Skip to content
This repository was archived by the owner on Aug 11, 2026. It is now read-only.

Commit c90e30e

Browse files
committed
fix: calculate accurate spectrogram time_resolution from audio recording database
Resolves Issue #51 - Spectrogram time_resolution was hardcoded to 0.0116 s/frame, causing 23% error in duration calculations and misaligned time-based cropping. Changes: - Add SwaraClient.get_audio_recording() to fetch recording metadata from /getAudioRecording endpoint - Update SpectrogramData.__init__() to accept optional time_resolution parameter - Update SpectrogramData.from_audio_id() to calculate time_resolution from recording.duration - Update time_resolution property to return calculated value instead of hardcoded constant - Add DEFAULT_TIME_RESOLUTION (0.015080) as improved fallback when DB unavailable Implementation: - Spectrograms always cover full audio recording (not just transcribed excerpts) - time_resolution = recording.duration / spectrogram_time_frames - Graceful fallback to DEFAULT_TIME_RESOLUTION if recording metadata unavailable - All 36 existing tests pass without modification Verification: - Test recording (2192.45s): Previously calculated as 1683.88s (508s error) - After fix: Perfectly matches DB duration (0.000000s error) - Accuracy improved from 77% to 100%
1 parent 9c39e84 commit c90e30e

2 files changed

Lines changed: 49 additions & 8 deletions

File tree

idtap/client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,28 @@ def download_spectrogram_metadata(self, audio_id: str) -> Dict[str, Any]:
708708
endpoint = f"spec_data/{audio_id}/spec_shape.json"
709709
return self._get(endpoint)
710710

711+
def get_audio_recording(self, audio_id: str) -> Dict[str, Any]:
712+
"""Get audio recording metadata by ID.
713+
714+
Fetches complete recording metadata including duration, musicians,
715+
ragas, location, and permissions.
716+
717+
Args:
718+
audio_id: The audio recording ID
719+
720+
Returns:
721+
Dictionary with recording metadata including:
722+
- duration: Audio duration in seconds (float)
723+
- musicians: Dictionary of performer information
724+
- raags: Dictionary of raga information
725+
- title: Recording title
726+
- etc.
727+
728+
Raises:
729+
requests.HTTPError: If recording not found (404)
730+
"""
731+
return self._get("getAudioRecording", params={"_id": audio_id})
732+
711733
def save_transcription(self, piece: Piece, fill_duration: bool = True) -> Any:
712734
"""Save a transcription piece to the server.
713735

idtap/spectrogram.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,21 @@ class SpectrogramData:
4949
# Constants matching web app implementation
5050
DEFAULT_FREQ_RANGE = (75.0, 2400.0) # Hz
5151
DEFAULT_BINS_PER_OCTAVE = 72
52+
DEFAULT_TIME_RESOLUTION = 0.015080 # seconds per frame (fallback when DB unavailable)
5253

5354
def __init__(self, data: np.ndarray, audio_id: str,
5455
freq_range: Tuple[float, float] = DEFAULT_FREQ_RANGE,
55-
bins_per_octave: int = DEFAULT_BINS_PER_OCTAVE):
56+
bins_per_octave: int = DEFAULT_BINS_PER_OCTAVE,
57+
time_resolution: Optional[float] = None):
5658
"""Initialize SpectrogramData with raw data.
5759
5860
Args:
5961
data: Raw uint8 spectrogram array [freq_bins, time_frames]
6062
audio_id: Audio recording ID
6163
freq_range: Frequency range (min_hz, max_hz)
6264
bins_per_octave: Number of frequency bins per octave
65+
time_resolution: Time resolution in seconds per frame (optional)
66+
If None, uses DEFAULT_TIME_RESOLUTION fallback
6367
"""
6468
if not isinstance(data, np.ndarray):
6569
raise TypeError(f"data must be numpy array, got {type(data)}")
@@ -72,19 +76,21 @@ def __init__(self, data: np.ndarray, audio_id: str,
7276
self.audio_id = audio_id
7377
self.freq_range = freq_range
7478
self.bins_per_octave = bins_per_octave
79+
self._time_resolution = time_resolution if time_resolution is not None else self.DEFAULT_TIME_RESOLUTION
7580

7681
@classmethod
7782
def from_audio_id(cls, audio_id: str, client: Optional['SwaraClient'] = None) -> 'SpectrogramData':
7883
"""Download and load spectrogram data from audio ID.
7984
8085
Fetches compressed spectrogram data from https://swara.studio/spec_data/{audio_id}/
86+
and calculates accurate time_resolution from the audio recording duration in the database.
8187
8288
Args:
8389
audio_id: IDTAP audio recording ID
8490
client: Optional SwaraClient instance (creates one if not provided)
8591
8692
Returns:
87-
SpectrogramData instance
93+
SpectrogramData instance with accurate time_resolution
8894
8995
Raises:
9096
requests.HTTPError: If spectrogram data doesn't exist or download fails
@@ -105,7 +111,19 @@ def from_audio_id(cls, audio_id: str, client: Optional['SwaraClient'] = None) ->
105111
shape = tuple(metadata['shape']) # [freq_bins, time_frames]
106112
data = np.frombuffer(decompressed, dtype=np.uint8).reshape(shape)
107113

108-
return cls(data, audio_id)
114+
# Get exact audio duration from recording database
115+
time_resolution = None
116+
try:
117+
recording = client.get_audio_recording(audio_id)
118+
audio_duration = recording['duration']
119+
time_frames = shape[1]
120+
time_resolution = audio_duration / time_frames
121+
except Exception:
122+
# Fallback to DEFAULT_TIME_RESOLUTION if recording not found
123+
# This will be handled by __init__
124+
pass
125+
126+
return cls(data, audio_id, time_resolution=time_resolution)
109127

110128
@classmethod
111129
def from_piece(cls, piece: 'Piece', client: Optional['SwaraClient'] = None) -> Optional['SpectrogramData']:
@@ -501,12 +519,13 @@ def duration(self) -> float:
501519
def time_resolution(self) -> float:
502520
"""Time resolution in seconds per frame.
503521
504-
Estimated based on typical CQT parameters for audio sampling.
522+
Calculated from audio recording duration in database (when available).
523+
Falls back to DEFAULT_TIME_RESOLUTION if recording metadata unavailable.
524+
525+
Note: Spectrograms always cover the full audio recording, even when
526+
the associated Piece transcribes only an excerpt.
505527
"""
506-
# Typical hop size for CQT is around 0.01s per frame
507-
# This is an approximation - exact value depends on sample rate and hop length
508-
# For 44100 Hz sample rate with hop_length=512: 512/44100 ≈ 0.0116s
509-
return 0.0116 # seconds per frame (approximate)
528+
return self._time_resolution
510529

511530
@property
512531
def freq_bins(self) -> np.ndarray:

0 commit comments

Comments
 (0)