From ff02010ec92c436851fc320848fe27502eeba723 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 3 Sep 2026 03:57:32 +0000 Subject: [PATCH] Optimize list deduplication --- .jules/bolt.md | 5 ++ .../src/bandscope_analysis/api.py | 6 +- .../src/bandscope_analysis/chords/analyzer.py | 60 ++++++++----------- .../src/bandscope_analysis/exports/chart.py | 31 +++++----- 4 files changed, 47 insertions(+), 55 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index d54cf10fc..0844aff4b 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -61,3 +61,8 @@ ## 2026-07-13 - Array.from mapping optimization **Learning:** Using `Array.from({ length: N }).map(...)` creates an intermediate array of `undefined` values which requires memory allocation and garbage collection, adding O(N) unnecessary overhead in frequently re-rendered UI components. **Action:** Use `Array.from({ length: N }, (_, index) => ...)` to map elements directly during array creation, avoiding intermediate allocations. +## 2024-05-18 - Dictionary deduplication maintains list order + +**Learning:** When optimizing O(N^2) list appends (`if x not in lst: lst.append(x)`) with dictionaries (`dict_obj[x] = None`) for deduplication, Python 3.7+ ensures insertion order is maintained. + +**Action:** Confidently use this optimization for arrays/lists without fear of changing return order or causing regressions. diff --git a/services/analysis-engine/src/bandscope_analysis/api.py b/services/analysis-engine/src/bandscope_analysis/api.py index b376de293..e7106b1da 100644 --- a/services/analysis-engine/src/bandscope_analysis/api.py +++ b/services/analysis-engine/src/bandscope_analysis/api.py @@ -398,7 +398,7 @@ def _build_from_pipeline( # 4. Build final payload sections payload_sections: list[RehearsalSectionPayload] = [] - focus_sections: list[str] = [] + focus_sections_dict: dict[str, None] = {} for i, section in enumerate(detected_sections): # Compute time range from boundaries @@ -436,9 +436,9 @@ def _build_from_pipeline( # Track high-priority sections for export summary if section["form_label"] in ("chorus", "verse"): - if section["form_label"] not in focus_sections: - focus_sections.append(section["form_label"]) + focus_sections_dict[section["form_label"]] = None + focus_sections = list(focus_sections_dict.keys()) if not focus_sections and payload_sections: focus_sections = [payload_sections[0]["label"]] diff --git a/services/analysis-engine/src/bandscope_analysis/chords/analyzer.py b/services/analysis-engine/src/bandscope_analysis/chords/analyzer.py index a4278c6d3..fca6ee340 100644 --- a/services/analysis-engine/src/bandscope_analysis/chords/analyzer.py +++ b/services/analysis-engine/src/bandscope_analysis/chords/analyzer.py @@ -140,42 +140,34 @@ def _recognize_from_stems(self, stems: dict[str, np.ndarray], sr: int) -> list[T def _extract_user_chords(self, section_roles: list[dict[str, Any]]) -> list[ChordLabel]: """Extract only user-sourced chords from role harmony data.""" - chords: list[ChordLabel] = [] - seen: set[str] = set() + chords: dict[str, ChordLabel] = {} for role in section_roles: harmony = role.get("harmony") if isinstance(harmony, dict) and "chord" in harmony: if harmony.get("source") == "user": chord_name = str(harmony["chord"]) - if chord_name not in seen: - seen.add(chord_name) - chords.append( - { - "chord": chord_name, - "functionLabel": str(harmony.get("functionLabel", "")), - "source": "user", - } - ) - return chords + if chord_name not in chords: + chords[chord_name] = { + "chord": chord_name, + "functionLabel": str(harmony.get("functionLabel", "")), + "source": "user", + } + return list(chords.values()) def _extract_role_chords(self, section_roles: list[dict[str, Any]]) -> list[ChordLabel]: """Extract model-sourced chords from role harmony data (legacy path).""" - chords: list[ChordLabel] = [] - seen: set[str] = set() + chords: dict[str, ChordLabel] = {} for role in section_roles: harmony = role.get("harmony") if isinstance(harmony, dict) and "chord" in harmony: chord_name = str(harmony["chord"]) - if chord_name not in seen: - seen.add(chord_name) - chords.append( - { - "chord": chord_name, - "functionLabel": str(harmony.get("functionLabel", "")), - "source": "model", - } - ) - return chords + if chord_name not in chords: + chords[chord_name] = { + "chord": chord_name, + "functionLabel": str(harmony.get("functionLabel", "")), + "source": "model", + } + return list(chords.values()) def _filter_recognized_for_section( self, @@ -213,21 +205,17 @@ def _chords_for_section( ] # Deduplicate while preserving order - seen: set[str] = set() - chords: list[ChordLabel] = [] + chords: dict[str, ChordLabel] = {} for chord_seg in valid_chords: chord_name = chord_seg["chord"] - if chord_name not in seen: - seen.add(chord_name) - chords.append( - { - "chord": chord_name, - "functionLabel": "", - "source": "model", - } - ) + if chord_name not in chords: + chords[chord_name] = { + "chord": chord_name, + "functionLabel": "", + "source": "model", + } - return chords + return list(chords.values()) def _compute_section_confidence( self, diff --git a/services/analysis-engine/src/bandscope_analysis/exports/chart.py b/services/analysis-engine/src/bandscope_analysis/exports/chart.py index 3a84b59c8..9a2a6da40 100644 --- a/services/analysis-engine/src/bandscope_analysis/exports/chart.py +++ b/services/analysis-engine/src/bandscope_analysis/exports/chart.py @@ -78,14 +78,14 @@ def _active_role_ids(section: Mapping[str, object]) -> list[str] | None: part_graph = section.get("partGraph") if not isinstance(part_graph, list): return None - active: list[str] = [] + active: dict[str, None] = {} for node in part_graph: if not isinstance(node, Mapping) or node.get("is_active") is not True: continue role_id = node.get("role_id") - if isinstance(role_id, str) and role_id and role_id not in active: - active.append(role_id) - return active + if isinstance(role_id, str) and role_id: + active[role_id] = None + return list(active.keys()) def _active_roles(section: Mapping[str, object]) -> list[Mapping[str, object]]: @@ -121,25 +121,25 @@ def _role_display_name(role: Mapping[str, object]) -> str | None: def _active_role_names(section: Mapping[str, object]) -> list[str]: """Return de-duplicated display names for the section's active roles.""" - names: list[str] = [] + names: dict[str, None] = {} for role in _active_roles(section): name = _role_display_name(role) - if name is not None and name not in names: - names.append(name) - return names + if name is not None: + names[name] = None + return list(names.keys()) def _section_cue(section: Mapping[str, object]) -> str: """Join the active roles' cue values into a single cue string.""" - cues: list[str] = [] + cues: dict[str, None] = {} for role in _active_roles(section): cue = role.get("cue") if not isinstance(cue, Mapping): continue value = cue.get("value") - if isinstance(value, str) and value and value not in cues: - cues.append(value) - return "; ".join(cues) + if isinstance(value, str) and value: + cues[value] = None + return "; ".join(cues.keys()) def _confidence_level(section: Mapping[str, object]) -> str | None: @@ -188,7 +188,7 @@ def _section_lines(sections: list[Mapping[str, object]]) -> list[str]: def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object]]) -> list[str]: """Build the footer: per-role rehearsal priorities and the export focus.""" lines: list[str] = [] - priorities: list[str] = [] + priorities: dict[str, None] = {} for section in sections: for role in _section_roles(section): name = _role_display_name(role) @@ -196,11 +196,10 @@ def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object if name is None or not isinstance(priority, str) or not priority: continue entry = f" - {name}: {priority}" - if entry not in priorities: - priorities.append(entry) + priorities[entry] = None if priorities: lines.append("Priorities:") - lines.extend(priorities) + lines.extend(priorities.keys()) summary = song.get("exportSummary") if isinstance(summary, Mapping): headline = summary.get("headline")