Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### Fixed

- Kept dependency-controlled pYIN and range-pressure failure payloads and traceback paths out of routine range-analysis logs while preserving bounded operation and exception-class diagnostics.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class PitchTracker:
- No file I/O, network access, or shell execution.
- Bounded computation: frame count capped by input duration.
- Safe failure: exceptions in pYIN return empty range with low confidence.
- pYIN failures log only the operation and exception class; dependency
messages and traceback payloads stay out of routine logs.
"""

def track(self, y: np.ndarray, sr: int = 22050) -> TrackedPitchRange:
Expand All @@ -50,8 +52,8 @@ def track(self, y: np.ndarray, sr: int = 22050) -> TrackedPitchRange:

try:
f0, voiced_flag, voiced_probs = librosa.pyin(y, fmin=fmin, fmax=fmax, sr=sr)
except librosa.util.exceptions.ParameterError as e:
logger.warning("pYIN failed: %s", e)
except librosa.util.exceptions.ParameterError as error:
logger.warning("pYIN failed during pitch tracking: %s", type(error).__name__)
return {"lowest_note": None, "highest_note": None, "confidence": "low"}

# Filter f0 to only keep voiced frames
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
- Bounded computation: all work is linear in the number of input frames.
- Safe failure: malformed, empty, or fully-unvoiced input yields a neutral
default result. No exceptions escape the public functions.
- Unexpected failures log only the operation and exception class; dependency
messages and traceback payloads stay out of routine logs.
"""

import logging
Expand Down Expand Up @@ -184,8 +186,11 @@ def analyze_range_pressure(
"""
try:
return _analyze(f0_hz, voiced_flag, times)
except Exception:
logger.warning("Range-pressure analysis failed; returning default", exc_info=True)
except Exception as error:
logger.warning(
"Range-pressure analysis failed; returning default: %s",
type(error).__name__,
)
return _empty_result()


Expand All @@ -212,8 +217,11 @@ def analyze_range_pressure_from_audio(audio: np.ndarray, sr: int = 22050) -> Ran
fmax = float(librosa.note_to_hz("C8"))
try:
f0, voiced_flag, _voiced_probs = librosa.pyin(audio, fmin=fmin, fmax=fmax, sr=sr)
except librosa.util.exceptions.ParameterError:
logger.warning("pYIN failed during range-pressure analysis", exc_info=True)
except librosa.util.exceptions.ParameterError as error:
logger.warning(
"pYIN failed during range-pressure analysis: %s",
type(error).__name__,
)
return _empty_result()

times = librosa.times_like(f0, sr=sr)
Expand Down
94 changes: 94 additions & 0 deletions services/analysis-engine/tests/test_range_logging_privacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Privacy regressions for range-analysis safe-failure logging."""

from unittest.mock import patch

import librosa
import numpy as np
import pytest

from bandscope_analysis.ranges.pitch_tracker import PitchTracker
from bandscope_analysis.ranges.pressure import (
analyze_range_pressure,
analyze_range_pressure_from_audio,
)

_DEFAULT_PRESSURE = {
"range_semitones": 0,
"tessitura_center": "",
"time_in_top_range": 0.0,
"longest_high_sustain_seconds": 0.0,
"pressure_level": "low",
}


def _assert_payload_safe_log(
caplog: pytest.LogCaptureFixture,
operation: str,
filename: str,
) -> None:
"""Require the operation while rejecting path, file, and secret payloads."""
assert operation in caplog.text
assert "/Users/Alice" not in caplog.text
assert filename not in caplog.text
assert "super-secret" not in caplog.text


def test_pitch_tracker_parameter_error_log_is_payload_safe(
caplog: pytest.LogCaptureFixture,
) -> None:
"""pYIN parameter failures keep dependency payloads out of tracker logs."""
tracker = PitchTracker()
audio = np.ones(2048, dtype=np.float64)
detail = "/Users/Alice/private-pitch.wav token=super-secret"

with patch(
"bandscope_analysis.ranges.pitch_tracker.librosa.pyin",
side_effect=librosa.util.exceptions.ParameterError(detail),
):
result = tracker.track(audio, sr=22050)

assert result == {"lowest_note": None, "highest_note": None, "confidence": "low"}
_assert_payload_safe_log(caplog, "pYIN failed", "private-pitch.wav")


def test_range_pressure_internal_failure_log_is_payload_safe(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Generic range-pressure failures retain no traceback or message payload."""
detail = "/Users/Alice/private-pressure.wav token=super-secret"

def _boom(*_args: object, **_kwargs: object) -> dict[str, object]:
raise RuntimeError(detail)

monkeypatch.setattr("bandscope_analysis.ranges.pressure._analyze", _boom)
values = np.ones(1, dtype=np.float64)
result = analyze_range_pressure(values, np.ones(1, dtype=bool), values)

assert result == _DEFAULT_PRESSURE
_assert_payload_safe_log(
caplog,
"Range-pressure analysis failed; returning default",
"private-pressure.wav",
)


def test_range_pressure_pyin_failure_log_is_payload_safe(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Audio-wrapper pYIN failures keep dependency payloads out of routine logs."""
audio = np.ones(2048, dtype=np.float64)
detail = "/Users/Alice/private-pressure-audio.wav token=super-secret"

with patch(
"bandscope_analysis.ranges.pressure.librosa.pyin",
side_effect=librosa.util.exceptions.ParameterError(detail),
):
result = analyze_range_pressure_from_audio(audio, sr=22050)

assert result == _DEFAULT_PRESSURE
_assert_payload_safe_log(
caplog,
"pYIN failed during range-pressure analysis",
"private-pressure-audio.wav",
)
Loading