Skip to content

feat(analysis): implement DSP-based harmonic and pitch pipelines (#107)#112

Merged
seonghobae merged 2 commits into
developfrom
feature/issue-107-harmonic-pitch
Apr 25, 2026
Merged

feat(analysis): implement DSP-based harmonic and pitch pipelines (#107)#112
seonghobae merged 2 commits into
developfrom
feature/issue-107-harmonic-pitch

Conversation

@seonghobae

Copy link
Copy Markdown
Collaborator

Summary

  • Added ChordRecognizer using librosa's chromagrams to extract chords from stems.
  • Added PitchTracker using librosa's pYIN/YIN to determine pitch ranges.
  • Updated RoleExtractor to dynamically extract ranges and chords when audio stems are provided.
  • Maintained 100% test coverage for the analysis engine.
  • Marked Track 3 (Epic: Harmonic & Melodic Analysis (Chords & Pitch) #107) as completed in the ML Engine Integration Plan.

Testing

Ran uv run pytest --cov resulting in 100% coverage. All librosa components were mocked and edge cases gracefully handled.

- Added ChordRecognizer using librosa's chromagrams to extract chords from stems.
- Added PitchTracker using librosa's pYIN/YIN to determine pitch ranges.
- Updated RoleExtractor to use real stems and these new DSP components to inject actual chords and ranges into roles.
- Reached 100% test coverage with extensive mocking of librosa functions to simulate error conditions.
- Resolved type hinting and formatting issues.
@seonghobae seonghobae enabled auto-merge (squash) March 29, 2026 02:00
@coderabbitai

coderabbitai Bot commented Mar 29, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 오디오에서 화음을 자동으로 인식하는 기능 추가
    • 음성 음역대를 자동으로 추적하는 기능 추가
    • 인식된 화음과 음역대를 역할 생성에 통합하여 더 정확한 음성 분석 제공
  • 문서

    • ML 엔진 통합 Track 3 "Harmonic & Pitch Pipelines" 완료로 표시

워크스루

새로운 PitchTrackerChordRecognizer 모듈을 도입하여 오디오에서 음정 범위와 화음을 추출합니다. RoleExtractor.extract()는 스템 기반 기능 추출을 지원하도록 업데이트되어 역할의 범위 및 화음 값을 동적으로 구동합니다. 계획 문서에서 Track 3을 완료로 표시합니다.

변경사항

코호트 / 파일 요약
음정 추적 및 화음 인식
services/analysis-engine/src/bandscope_analysis/chords/chord_recognizer.py, services/analysis-engine/src/bandscope_analysis/ranges/pitch_tracker.py
TrackedChordTrackedPitchRange TypedDict와 해당 인식기 클래스를 정의합니다. 크로마그램, 음정 추적(pYIN), 템플릿 매칭을 통해 오디오에서 화음 및 음정 범위를 계산합니다.
역할 추출기 통합
services/analysis-engine/src/bandscope_analysis/roles/extractor.py
동적으로 PitchTrackerChordRecognizer를 임포트하여 스템 기반 기능 추출을 지원합니다. 음성 및 베이스 스템에서 음정 범위를 추출하고 베이스/기타 스템에서 화음을 추출하여 역할 정의를 구동합니다. _audio_features 매개변수를 audio_features로 이름 변경합니다.
테스트 스위트
services/analysis-engine/tests/test_chord_recognizer.py, services/analysis-engine/tests/test_pitch_tracker.py, services/analysis-engine/tests/test_roles_ml.py
새로운 모듈의 기능을 검증합니다. 빈 입력, 잡음, 합성 신호 및 오류 조건에 대한 테스트를 포함합니다.
계획 문서 업데이트
docs/plans/2026-03-28-ml-engine-integration.md
Track 3 "Harmonic & Pitch Pipelines (#107)"을 완료로 표시합니다.

시퀀스 다이어그램

sequenceDiagram
    participant RoleExtractor
    participant PitchTracker
    participant ChordRecognizer
    participant librosa

    RoleExtractor->>RoleExtractor: extract() with audio_features + stems
    RoleExtractor->>RoleExtractor: Dynamic import PitchTracker & ChordRecognizer
    
    RoleExtractor->>PitchTracker: track(stems["vocals"], sr=22050)
    PitchTracker->>librosa: pyin() for pitch estimation
    librosa-->>PitchTracker: f0, voiced frames
    PitchTracker-->>RoleExtractor: TrackedPitchRange (vocal_range)
    
    RoleExtractor->>PitchTracker: track(stems["bass"], sr=22050)
    PitchTracker->>librosa: pyin() for pitch estimation
    librosa-->>PitchTracker: f0, voiced frames
    PitchTracker-->>RoleExtractor: TrackedPitchRange (bass_range)
    
    RoleExtractor->>ChordRecognizer: recognize(stems["bass"], sr=22050)
    ChordRecognizer->>librosa: chroma_cqt() + HPSS filtering
    librosa-->>ChordRecognizer: chromagram
    ChordRecognizer-->>RoleExtractor: list[TrackedChord] (bass_chord)
    
    RoleExtractor->>ChordRecognizer: recognize(stems["other"], sr=22050)
    ChordRecognizer->>librosa: chroma_cqt() + HPSS filtering
    librosa-->>ChordRecognizer: chromagram
    ChordRecognizer-->>RoleExtractor: list[TrackedChord] (vocal_chord)
    
    RoleExtractor->>RoleExtractor: Assemble roles with extracted ranges & harmony
    RoleExtractor-->>RoleExtractor: Return RoleExtractionResult
Loading

예상 코드 리뷰 노력

🎯 3 (보통) | ⏱️ ~20분

관련된 PR

  • feat: implement role extraction target and part graph (closes #34) #86: 동일한 RoleExtractor.extract() 파이프라인을 수정합니다. 검색된 PR이 추출기 및 타입을 도입했고, 이번 PR이 PitchTracker/ChordRecognizer를 소비하도록 확장하여 역할의 범위/화음을 구동합니다.
  • feat: add shared rehearsal contract baseline #37: 이번 PR의 RoleExtractor 변경사항(이제 RangeSummary를 임포트/사용하고 음정/화음 출력을 역할의 범위/화음으로 연결)은 검색된 PR(#37)에서 추가된 리허설 계약 타입에 직접 의존합니다.

🐰 음정 추적기, 화음 인식기를 짜맞춰,
스템마다 울려 퍼지는 악보가 생겨,
베이스와 보컬이 제 목소리 찾으면,
역할들도 조화를 이루어 춤을 추네! 🎵

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Pull request title clearly summarizes the main change: implementing DSP-based harmonic (chord) and pitch pipelines using librosa, which matches the core additions of ChordRecognizer and PitchTracker.
Description check ✅ Passed Pull request description is directly related to the changeset, detailing the ChordRecognizer, PitchTracker, and RoleExtractor updates with testing information.
Docstring Coverage ✅ Passed Docstring coverage is 87.10% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-107-harmonic-pitch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plans/2026-03-28-ml-engine-integration.md`:
- Line 20: 제목 줄 "### Track 3: Harmonic & Pitch Pipelines (`#107`) (COMPLETED)" 바로
다음에 빈 줄을 추가하여 Markdown 제목 아래에 공백을 넣어 주세요 (markdownlint MD022 경고 해결). 파일의 해당 헤더
텍스트를 찾아 그 다음 줄에 하나의 빈 줄을 삽입하면 됩니다.

In `@services/analysis-engine/src/bandscope_analysis/chords/chord_recognizer.py`:
- Around line 85-86: librosa.decompose.nn_filter 호출이 try/except 밖에 있어
chromagram이 비정상일 때 전체 처리가 중단될 수 있으니 nn_filter(chromagram, aggregate=np.median,
metric="cosine") 호출을 try/except로 감싸고 예외 발생 시 processLogger 또는 로깅 시스템에 에러를 기록한 뒤
원래의 chromagram을 유지하거나 대체(예: smoothing 건너뛰기)하도록 처리하십시오; 대상 심볼: chromagram 변수와
librosa.decompose.nn_filter, aggregate=np.median, metric="cosine"를 수정하여 안전하게
재할당하거나 예외 시 그대로 반환하도록 구현하세요.
- Around line 118-129: Extract the magic numbers used in the decision block
(max_sim < 0.3, rms_val < 0.01, chroma_var < 0.02) into named constants or class
attributes so they are configurable and documented; for example define
UNVOICED_SIM_THRESHOLD, RMS_THRESHOLD, CHROMA_VAR_THRESHOLD (as module-level
constants or on the ChordRecognizer class as self.UNVOICED_SIM_THRESHOLD) and
replace the inline literals in the chord detection block in chord_recognizer.py,
preserving the current defaults and add a short docstring or comment describing
each threshold and recommended tuning guidance.

In `@services/analysis-engine/src/bandscope_analysis/roles/extractor.py`:
- Around line 68-71: Change the vocal_range fallback so missing notes are
preserved as None instead of an empty string: in extractor.py set "lowestNote":
p_res["lowest_note"] and "highestNote": p_res["highest_note"] (no "or ''"
fallback) so analyzer._parse_note() receives None and can apply explicit
handling; update any callers expecting "" to handle None (or raise) and add a
unit test for missing lowest_note/highest_note to assert explicit rejection or
proper error behavior.

In `@services/analysis-engine/tests/test_chord_recognizer.py`:
- Around line 47-48: There are three consecutive blank lines between top-level
functions in services/analysis-engine/tests/test_chord_recognizer.py; reduce
them to two blank lines to comply with PEP 8 (ensure at most two blank lines
between top-level function and class definitions, e.g., between the surrounding
test functions in this file).
- Line 24: Remove the leftover debug print statement from the test by deleting
the line print("RESULT:", result) in the test function (where variable result is
asserted); ensure the test only uses assertions (no console output) so the test
suite remains clean and deterministic.

In `@services/analysis-engine/tests/test_pitch_tracker.py`:
- Around line 70-73: Remove the excessive empty lines in
tests/test_pitch_tracker.py: there are four consecutive blank lines between two
top-level test functions; conform to PEP 8 by reducing them to at most two blank
lines between the adjacent test functions (i.e., the two surrounding def test_*
functions) so only one or two blank lines remain separating those function
definitions.

In `@services/analysis-engine/tests/test_roles_ml.py`:
- Around line 30-35: The test double side_effect_track returns None for
unmatched inputs which mismatches the real PitchTracker.track() return type (a
TrackedPitchRange dict); update side_effect_track in the test to always return a
dict with the same keys (e.g. {"lowest_note": None, "highest_note": None} or
suitable default note strings) instead of None so the stub matches
PitchTracker.track()’s shape and types.
- Line 1: The test module services/analysis-engine/tests/test_roles_ml.py is
missing a module-level docstring (D100); add a short descriptive docstring at
the top of the file (above the import lines) that states the purpose of the
tests in this file (e.g., what behavior or component the tests cover such as
role-related ML logic) so the module has a proper top-level docstring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 43572482-428f-41a4-8cb7-36d2f02bdc54

📥 Commits

Reviewing files that changed from the base of the PR and between f6cbd3b and 103e6ca.

📒 Files selected for processing (7)
  • docs/plans/2026-03-28-ml-engine-integration.md
  • services/analysis-engine/src/bandscope_analysis/chords/chord_recognizer.py
  • services/analysis-engine/src/bandscope_analysis/ranges/pitch_tracker.py
  • services/analysis-engine/src/bandscope_analysis/roles/extractor.py
  • services/analysis-engine/tests/test_chord_recognizer.py
  • services/analysis-engine/tests/test_pitch_tracker.py
  • services/analysis-engine/tests/test_roles_ml.py

- **Output**: 4 or 6 discrete stems (vocals, bass, drums, other).

### Track 3: Harmonic & Pitch Pipelines (#107)
### Track 3: Harmonic & Pitch Pipelines (#107) (COMPLETED)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Markdown 포맷팅: 제목 아래 빈 줄 필요

정적 분석 도구(markdownlint MD022)에서 제목 아래에 빈 줄이 필요하다고 경고합니다.

📝 포맷팅 수정 제안
 ### Track 3: Harmonic & Pitch Pipelines (`#107`) (COMPLETED)
+
 - **Goal**: Replace hardcoded `C#m7` strings with DSP-derived chord and pitch arrays.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Track 3: Harmonic & Pitch Pipelines (#107) (COMPLETED)
### Track 3: Harmonic & Pitch Pipelines (`#107`) (COMPLETED)
- **Goal**: Replace hardcoded `C#m7` strings with DSP-derived chord and pitch arrays.
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 20-20: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-28-ml-engine-integration.md` at line 20, 제목 줄 "### Track
3: Harmonic & Pitch Pipelines (`#107`) (COMPLETED)" 바로 다음에 빈 줄을 추가하여 Markdown 제목
아래에 공백을 넣어 주세요 (markdownlint MD022 경고 해결). 파일의 해당 헤더 텍스트를 찾아 그 다음 줄에 하나의 빈 줄을
삽입하면 됩니다.

Comment on lines +85 to +86
# Optional: apply temporal smoothing to chromagram to reduce noise
chromagram = librosa.decompose.nn_filter(chromagram, aggregate=np.median, metric="cosine")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

nn_filter 호출에 대한 예외 처리 누락

librosa.decompose.nn_filter 호출이 try/except 블록 외부에 있습니다. chromagram이 비정상적인 형태일 경우 예외가 발생할 수 있으며, 이는 전체 인식 과정을 실패시킬 수 있습니다.

🛡️ 예외 처리 추가 제안
         # Optional: apply temporal smoothing to chromagram to reduce noise
-        chromagram = librosa.decompose.nn_filter(chromagram, aggregate=np.median, metric="cosine")
+        try:
+            chromagram = librosa.decompose.nn_filter(chromagram, aggregate=np.median, metric="cosine")
+        except Exception:
+            pass  # Use unsmoothed chromagram if filtering fails
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/analysis-engine/src/bandscope_analysis/chords/chord_recognizer.py`
around lines 85 - 86, librosa.decompose.nn_filter 호출이 try/except 밖에 있어
chromagram이 비정상일 때 전체 처리가 중단될 수 있으니 nn_filter(chromagram, aggregate=np.median,
metric="cosine") 호출을 try/except로 감싸고 예외 발생 시 processLogger 또는 로깅 시스템에 에러를 기록한 뒤
원래의 chromagram을 유지하거나 대체(예: smoothing 건너뛰기)하도록 처리하십시오; 대상 심볼: chromagram 변수와
librosa.decompose.nn_filter, aggregate=np.median, metric="cosine"를 수정하여 안전하게
재할당하거나 예외 시 그대로 반환하도록 구현하세요.

Comment on lines +118 to +129
# Simple threshold for unvoiced/noise (if max similarity is very low)
max_sim = similarity[match, i]
rms_val = rms[i] if i < len(rms) else 0.0

# For noise, the max similarity is usually lower, but to be robust
# we should check if the chromagram is too flat (e.g. low variance)
# or if the RMS energy is really low.
# However, since dot product normalization makes noise match *something*,
# we can look at the variance of the chromagram frame.
chroma_var = np.var(chromagram[:, i])
if max_sim < 0.3 or rms_val < 0.01 or chroma_var < 0.02:
chord_label = "N"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

매직 넘버에 대한 문서화 권장

max_sim < 0.3, rms_val < 0.01, chroma_var < 0.02 등의 임계값이 하드코딩되어 있습니다. 이 값들은 도메인별 튜닝이 필요할 수 있으므로, 클래스 속성이나 상수로 정의하고 문서화하는 것이 좋습니다.

♻️ 상수 추출 제안
 class ChordRecognizer:
     """Extracts chords from audio data."""

+    # Noise detection thresholds (may require tuning per domain)
+    SIMILARITY_THRESHOLD = 0.3
+    RMS_THRESHOLD = 0.01
+    CHROMA_VARIANCE_THRESHOLD = 0.02
+
     def __init__(self) -> None:
-            if max_sim < 0.3 or rms_val < 0.01 or chroma_var < 0.02:
+            if (max_sim < self.SIMILARITY_THRESHOLD or
+                rms_val < self.RMS_THRESHOLD or
+                chroma_var < self.CHROMA_VARIANCE_THRESHOLD):
                 chord_label = "N"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/analysis-engine/src/bandscope_analysis/chords/chord_recognizer.py`
around lines 118 - 129, Extract the magic numbers used in the decision block
(max_sim < 0.3, rms_val < 0.01, chroma_var < 0.02) into named constants or class
attributes so they are configurable and documented; for example define
UNVOICED_SIM_THRESHOLD, RMS_THRESHOLD, CHROMA_VAR_THRESHOLD (as module-level
constants or on the ChordRecognizer class as self.UNVOICED_SIM_THRESHOLD) and
replace the inline literals in the chord detection block in chord_recognizer.py,
preserving the current defaults and add a short docstring or comment describing
each threshold and recommended tuning guidance.

Comment on lines +68 to +71
vocal_range = {
"lowestNote": p_res["lowest_note"] or "",
"highestNote": p_res["highest_note"] or "",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how lowestNote/highestNote are used downstream

rg -n "lowestNote|highestNote" --type py -A 3

Repository: seonghobae/bandscope

Length of output: 10109


🏁 Script executed:

# Find the _overlap_severity and _ranges_overlap functions to see how they handle the values
rg -n "_overlap_severity|_ranges_overlap" --type py -A 5 services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py

Repository: seonghobae/bandscope

Length of output: 1053


🏁 Script executed:

# Check for any validation patterns around lowestNote/highestNote usage
rg -n "lowestNote|highestNote" services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py -B 3 | head -50

Repository: seonghobae/bandscope

Length of output: 1021


🏁 Script executed:

# Get the full implementation of _ranges_overlap and _overlap_severity functions
rg -n "_ranges_overlap|_overlap_severity" services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py -A 15 | head -80

Repository: seonghobae/bandscope

Length of output: 2252


🏁 Script executed:

# Check test files for empty string test cases related to ranges
rg -n '"".*lowestNote|"".*highestNote|lowestNote.*""' services/analysis-engine/tests/ --type py

Repository: seonghobae/bandscope

Length of output: 46


🏁 Script executed:

# Find _note_to_midi function and see how it handles input
rg -n "_note_to_midi" services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py -A 8

Repository: seonghobae/bandscope

Length of output: 1310


🏁 Script executed:

# Check imports at the top of the analyzer.py file
head -30 services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py

Repository: seonghobae/bandscope

Length of output: 552


🏁 Script executed:

# Get complete implementation of _note_to_midi function
sed -n '73,107p' services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py

Repository: seonghobae/bandscope

Length of output: 774


🏁 Script executed:

# Check the full context around where _ranges_overlap is called
sed -n '200,240p' services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py

Repository: seonghobae/bandscope

Length of output: 1829


🏁 Script executed:

# Find _parse_note function
rg -n "_parse_note" services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py -A 10

Repository: seonghobae/bandscope

Length of output: 787


🏁 Script executed:

# Get full implementation of _parse_note
sed -n '39,71p' services/analysis-engine/src/bandscope_analysis/ranges/analyzer.py

Repository: seonghobae/bandscope

Length of output: 1363


빈 문자열 기본값이 조용히 C4로 변환되어 부정확한 범위 감지를 초래할 수 있습니다

lowest_noteNone일 때 빈 문자열로 fallback하면, 범위 분석 함수의 _parse_note()에서 이를 감지하고 중음(C4)의 기본값으로 변환합니다(line 55 in analyzer.py). 이 조용한 기본값 변환은:

  • 실제 데이터 누락을 감지할 수 없게 함
  • 여러 범위가 C4로 기본값이 되면 거짓 양성 겹침 감지 가능
  • 불완전한 입력에 대한 테스트 커버리지 부재

None 값을 명시적으로 거부하거나 기본값 동작에 대한 테스트를 추가하는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/analysis-engine/src/bandscope_analysis/roles/extractor.py` around
lines 68 - 71, Change the vocal_range fallback so missing notes are preserved as
None instead of an empty string: in extractor.py set "lowestNote":
p_res["lowest_note"] and "highestNote": p_res["highest_note"] (no "or ''"
fallback) so analyzer._parse_note() receives None and can apply explicit
handling; update any callers expecting "" to handle None (or raise) and add a
unit test for missing lowest_note/highest_note to assert explicit rejection or
proper error behavior.

np.random.seed(42)
y = np.random.randn(22050 * 2) * 0.1
result = recognizer.recognize(y, sr=22050)
print("RESULT:", result)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

디버그 print 문 제거 필요

테스트 코드에 디버그용 print 문이 남아 있습니다. 프로덕션 테스트 스위트에서는 제거해야 합니다.

🧹 print 문 제거
     result = recognizer.recognize(y, sr=22050)
-    print("RESULT:", result)
     # Could be N (No chord) or empty
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/analysis-engine/tests/test_chord_recognizer.py` at line 24, Remove
the leftover debug print statement from the test by deleting the line
print("RESULT:", result) in the test function (where variable result is
asserted); ensure the test only uses assertions (no console output) so the test
suite remains clean and deterministic.

Comment on lines +47 to +48


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

연속된 빈 줄 정리

함수 사이에 3개의 빈 줄이 있습니다. PEP 8 권장에 따라 2개로 줄이는 것이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/analysis-engine/tests/test_chord_recognizer.py` around lines 47 -
48, There are three consecutive blank lines between top-level functions in
services/analysis-engine/tests/test_chord_recognizer.py; reduce them to two
blank lines to comply with PEP 8 (ensure at most two blank lines between
top-level function and class definitions, e.g., between the surrounding test
functions in this file).

Comment on lines +70 to +73




Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

불필요한 빈 줄 제거

연속된 4개의 빈 줄이 있습니다. PEP 8 권장에 따라 함수 사이에는 2개의 빈 줄이 적절합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/analysis-engine/tests/test_pitch_tracker.py` around lines 70 - 73,
Remove the excessive empty lines in tests/test_pitch_tracker.py: there are four
consecutive blank lines between two top-level test functions; conform to PEP 8
by reducing them to at most two blank lines between the adjacent test functions
(i.e., the two surrounding def test_* functions) so only one or two blank lines
remain separating those function definitions.

@@ -0,0 +1,123 @@
from unittest.mock import patch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

파이프라인 실패: 모듈 docstring 누락 (D100)

CI 파이프라인에서 ruff check가 실패했습니다. 모듈 수준 docstring을 추가해야 합니다.

📝 docstring 추가 제안
+"""Tests for RoleExtractor with audio-driven feature extraction."""
+
 from unittest.mock import patch
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from unittest.mock import patch
"""Tests for RoleExtractor with audio-driven feature extraction."""
from unittest.mock import patch
🧰 Tools
🪛 GitHub Actions: ci

[error] 1-1: ruff check (D100): Missing docstring in public module

🪛 GitHub Actions: release

[error] 1-1: ruff check failed: D100 Missing docstring in public module. (Found 1 error.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/analysis-engine/tests/test_roles_ml.py` at line 1, The test module
services/analysis-engine/tests/test_roles_ml.py is missing a module-level
docstring (D100); add a short descriptive docstring at the top of the file
(above the import lines) that states the purpose of the tests in this file
(e.g., what behavior or component the tests cover such as role-related ML logic)
so the module has a proper top-level docstring.

Comment on lines +30 to +35
def side_effect_track(y, sr):
if y is vocals_stem:
return {"lowest_note": "A3", "highest_note": "A4"}
elif y is bass_stem:
return {"lowest_note": "E1", "highest_note": "E2"}
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

side_effect 함수에서 None 반환 시 타입 불일치 가능

side_effect_track 함수가 매칭되지 않는 경우 None을 반환하지만, 실제 PitchTracker.track()은 항상 TrackedPitchRange dict를 반환합니다. 이로 인해 테스트가 실제 동작과 다른 시나리오를 검증할 수 있습니다.

♻️ 일관된 반환 타입 제안
         def side_effect_track(y, sr):
             if y is vocals_stem:
                 return {"lowest_note": "A3", "highest_note": "A4", "confidence": "high"}
             elif y is bass_stem:
                 return {"lowest_note": "E1", "highest_note": "E2", "confidence": "high"}
-            return None
+            return {"lowest_note": None, "highest_note": None, "confidence": "low"}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/analysis-engine/tests/test_roles_ml.py` around lines 30 - 35, The
test double side_effect_track returns None for unmatched inputs which mismatches
the real PitchTracker.track() return type (a TrackedPitchRange dict); update
side_effect_track in the test to always return a dict with the same keys (e.g.
{"lowest_note": None, "highest_note": None} or suitable default note strings)
instead of None so the stub matches PitchTracker.track()’s shape and types.

@seonghobae seonghobae merged commit 103e6ca into develop Apr 25, 2026
21 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant