speaker_manager.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. """
  2. Dynamic Speaker Management system.
  3. This module provides comprehensive speaker management functionality including
  4. adding/removing speakers, enrollment, verification, and speaker database operations.
  5. """
  6. import logging
  7. import os
  8. from typing import Dict, List, Optional, Tuple, Union, Any, Callable
  9. from dataclasses import dataclass
  10. from datetime import datetime
  11. from pathlib import Path
  12. import numpy as np
  13. import torch
  14. from .speaker_embeddings import SpeakerDatabase, SpeakerProfile, EmbeddingComparator, SpeakerEmbeddingExtractor
  15. from .model_loader import VRModelMetadata
  16. # Configure logging
  17. logger = logging.getLogger(__name__)
  18. class SpeakerManagerError(Exception):
  19. """Exception raised by speaker manager operations."""
  20. pass
  21. @dataclass
  22. class SpeakerInfo:
  23. """Information about a speaker in the system."""
  24. speaker_id: str
  25. speaker_name: str
  26. enrollment_status: str # "enrolled", "pending", "rejected"
  27. enrollment_date: Optional[str] = None
  28. last_seen: Optional[str] = None
  29. embedding_count: int = 0
  30. average_quality: float = 0.0
  31. verification_threshold: float = 0.7
  32. metadata: Dict[str, Any] = None
  33. def __post_init__(self):
  34. if self.metadata is None:
  35. self.metadata = {}
  36. @dataclass
  37. class EnrollmentResult:
  38. """Result of speaker enrollment process."""
  39. success: bool
  40. speaker_id: str
  41. speaker_name: str
  42. message: str
  43. quality_score: float = 0.0
  44. embedding_count: int = 0
  45. verification_score: float = 0.0
  46. @dataclass
  47. class VerificationResult:
  48. """Result of speaker verification."""
  49. success: bool
  50. speaker_id: Optional[str]
  51. speaker_name: Optional[str]
  52. confidence: float
  53. similarity_score: float
  54. threshold: float
  55. message: str
  56. class SpeakerManager:
  57. """
  58. Comprehensive speaker management system.
  59. Handles dynamic speaker enrollment, verification, and database management
  60. with support for real-time operations.
  61. """
  62. def __init__(self,
  63. database_path: str,
  64. embedding_extractor: SpeakerEmbeddingExtractor,
  65. similarity_metric: str = "cosine",
  66. min_enrollment_samples: int = 3,
  67. max_enrollment_samples: int = 10,
  68. enrollment_threshold: float = 0.8,
  69. verification_threshold: float = 0.7,
  70. quality_threshold: float = 0.6):
  71. """
  72. Initialize speaker manager.
  73. Args:
  74. database_path: Path to speaker database
  75. embedding_extractor: Speaker embedding extractor
  76. similarity_metric: Similarity metric for comparisons
  77. min_enrollment_samples: Minimum samples for enrollment
  78. max_enrollment_samples: Maximum samples for enrollment
  79. enrollment_threshold: Threshold for successful enrollment
  80. verification_threshold: Threshold for speaker verification
  81. quality_threshold: Minimum quality threshold for embeddings
  82. """
  83. self.database_path = database_path
  84. self.embedding_extractor = embedding_extractor
  85. self.similarity_metric = similarity_metric
  86. # Configuration
  87. self.min_enrollment_samples = min_enrollment_samples
  88. self.max_enrollment_samples = max_enrollment_samples
  89. self.enrollment_threshold = enrollment_threshold
  90. self.verification_threshold = verification_threshold
  91. self.quality_threshold = quality_threshold
  92. # Initialize components
  93. self.database = SpeakerDatabase(database_path)
  94. self.comparator = EmbeddingComparator(similarity_metric)
  95. # Statistics
  96. self.enrollment_stats = {
  97. 'successful_enrollments': 0,
  98. 'failed_enrollments': 0,
  99. 'total_verifications': 0,
  100. 'successful_verifications': 0
  101. }
  102. # Callbacks for events
  103. self.on_speaker_enrolled: Optional[Callable] = None
  104. self.on_speaker_removed: Optional[Callable] = None
  105. self.on_verification_complete: Optional[Callable] = None
  106. logger.info(f"Speaker manager initialized with {self.get_speaker_count()} speakers")
  107. def enroll_speaker(self,
  108. speaker_name: str,
  109. audio_features_list: List[torch.Tensor],
  110. speaker_id: Optional[str] = None,
  111. metadata: Optional[Dict[str, Any]] = None) -> EnrollmentResult:
  112. """
  113. Enroll a new speaker using multiple audio samples.
  114. Args:
  115. speaker_name: Name of the speaker
  116. audio_features_list: List of audio feature tensors
  117. speaker_id: Optional custom speaker ID
  118. metadata: Optional speaker metadata
  119. Returns:
  120. Enrollment result
  121. """
  122. try:
  123. # Generate speaker ID if not provided
  124. if speaker_id is None:
  125. speaker_id = self._generate_speaker_id(speaker_name)
  126. # Check if speaker already exists
  127. if self.database.get_speaker_by_id(speaker_id) or self.database.get_speaker_by_name(speaker_name):
  128. return EnrollmentResult(
  129. success=False,
  130. speaker_id=speaker_id,
  131. speaker_name=speaker_name,
  132. message=f"Speaker {speaker_name} already exists"
  133. )
  134. # Validate number of samples
  135. if len(audio_features_list) < self.min_enrollment_samples:
  136. return EnrollmentResult(
  137. success=False,
  138. speaker_id=speaker_id,
  139. speaker_name=speaker_name,
  140. message=f"Insufficient samples: need at least {self.min_enrollment_samples}, got {len(audio_features_list)}"
  141. )
  142. # Extract embeddings from all samples
  143. embeddings = []
  144. quality_scores = []
  145. for features in audio_features_list[:self.max_enrollment_samples]:
  146. embedding = self.embedding_extractor.extract_embedding(features)
  147. quality = self.embedding_extractor.compute_embedding_quality(features, embedding)
  148. # Only keep embeddings above quality threshold
  149. if quality >= self.quality_threshold:
  150. embeddings.append(embedding)
  151. quality_scores.append(quality)
  152. if len(embeddings) < self.min_enrollment_samples:
  153. return EnrollmentResult(
  154. success=False,
  155. speaker_id=speaker_id,
  156. speaker_name=speaker_name,
  157. message=f"Insufficient quality samples: need {self.min_enrollment_samples}, got {len(embeddings)}"
  158. )
  159. # Verify enrollment quality by computing intra-speaker similarities
  160. intra_similarities = self._compute_intra_speaker_similarities(embeddings)
  161. average_similarity = np.mean(intra_similarities)
  162. if average_similarity < self.enrollment_threshold:
  163. return EnrollmentResult(
  164. success=False,
  165. speaker_id=speaker_id,
  166. speaker_name=speaker_name,
  167. message=f"Low enrollment quality: {average_similarity:.3f} < {self.enrollment_threshold}",
  168. quality_score=float(np.mean(quality_scores)),
  169. verification_score=average_similarity
  170. )
  171. # Create speaker profile
  172. profile = self.database.add_speaker(speaker_id, speaker_name, metadata)
  173. # Add embeddings to profile
  174. enrollment_date = datetime.now().isoformat()
  175. for embedding, quality in zip(embeddings, quality_scores):
  176. profile.add_embedding(embedding, enrollment_date, quality)
  177. # Save database
  178. self.database.save_database()
  179. # Update statistics
  180. self.enrollment_stats['successful_enrollments'] += 1
  181. # Trigger callback
  182. if self.on_speaker_enrolled:
  183. self.on_speaker_enrolled(speaker_id, speaker_name)
  184. logger.info(f"Successfully enrolled speaker: {speaker_name} ({speaker_id})")
  185. return EnrollmentResult(
  186. success=True,
  187. speaker_id=speaker_id,
  188. speaker_name=speaker_name,
  189. message="Speaker enrolled successfully",
  190. quality_score=float(np.mean(quality_scores)),
  191. embedding_count=len(embeddings),
  192. verification_score=average_similarity
  193. )
  194. except Exception as e:
  195. self.enrollment_stats['failed_enrollments'] += 1
  196. logger.error(f"Enrollment failed for {speaker_name}: {e}")
  197. return EnrollmentResult(
  198. success=False,
  199. speaker_id=speaker_id or "unknown",
  200. speaker_name=speaker_name,
  201. message=f"Enrollment failed: {e}"
  202. )
  203. def verify_speaker(self,
  204. audio_features: torch.Tensor,
  205. expected_speaker_id: Optional[str] = None,
  206. threshold: Optional[float] = None) -> VerificationResult:
  207. """
  208. Verify a speaker from audio features.
  209. Args:
  210. audio_features: Audio features tensor
  211. expected_speaker_id: Expected speaker ID for verification
  212. threshold: Custom verification threshold
  213. Returns:
  214. Verification result
  215. """
  216. try:
  217. self.enrollment_stats['total_verifications'] += 1
  218. # Extract embedding
  219. query_embedding = self.embedding_extractor.extract_embedding(audio_features)
  220. # Get all speaker embeddings
  221. speaker_embeddings = self.database.get_all_speaker_embeddings("weighted_mean")
  222. if not speaker_embeddings:
  223. return VerificationResult(
  224. success=False,
  225. speaker_id=None,
  226. speaker_name=None,
  227. confidence=0.0,
  228. similarity_score=0.0,
  229. threshold=threshold or self.verification_threshold,
  230. message="No enrolled speakers found"
  231. )
  232. # Find best match
  233. best_speaker_id, best_similarity = self.comparator.find_best_match(
  234. query_embedding, speaker_embeddings
  235. )
  236. # Get speaker profile
  237. speaker_profile = self.database.get_speaker_by_id(best_speaker_id)
  238. speaker_name = speaker_profile.speaker_name if speaker_profile else "Unknown"
  239. # Use custom threshold or default
  240. used_threshold = threshold or self.verification_threshold
  241. # Check if verification passes threshold
  242. verification_success = best_similarity >= used_threshold
  243. # If expected speaker is specified, check if it matches
  244. if expected_speaker_id and best_speaker_id != expected_speaker_id:
  245. verification_success = False
  246. message = f"Expected {expected_speaker_id}, got {best_speaker_id}"
  247. else:
  248. if verification_success:
  249. message = f"Speaker verified: {speaker_name}"
  250. self.enrollment_stats['successful_verifications'] += 1
  251. else:
  252. message = f"Verification failed: similarity {best_similarity:.3f} < {used_threshold}"
  253. # Update last seen for verified speaker
  254. if verification_success and speaker_profile:
  255. speaker_profile.metadata['last_seen'] = datetime.now().isoformat()
  256. self.database.save_database()
  257. # Trigger callback
  258. if self.on_verification_complete:
  259. self.on_verification_complete(verification_success, best_speaker_id, best_similarity)
  260. return VerificationResult(
  261. success=verification_success,
  262. speaker_id=best_speaker_id if verification_success else None,
  263. speaker_name=speaker_name if verification_success else None,
  264. confidence=best_similarity,
  265. similarity_score=best_similarity,
  266. threshold=used_threshold,
  267. message=message
  268. )
  269. except Exception as e:
  270. logger.error(f"Verification failed: {e}")
  271. return VerificationResult(
  272. success=False,
  273. speaker_id=None,
  274. speaker_name=None,
  275. confidence=0.0,
  276. similarity_score=0.0,
  277. threshold=threshold or self.verification_threshold,
  278. message=f"Verification error: {e}"
  279. )
  280. def identify_speaker(self, audio_features: torch.Tensor) -> VerificationResult:
  281. """
  282. Identify speaker without knowing expected identity.
  283. Args:
  284. audio_features: Audio features tensor
  285. Returns:
  286. Identification result
  287. """
  288. return self.verify_speaker(audio_features, expected_speaker_id=None)
  289. def remove_speaker(self, speaker_id: str) -> bool:
  290. """
  291. Remove speaker from database.
  292. Args:
  293. speaker_id: Speaker ID to remove
  294. Returns:
  295. True if speaker was removed
  296. """
  297. speaker_profile = self.database.get_speaker_by_id(speaker_id)
  298. if not speaker_profile:
  299. return False
  300. speaker_name = speaker_profile.speaker_name
  301. success = self.database.remove_speaker(speaker_id)
  302. if success:
  303. self.database.save_database()
  304. # Trigger callback
  305. if self.on_speaker_removed:
  306. self.on_speaker_removed(speaker_id, speaker_name)
  307. logger.info(f"Removed speaker: {speaker_name} ({speaker_id})")
  308. return success
  309. def add_speaker_sample(self,
  310. speaker_id: str,
  311. audio_features: torch.Tensor) -> bool:
  312. """
  313. Add additional sample to existing speaker.
  314. Args:
  315. speaker_id: Speaker ID
  316. audio_features: Audio features tensor
  317. Returns:
  318. True if sample was added successfully
  319. """
  320. speaker_profile = self.database.get_speaker_by_id(speaker_id)
  321. if not speaker_profile:
  322. return False
  323. # Extract embedding
  324. embedding = self.embedding_extractor.extract_embedding(audio_features)
  325. quality = self.embedding_extractor.compute_embedding_quality(audio_features, embedding)
  326. # Only add if quality is sufficient
  327. if quality >= self.quality_threshold:
  328. enrollment_date = datetime.now().isoformat()
  329. speaker_profile.add_embedding(embedding, enrollment_date, quality)
  330. self.database.save_database()
  331. logger.info(f"Added sample to speaker {speaker_id} (quality: {quality:.3f})")
  332. return True
  333. else:
  334. logger.warning(f"Sample quality too low for speaker {speaker_id}: {quality:.3f}")
  335. return False
  336. def get_speaker_info(self, speaker_id: str) -> Optional[SpeakerInfo]:
  337. """Get detailed information about a speaker."""
  338. profile = self.database.get_speaker_by_id(speaker_id)
  339. if not profile:
  340. return None
  341. return SpeakerInfo(
  342. speaker_id=speaker_id,
  343. speaker_name=profile.speaker_name,
  344. enrollment_status="enrolled",
  345. enrollment_date=profile.enrollment_dates[0] if profile.enrollment_dates else None,
  346. last_seen=profile.metadata.get('last_seen'),
  347. embedding_count=len(profile.embeddings),
  348. average_quality=float(np.mean(profile.quality_scores)) if profile.quality_scores else 0.0,
  349. verification_threshold=self.verification_threshold,
  350. metadata=profile.metadata
  351. )
  352. def list_speakers(self) -> List[SpeakerInfo]:
  353. """Get list of all speakers with their information."""
  354. speakers = []
  355. for speaker_id, _ in self.database.get_speaker_list():
  356. info = self.get_speaker_info(speaker_id)
  357. if info:
  358. speakers.append(info)
  359. return speakers
  360. def get_speaker_count(self) -> int:
  361. """Get total number of enrolled speakers."""
  362. return self.database.get_speaker_count()
  363. def get_statistics(self) -> Dict[str, Any]:
  364. """Get system statistics."""
  365. stats = self.enrollment_stats.copy()
  366. stats.update({
  367. 'total_speakers': self.get_speaker_count(),
  368. 'total_embeddings': self.database.get_embedding_count(),
  369. 'verification_success_rate': (
  370. stats['successful_verifications'] / max(1, stats['total_verifications'])
  371. ),
  372. 'enrollment_success_rate': (
  373. stats['successful_enrollments'] / max(1, stats['successful_enrollments'] + stats['failed_enrollments'])
  374. )
  375. })
  376. return stats
  377. def update_model(self, new_embedding_extractor: SpeakerEmbeddingExtractor) -> None:
  378. """
  379. Update the embedding extractor model.
  380. Args:
  381. new_embedding_extractor: New embedding extractor
  382. """
  383. self.embedding_extractor = new_embedding_extractor
  384. logger.info("Updated embedding extractor model")
  385. def validate_speaker_database(self) -> Dict[str, Any]:
  386. """
  387. Validate speaker database integrity.
  388. Returns:
  389. Validation report
  390. """
  391. report = {
  392. 'is_valid': True,
  393. 'issues': [],
  394. 'warnings': [],
  395. 'speaker_stats': {}
  396. }
  397. for speaker_id, profile in self.database.speakers.items():
  398. speaker_issues = []
  399. # Check embedding consistency
  400. if not profile.embeddings:
  401. speaker_issues.append("No embeddings")
  402. elif len(profile.embeddings) < self.min_enrollment_samples:
  403. speaker_issues.append(f"Too few embeddings: {len(profile.embeddings)}")
  404. # Check quality scores
  405. if profile.quality_scores:
  406. avg_quality = np.mean(profile.quality_scores)
  407. if avg_quality < self.quality_threshold:
  408. speaker_issues.append(f"Low average quality: {avg_quality:.3f}")
  409. # Check intra-speaker consistency
  410. if len(profile.embeddings) > 1:
  411. similarities = self._compute_intra_speaker_similarities(profile.embeddings)
  412. avg_similarity = np.mean(similarities)
  413. if avg_similarity < self.enrollment_threshold:
  414. speaker_issues.append(f"Low intra-speaker similarity: {avg_similarity:.3f}")
  415. if speaker_issues:
  416. report['issues'].append(f"Speaker {speaker_id}: {', '.join(speaker_issues)}")
  417. report['is_valid'] = False
  418. report['speaker_stats'][speaker_id] = {
  419. 'embedding_count': len(profile.embeddings),
  420. 'average_quality': float(np.mean(profile.quality_scores)) if profile.quality_scores else 0.0,
  421. 'issues': speaker_issues
  422. }
  423. return report
  424. def _generate_speaker_id(self, speaker_name: str) -> str:
  425. """Generate unique speaker ID from name."""
  426. # Simple ID generation - in production, use more robust method
  427. base_id = "spk_" + speaker_name.lower().replace(" ", "_")
  428. # Ensure uniqueness
  429. counter = 1
  430. speaker_id = base_id
  431. while self.database.get_speaker_by_id(speaker_id):
  432. speaker_id = f"{base_id}_{counter:03d}"
  433. counter += 1
  434. return speaker_id
  435. def _compute_intra_speaker_similarities(self, embeddings: List[np.ndarray]) -> List[float]:
  436. """Compute similarities between all pairs of embeddings."""
  437. similarities = []
  438. for i in range(len(embeddings)):
  439. for j in range(i + 1, len(embeddings)):
  440. sim = self.comparator.compute_similarity(embeddings[i], embeddings[j])
  441. similarities.append(sim)
  442. return similarities
  443. def create_speaker_manager(database_path: str,
  444. embedding_extractor: SpeakerEmbeddingExtractor,
  445. **kwargs) -> SpeakerManager:
  446. """
  447. Factory function to create speaker manager.
  448. Args:
  449. database_path: Path to speaker database
  450. embedding_extractor: Speaker embedding extractor
  451. **kwargs: Additional configuration parameters
  452. Returns:
  453. Configured speaker manager
  454. """
  455. return SpeakerManager(database_path, embedding_extractor, **kwargs)
  456. def add_speaker_to_database(manager: SpeakerManager,
  457. speaker_name: str,
  458. audio_features_list: List[torch.Tensor],
  459. **kwargs) -> EnrollmentResult:
  460. """
  461. Convenience function to add speaker to database.
  462. Args:
  463. manager: Speaker manager instance
  464. speaker_name: Name of speaker to add
  465. audio_features_list: List of audio features for enrollment
  466. **kwargs: Additional enrollment parameters
  467. Returns:
  468. Enrollment result
  469. """
  470. return manager.enroll_speaker(speaker_name, audio_features_list, **kwargs)
  471. def remove_speaker_from_database(manager: SpeakerManager, speaker_id: str) -> bool:
  472. """
  473. Convenience function to remove speaker from database.
  474. Args:
  475. manager: Speaker manager instance
  476. speaker_id: Speaker ID to remove
  477. Returns:
  478. True if speaker was removed
  479. """
  480. return manager.remove_speaker(speaker_id)
  481. # Example usage and testing
  482. def test_speaker_manager():
  483. """Test speaker manager functionality."""
  484. from .model_architectures import ECADATDNNModel
  485. from .speaker_embeddings import SpeakerEmbeddingExtractor
  486. # Create mock model and extractor
  487. model = ECADATDNNModel(input_dim=40, embedding_dim=192, num_speakers=10)
  488. extractor = SpeakerEmbeddingExtractor(model, "cpu")
  489. # Create speaker manager
  490. database_path = "/tmp/test_speaker_db.pkl"
  491. manager = create_speaker_manager(database_path, extractor)
  492. # Test enrollment
  493. print("Testing speaker enrollment...")
  494. # Generate mock audio features
  495. audio_features_list = [torch.randn(40, 100) for _ in range(5)]
  496. # Enroll speaker
  497. result = manager.enroll_speaker("Test Speaker", audio_features_list)
  498. print(f"Enrollment result: {result.success}, {result.message}")
  499. if result.success:
  500. # Test verification
  501. print("Testing speaker verification...")
  502. test_features = torch.randn(40, 100)
  503. verification = manager.verify_speaker(test_features, result.speaker_id)
  504. print(f"Verification result: {verification.success}, confidence: {verification.confidence:.3f}")
  505. # Test identification
  506. identification = manager.identify_speaker(test_features)
  507. print(f"Identification result: {identification.speaker_name}, confidence: {identification.confidence:.3f}")
  508. # Get speaker info
  509. info = manager.get_speaker_info(result.speaker_id)
  510. print(f"Speaker info: {info.speaker_name}, embeddings: {info.embedding_count}")
  511. # Get statistics
  512. stats = manager.get_statistics()
  513. print(f"Statistics: {stats}")
  514. # Clean up
  515. if os.path.exists(database_path):
  516. os.remove(database_path)
  517. if __name__ == "__main__":
  518. test_speaker_manager()