Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b4f005c
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화
seonghobae Aug 9, 2026
ccbb9fe
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 trivy ignore 추가
seonghobae Aug 9, 2026
a922c37
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 trivy ignore/npm audit 의존성 픽스 추가
seonghobae Aug 9, 2026
93dd0f2
fix(security): remove obsolete CVE ignores
seonghobae Aug 11, 2026
c41e5fc
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 trivy ignore/npm audit 의존성 픽스 추가
seonghobae Aug 14, 2026
8730b85
chore(perf): isolate register-overlap optimization
seonghobae Aug 14, 2026
c4f0c58
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 보안 취약점 수정
seonghobae Aug 14, 2026
ab763c3
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 보안 취약점 수정
seonghobae Aug 14, 2026
6b34b09
chore(perf): restore isolated register-overlap slice
seonghobae Aug 14, 2026
5d5b940
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 보안 취약점 수정
seonghobae Aug 14, 2026
8b0ce9e
perf(overlap): restore register optimization to atomic scope
seonghobae Aug 14, 2026
20bdd39
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 보안 취약점 수정
seonghobae Aug 14, 2026
c955077
fix(scope): isolate active-stem overlap optimization
seonghobae Aug 14, 2026
b317318
test(perf): cover register-overlap resource guards
seonghobae Aug 14, 2026
cefa443
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 보안 취약점 수정
seonghobae Aug 14, 2026
36c821b
fix(perf): restore atomic register-overlap scope
seonghobae Aug 14, 2026
95ae2c5
test(overlap): preserve equal-severity band order
seonghobae Aug 15, 2026
930496c
fix(overlap): preserve pre-optimization tie order
seonghobae Aug 15, 2026
b1db384
docs(changelog): record register-overlap optimization
seonghobae Aug 15, 2026
11221cf
test(overlap): keep resource policy out of feature optimization
seonghobae Aug 16, 2026
7a3f566
fix(overlap): keep resource admission in canonical policy
seonghobae Aug 16, 2026
c5f2b49
test(overlap): reject fabricated silent-stem warnings
seonghobae Aug 16, 2026
b55e21c
fix(overlap): prevent threshold edge cases from fabricating warnings
seonghobae Aug 16, 2026
4c08619
test(overlap): cover boolean threshold fail-closed path
seonghobae Aug 16, 2026
171f0ab
docs(changelog): record overlap threshold fail-closed behavior
seonghobae Aug 16, 2026
a559d65
⚡ Bolt: O(1) 레지스터 중복 감지 루프 최적화 및 보안 취약점 수정
seonghobae Aug 16, 2026
05533f2
fix(overlap): restore isolated active-stem filter without local caps
cursoragent Aug 16, 2026
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### Changed

- Reduce register-overlap pair work by comparing only stems that meet the measured occupancy threshold for each register band while preserving deterministic result ordering.

### Fixed

- Keep silent stems from becoming zero-severity rehearsal overlap warnings at a zero threshold, and fail closed on invalid negative, non-finite, or boolean threshold configuration.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
71 changes: 49 additions & 22 deletions services/analysis-engine/src/bandscope_analysis/roles/overlap.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

Security Notes:
- Operates only on in-memory numpy arrays; no file I/O or network access.
- All FFT and reduction operations are bounded by the input array sizes.
- Canonical orchestration owns audio-size, stem-count, memory, CPU/GPU, and
cancellation admission policy before feature analyzers execute.
- Fails safe: empty, silent, or malformed stems produce an empty result and
no exception escapes the public functions.
"""
Expand All @@ -33,6 +34,7 @@
"mid": (250.0, 2000.0),
"high": (2000.0, 8000.0),
}
_BAND_ORDER = {band: index for index, band in enumerate(BANDS)}

# Drums are excluded from pitched-register analysis: percussion is broadband
# (energy is spread across the spectrum by transients and noise), so band
Expand All @@ -51,7 +53,9 @@ def band_energy_profile(
"""Compute the fraction of a stem's spectral energy in each register band.

Energy is the magnitude-squared of the real FFT summed over the bins that
fall inside each band defined in :data:`BANDS`.
fall inside each band defined in :data:`BANDS`. Resource admission is a
canonical orchestration concern; this feature consumes the accepted audio
artifact without inventing a second sample-count ceiling.

