Skip to content
Merged
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
1 change: 1 addition & 0 deletions services/analysis-engine/src/bandscope_analysis/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class RehearsalRolePayload(TypedDict):
simplification: str
setupNote: str
manualOverrides: list[ManualOverridePayload]
overlapWarnings: list[str]


class PartGraphNodePayload(TypedDict):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,35 @@ def extract(
"simplification": "Stay on roots if the chorus entrance gets muddy.",
"setupNote": "Keep the attack short so the verse breathes.",
"manualOverrides": [],
"overlapWarnings": [
"Density warning: competing with Keyboard Left Hand in low register."
],
}

keys_left_role: RehearsalRole = {
"id": "keys-left",
"name": "Keyboard 1 Left Hand",
"roleType": RoleType.HAND,
"harmony": {
"chord": "C#",
"functionLabel": "Root reinforcement",
"source": "model",
},
"cue": {
"kind": CueAnchorKind.TRANSITION,
"value": "Lock in with bass pedal.",
},
"range": {"lowestNote": "C#2", "highestNote": "C#3"},
"confidence": {
"level": "low",
"source": "model",
"notes": "Muddy frequency range, difficult to clearly separate from bass.",
},
"rehearsalPriority": RehearsalPriority.MEDIUM,
"simplification": "Omit if bass is covering the lower register.",
"setupNote": "Use a darker patch to avoid clashing with right hand.",
"manualOverrides": [],
"overlapWarnings": ["Density warning: competing with Bass Guitar in low register."],
}

keys_role: RehearsalRole = {
Expand All @@ -98,6 +127,7 @@ def extract(
"simplification": "Drop top extension if the chorus turnaround feels busy.",
"setupNote": "Keep the patch bright enough to stay over the guitars.",
"manualOverrides": [],
"overlapWarnings": ["Melodic overlap: top notes conflict with Lead Vocal range."],
}

vocal_role: RehearsalRole = {
Expand Down Expand Up @@ -130,6 +160,7 @@ def extract(
"source": "user",
}
],
"overlapWarnings": ["Melodic overlap: competing with Keyboard 1 Right Hand."],
}

active_roles = [bass_role]
Expand All @@ -140,9 +171,15 @@ def extract(
]

if i == 0:
active_roles.extend([keys_role, vocal_role])
active_roles.extend([keys_left_role, keys_role, vocal_role])
part_graph.extend(
[
{
"role_id": "keys-left",
"is_active": True,
"handoff_to": [],
"handoff_from": [],
},
{
"role_id": "keys-right",
"is_active": True,
Expand All @@ -158,10 +195,16 @@ def extract(
]
)
part_graph[0]["handoff_to"].append("lead-vocal")
part_graph[2]["handoff_from"].append("bass-guitar")
part_graph[3]["handoff_from"].append("bass-guitar")
else:
part_graph.extend(
[
{
"role_id": "keys-left",
"is_active": False,
"handoff_to": [],
"handoff_from": [],
},
{
"role_id": "keys-right",
"is_active": False,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class RehearsalRole(TypedDict):
simplification: str
setupNote: str
manualOverrides: list[ManualOverride]
overlapWarnings: list[str]


class PartGraphNode(TypedDict):
Expand Down
2 changes: 1 addition & 1 deletion services/analysis-engine/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ def test_build_demo_rehearsal_song_matches_expected_fixture() -> None:

assert song["title"] == "Late Night Set"
assert song["sections"][0]["roles"][0]["id"] == "bass-guitar"
assert song["sections"][0]["roles"][2]["manualOverrides"][0]["value"]["source"] == "user"
assert song["sections"][0]["roles"][3]["manualOverrides"][0]["value"]["source"] == "user"


def test_run_analysis_job_returns_success_and_failure_envelopes() -> None:
Expand Down
41 changes: 23 additions & 18 deletions services/analysis-engine/tests/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,28 @@
)


def test_role_type_enum():
def test_role_type_enum() -> None:
"""Verify RoleType enum values match the domain requirements."""
assert RoleType.INSTRUMENT == "instrument"
assert RoleType.VOCAL == "vocal"
assert RoleType.HAND == "hand"
assert RoleType.INSTRUMENT.value == "instrument"
assert RoleType.VOCAL.value == "vocal"
assert RoleType.HAND.value == "hand"


def test_rehearsal_priority_enum():
def test_rehearsal_priority_enum() -> None:
"""Verify RehearsalPriority enum values match."""
assert RehearsalPriority.LOW == "low"
assert RehearsalPriority.MEDIUM == "medium"
assert RehearsalPriority.HIGH == "high"
assert RehearsalPriority.LOW.value == "low"
assert RehearsalPriority.MEDIUM.value == "medium"
assert RehearsalPriority.HIGH.value == "high"


def test_cue_anchor_kind_enum():
def test_cue_anchor_kind_enum() -> None:
"""Verify CueAnchorKind enum values match."""
assert CueAnchorKind.LYRIC == "lyric"
assert CueAnchorKind.COUNT == "count"
assert CueAnchorKind.TRANSITION == "transition"
assert CueAnchorKind.LYRIC.value == "lyric"
assert CueAnchorKind.COUNT.value == "count"
assert CueAnchorKind.TRANSITION.value == "transition"


def test_role_extractor_basic():
def test_role_extractor_basic() -> None:
"""Test that RoleExtractor returns a valid topology structure."""
extractor = RoleExtractor()

Expand All @@ -44,13 +44,15 @@ def test_role_extractor_basic():
# Check intro section
intro_topology = result["topologies"][0]
assert intro_topology["section_id"] == "intro"
assert len(intro_topology["active_roles"]) == 3
assert len(intro_topology["active_roles"]) == 4

roles_by_id = {r["id"]: r for r in intro_topology["active_roles"]}
assert "bass-guitar" in roles_by_id
assert "lead-vocal" in roles_by_id
assert "keys-right" in roles_by_id
assert "keys-left" in roles_by_id
assert roles_by_id["lead-vocal"]["roleType"] == "vocal"
assert "Melodic overlap" in roles_by_id["lead-vocal"]["overlapWarnings"][0]

intro_graph = intro_topology["part_graph"]
graph_by_role = {n["role_id"]: n for n in intro_graph}
Expand All @@ -66,23 +68,26 @@ def test_role_extractor_basic():
assert verse_topology["active_roles"][0]["id"] == "bass-guitar"
assert verse_topology["active_roles"][0]["roleType"] == "instrument"
assert verse_topology["active_roles"][0]["rehearsalPriority"] == "high"
assert "Density warning" in verse_topology["active_roles"][0]["overlapWarnings"][0]

verse_graph = verse_topology["part_graph"]
assert len(verse_graph) == 3
assert verse_graph[1]["role_id"] == "keys-right"
assert len(verse_graph) == 4
assert verse_graph[1]["role_id"] == "keys-left"
assert verse_graph[1]["is_active"] is False
assert verse_graph[2]["role_id"] == "keys-right"
assert verse_graph[2]["is_active"] is False
assert verse_graph[0]["role_id"] == "bass-guitar"
assert verse_graph[0]["handoff_to"] == []


def test_role_extractor_empty():
def test_role_extractor_empty() -> None:
"""Test extractor with empty sections list."""
extractor = RoleExtractor()
result = extractor.extract([])
assert result["topologies"] == []


def test_role_extractor_invalid_section():
def test_role_extractor_invalid_section() -> None:
"""Test that RoleExtractor handles non-dict sections gracefully."""
extractor = RoleExtractor()
sections = [{"id": "intro"}, "invalid-section-string"]
Expand Down
8 changes: 8 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading