| """Talker-of-interest (ToI) selection heuristics.""" |
|
|
| import logging |
| from typing import List, Tuple |
| from .data_structures import TalkerInfo |
|
|
| log = logging.getLogger(__name__) |
|
|
|
|
| def select_talker_of_interest( |
| talkers: List[TalkerInfo], |
| weights: dict = None, |
| ) -> Tuple[int, TalkerInfo]: |
| """ |
| Select the most likely talker of interest using weighted scoring. |
| |
| Heuristics (in order of importance if using defaults): |
| 1. Front-facing direction (within 45° of 0°) - hearing aid users typically face target |
| 2. English language - contextual heuristic |
| 3. Energy/loudness - louder speakers are more salient |
| 4. Gender clarity - non-ambiguous gender increases confidence |
| |
| Args: |
| talkers: List of TalkerInfo objects |
| weights: Optional dict with keys: 'front_bonus', 'english_bonus', |
| 'energy_weight', 'gender_bonus' |
| |
| Returns: |
| Tuple of (selected_index, selected_talker_info) |
| """ |
| if not talkers: |
| raise ValueError("No talkers to select from") |
| |
| |
| default_weights = { |
| 'front_bonus': 10.0, |
| 'english_bonus': 5.0, |
| 'energy_weight': 5.0, |
| 'gender_bonus': 0.5, |
| } |
| |
| if weights: |
| default_weights.update(weights) |
| |
| w = default_weights |
| best_idx = 0 |
| best_score = -float('inf') |
| |
| scores = [] |
| |
| for i, talker in enumerate(talkers): |
| score = 0.0 |
| score_components = {} |
| |
| |
| if talker.direction_deg is not None: |
| angle = talker.direction_deg |
| angle_from_front = min(angle, 360 - angle) |
| if angle_from_front < 45: |
| score += w['front_bonus'] |
| score_components['front'] = w['front_bonus'] |
| |
| |
| if talker.language == 'en': |
| score += w['english_bonus'] |
| score_components['english'] = w['english_bonus'] |
| |
| |
| if talker.energy is not None: |
| score += talker.energy * w['energy_weight'] |
| score_components['energy'] = talker.energy * w['energy_weight'] |
| |
| |
| if talker.gender and talker.gender != 'ambiguous' and talker.gender != 'unknown': |
| score += w['gender_bonus'] |
| score_components['gender'] = w['gender_bonus'] |
| |
| |
| talker.selection_score = round(score, 2) |
| scores.append((i, score, score_components)) |
| |
| log.debug(f"Speaker {i+1}: score={score:.2f}, components={score_components}") |
| |
| if score > best_score: |
| best_score = score |
| best_idx = i |
| |
| selected_talker = talkers[best_idx] |
| selected_talker.is_toi = True |
| |
| |
| reasons = [] |
| best_components = scores[best_idx][2] |
| if 'front' in best_components: |
| reasons.append("Front-facing") |
| if 'english' in best_components: |
| reasons.append("English speaker") |
| if 'energy' in best_components and best_components['energy'] > 0: |
| reasons.append(f"High energy ({selected_talker.energy:.3f})") |
| if 'gender' in best_components: |
| reasons.append(f"Clear {selected_talker.gender}") |
| |
| selected_talker.toi_reason = "; ".join(reasons) if reasons else "Default selection" |
| |
| log.info(f"Selected talker: Speaker {best_idx + 1} (score: {best_score:.2f})") |
| log.info(f" Reason: {selected_talker.toi_reason}") |
| |
| return best_idx, selected_talker |
|
|
|
|
| def select_by_energy(talkers: List[TalkerInfo]) -> Tuple[int, TalkerInfo]: |
| """Simple heuristic: select talker with highest energy.""" |
| best_idx = 0 |
| best_energy = -float('inf') |
| |
| for i, talker in enumerate(talkers): |
| if talker.energy is not None and talker.energy > best_energy: |
| best_energy = talker.energy |
| best_idx = i |
| |
| talkers[best_idx].is_toi = True |
| talkers[best_idx].toi_reason = f"Highest energy ({best_energy:.3f})" |
| return best_idx, talkers[best_idx] |
|
|
|
|
| def select_by_language(talkers: List[TalkerInfo], target_language: str = 'en') -> Tuple[int, TalkerInfo]: |
| """Select first talker with target language, fallback to speaker 0.""" |
| for i, talker in enumerate(talkers): |
| if talker.language == target_language: |
| talkers[i].is_toi = True |
| talkers[i].toi_reason = f"{target_language.upper()} speaker" |
| return i, talker |
| |
| |
| talkers[0].is_toi = True |
| talkers[0].toi_reason = f"No {target_language.upper()} speaker found, defaulting to speaker 1" |
| return 0, talkers[0] |
|
|