Args:
audio: Mono float audio samples for one stem.
Expand Down Expand Up @@ -91,40 +95,63 @@ def detect_register_overlap(
band in which both stems concentrate at least ``threshold`` of their
spectral energy. Drums are excluded (see :data:`UNPITCHED_STEMS`): as a
broadband percussion source they do not occupy a pitched register.
Resource admission is owned by canonical orchestration rather than a
feature-local stem-count ceiling.

Args:
stems: Dict mapping stem names to mono float audio arrays.
sr: Common sample rate in Hz.
threshold: Minimum energy fraction for a stem to occupy a band.
threshold: Minimum energy fraction for a stem to occupy a band. Values
outside the finite ``0.0..1.0`` range fail safe with no overlaps.

Returns:
List of overlap records ``{"stem_a", "stem_b", "band", "severity"}``
where ``severity`` is the smaller of the two energy shares rounded to
two decimals. Pairs are ordered alphabetically (stem_a < stem_b) and
the list is sorted by severity descending. Empty when fewer than two
pitched stems have energy or on any internal failure.
two decimals. Pairs are ordered alphabetically (stem_a < stem_b), the
list is sorted by severity descending, and equal-severity records keep
alphabetical pair order followed by the declared :data:`BANDS` order.
Empty when fewer than two pitched stems have positive band energy, the
threshold is invalid, or any internal failure occurs.
"""
try:
if isinstance(threshold, bool):
return []
threshold_value = float(threshold)
if not np.isfinite(threshold_value) or not 0.0 <= threshold_value <= 1.0:
return []

pitched = sorted(name for name in stems if name not in UNPITCHED_STEMS)
profiles = {name: band_energy_profile(stems[name], sr) for name in pitched}

overlaps: list[dict[str, Any]] = []
for i, stem_a in enumerate(pitched):
for stem_b in pitched[i + 1 :]:
for band in BANDS:
share_a = profiles[stem_a][band]
share_b = profiles[stem_b][band]
if share_a >= threshold and share_b >= threshold:
overlaps.append(
{
"stem_a": stem_a,
"stem_b": stem_b,
"band": band,
"severity": round(min(share_a, share_b), 2),
}
)

overlaps.sort(key=lambda item: -float(item["severity"]))
for band in BANDS:
active_stems = [
(stem, profiles[stem][band])
for stem in pitched
if profiles[stem][band] > 0.0
and profiles[stem][band] >= threshold_value
]
for i, (stem_a, share_a) in enumerate(active_stems):
for stem_b, share_b in active_stems[i + 1 :]:
overlaps.append(
{
"stem_a": stem_a,
"stem_b": stem_b,
"band": band,
"severity": round(min(share_a, share_b), 2),
}
)

# Preserve the pre-optimization stable tie order: alphabetical pairs,
# then the declared register-band order rather than lexical band names.
overlaps.sort(
key=lambda item: (
-float(item["severity"]),
item["stem_a"],
item["stem_b"],
_BAND_ORDER[str(item["band"])],
)
)
return overlaps
except Exception: # pragma: no cover - defensive fail-safe path
logger.warning("Register-overlap detection failed; returning no overlaps.", exc_info=True)
Expand Down
72 changes: 72 additions & 0 deletions services/analysis-engine/tests/test_register_overlap.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from typing import Any

import numpy as np
import pytest
from numpy.typing import NDArray

from bandscope_analysis.roles import overlap as overlap_module
from bandscope_analysis.roles.overlap import (
BANDS,
band_energy_profile,
Expand Down Expand Up @@ -68,6 +70,42 @@ def test_invalid_sample_rate_returns_all_zero(self) -> None:
profile = band_energy_profile(_sine(80.0), 0)
assert profile == {"low": 0.0, "mid": 0.0, "high": 0.0}

def test_feature_does_not_invent_audio_sample_budget(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Leave audio-size admission to the canonical orchestration policy."""

class PolicyOwnedAudio(np.ndarray):
"""Expose a policy-sized logical count without allocating that many samples."""

@property
def size(self) -> int:
"""Return a logical size above the removed feature-local threshold."""
return 100_000_001

audio = np.array([1.0], dtype=np.float64).view(PolicyOwnedAudio)
fft_called = False

def fake_rfft(values: np.ndarray) -> np.ndarray:
"""Prove the feature reaches DSP instead of applying its own admission cap."""
nonlocal fft_called
fft_called = True
assert values.shape == (1,)
return np.array([1.0], dtype=np.float64)

monkeypatch.setattr(np.fft, "rfft", fake_rfft)
monkeypatch.setattr(
np.fft,
"rfftfreq",
lambda _count, d: np.array([100.0 if d > 0 else 0.0], dtype=np.float64),
)

profile = band_energy_profile(audio, SR)

assert fft_called
assert profile == {"low": 1.0, "mid": 0.0, "high": 0.0}


class TestDetectRegisterOverlap:
"""Tests for detect_register_overlap."""
Expand Down Expand Up @@ -110,6 +148,28 @@ def test_single_pitched_stem_returns_empty(self) -> None:
stems = {"bass": _sine(80.0), "drums": _sine(200.0)}
assert detect_register_overlap(stems, SR) == []

def test_feature_does_not_invent_stem_count_budget(
self,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Leave per-job admission limits to the canonical orchestration policy."""
tiny = np.array([0.0], dtype=np.float64)
stems = {f"stem_{index}": tiny for index in range(101)}
profiled: list[str] = []

def fake_profile(_audio: np.ndarray, _sr: int) -> dict[str, float]:
"""Return one active register without doing FFT work."""
profiled.append("stem")
return {"low": 1.0, "mid": 0.0, "high": 0.0}

monkeypatch.setattr(overlap_module, "band_energy_profile", fake_profile)

overlaps = detect_register_overlap(stems, SR)

assert len(profiled) == 101
assert len(overlaps) == 101 * 100 // 2
assert all(overlap["band"] == "low" for overlap in overlaps)

def test_pairs_alphabetical_and_sorted_by_severity(self) -> None:
"""Overlaps are alphabetically paired and sorted by severity desc."""
stems = {
Expand All @@ -135,6 +195,18 @@ def test_multiple_overlaps_sorted_by_severity_descending(self) -> None:
assert all(a < b for a, b in pairs)
assert ("bass", "vocals") in pairs

def test_equal_severity_keeps_declared_band_order(self) -> None:
"""Optimization must preserve the historical band order for severity ties."""
broadband = _sine(100.0) + _sine(500.0) + _sine(3000.0)
overlaps = detect_register_overlap(
{"bass": broadband, "other": broadband.copy()},
SR,
threshold=0.2,
)

assert [overlap["band"] for overlap in overlaps] == list(BANDS)
assert len({overlap["severity"] for overlap in overlaps}) == 1

def test_malformed_stem_values_fail_safe(self) -> None:
"""Non-array stem values are treated as silent, not raised."""
stems: dict[str, Any] = {"bass": None, "other": _sine(80.0)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Threshold safety regressions for register-overlap detection."""

from __future__ import annotations

import numpy as np
import pytest

from bandscope_analysis.roles.overlap import detect_register_overlap


@pytest.mark.parametrize("threshold", [0.0, -0.1, float("-inf")])
def test_silent_stems_never_become_overlap_evidence_at_nonpositive_thresholds(
threshold: float,
) -> None:
"""Silent stems must not fabricate rehearsal warnings under edge thresholds."""
silent = np.zeros(64, dtype=np.float64)

assert (
detect_register_overlap(
{"bass": silent, "other": silent.copy()},
22_050,
threshold=threshold,
)
== []
)


def test_boolean_threshold_fails_closed_instead_of_acting_like_one() -> None:
"""Boolean configuration must not be coerced into a 100% overlap threshold."""
sample_count = 2_205
timeline = np.arange(sample_count, dtype=np.float64) / 22_050
tone = np.sin(2.0 * np.pi * 100.0 * timeline)

assert (
detect_register_overlap(
{"bass": tone, "other": tone.copy()},
22_050,
threshold=True,
)
== []
)
Loading