Source code for idtap.audio_models

"""Data models for audio upload functionality."""

from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any, Literal, Union
from datetime import datetime


[docs] @dataclass class Musician: """Represents a musician in a recording.""" name: str role: Literal['Soloist', 'Accompanist', 'Percussionist', 'Drone'] instrument: str gharana: Optional[str] = None
[docs] def to_json(self) -> Dict[str, Any]: """Convert to JSON format for API.""" return { 'role': self.role, 'instrument': self.instrument, 'gharana': self.gharana }
[docs] @dataclass class Location: """Represents a geographic location.""" continent: str country: str city: Optional[str] = None
[docs] def to_json(self) -> Dict[str, Any]: """Convert to JSON format for API.""" result = { 'continent': self.continent, 'country': self.country } if self.city: result['city'] = self.city return result
[docs] @dataclass class RecordingDate: """Represents a recording date.""" year: Optional[int] = None month: Optional[str] = None day: Optional[int] = None
[docs] def to_json(self) -> Dict[str, Any]: """Convert to JSON format for API.""" result = {} if self.year is not None: result['year'] = str(self.year) if self.month is not None: result['month'] = self.month if self.day is not None: result['day'] = str(self.day) return result
[docs] @dataclass class PerformanceSection: """Represents a performance section within a raga.""" name: str start: float = 0.0 end: float = 0.0
[docs] def to_json(self) -> Dict[str, Any]: """Convert to JSON format for API.""" return { 'start': self.start, 'end': self.end }
@dataclass class Raga: """Represents a raga with performance sections.""" name: str performance_sections: List[PerformanceSection] = field(default_factory=list) def to_json(self) -> Dict[str, Any]: """Convert to JSON format for API.""" sections = {} for section in self.performance_sections: sections[section.name] = section.to_json() return { 'performance sections': sections }
[docs] @dataclass class Permissions: """Represents access permissions for a recording.""" public_view: bool = True edit: List[str] = field(default_factory=list) view: List[str] = field(default_factory=list)
[docs] def to_json(self) -> Dict[str, Any]: """Convert to JSON format for API.""" return { 'publicView': self.public_view, 'edit': self.edit, 'view': self.view }
[docs] @dataclass class AudioMetadata: """Complete metadata for an audio recording. Args: title: Optional title for the recording musicians: List of Musician objects location: Optional Location object date: Optional RecordingDate object ragas: List of raga specifications. Accepts multiple formats: - AudioRaga objects: AudioRaga(name="Rageshree") (recommended) - Strings: "Rageshree" (auto-converted to AudioRaga) - Name dicts: {"name": "Rageshree"} (auto-converted to AudioRaga) - Legacy format: {"Rageshree": {"performance_sections": {}}} (auto-converted) sa_estimate: Optional fundamental frequency estimate in Hz permissions: Permissions object for access control """ title: Optional[str] = None musicians: List[Musician] = field(default_factory=list) location: Optional[Location] = None date: Optional[RecordingDate] = None ragas: List[Union[Raga, str, Dict[str, Any]]] = field(default_factory=list) sa_estimate: Optional[float] = None permissions: Permissions = field(default_factory=Permissions) def _normalize_ragas(self, ragas: List[Union[Raga, str, Dict[str, Any]]]) -> List[Raga]: """Convert various raga input formats to AudioRaga objects.""" normalized = [] for i, raga in enumerate(ragas): if isinstance(raga, Raga): # Already an AudioRaga object normalized.append(raga) elif isinstance(raga, str): # String format: "Rageshree" -> AudioRaga(name="Rageshree") normalized.append(Raga(name=raga)) elif isinstance(raga, dict): if 'name' in raga: # Name dict format: {"name": "Rageshree"} -> AudioRaga(name="Rageshree") normalized.append(Raga(name=raga['name'])) elif len(raga) == 1: # Legacy format: {"Rageshree": {...}} -> AudioRaga(name="Rageshree") raga_name = list(raga.keys())[0] normalized.append(Raga(name=raga_name)) else: raise ValueError(f"Raga at index {i}: Invalid dict format. " f"Use {{'name': 'RagaName'}} or AudioRaga(name='RagaName') instead.") else: # Check for wrong Raga class (musical analysis Raga) if hasattr(raga, 'name') and hasattr(raga, 'rule_set'): raise ValueError(f"Raga at index {i}: Musical analysis Raga class not supported for uploads. " f"Use AudioRaga(name='{raga.name}') instead.") else: raise ValueError(f"Raga at index {i}: Invalid raga format. " f"Expected AudioRaga object, string, or dict with 'name' key. " f"Got {type(raga).__name__}: {raga}") return normalized def _validate_ragas(self, ragas: List[Raga]) -> None: """Validate that all ragas are AudioRaga objects with to_json method.""" for i, raga in enumerate(ragas): if not hasattr(raga, 'to_json'): raise ValueError(f"Raga at index {i}: Object missing to_json method. " f"Expected AudioRaga object, got {type(raga).__name__}") if not hasattr(raga, 'name'): raise ValueError(f"Raga at index {i}: Object missing name attribute. " f"Expected AudioRaga object, got {type(raga).__name__}")
[docs] def to_json(self) -> Dict[str, Any]: """Convert to JSON format for API.""" # Convert musicians to dict format expected by API musicians_dict = {} for musician in self.musicians: musicians_dict[musician.name] = musician.to_json() # Normalize and validate ragas, then convert to dict format expected by API normalized_ragas = self._normalize_ragas(self.ragas) self._validate_ragas(normalized_ragas) ragas_dict = {} for raga in normalized_ragas: ragas_dict[raga.name] = raga.to_json() result = { 'musicians': musicians_dict, 'ragas': ragas_dict, 'permissions': self.permissions.to_json() } if self.title: result['title'] = self.title if self.location: result['location'] = self.location.to_json() if self.date: result['date'] = self.date.to_json() if self.sa_estimate is not None: result['sa_estimate'] = self.sa_estimate return result
[docs] @dataclass class AudioEventConfig: """Configuration for audio event association.""" mode: Literal['add', 'create', 'none'] = 'none' event_id: Optional[str] = None # For 'add' mode name: Optional[str] = None # For 'create' mode event_type: Optional[str] = None # For 'create' mode
[docs] def to_json(self) -> Dict[str, Any]: """Convert to JSON format for API.""" result = {'mode': self.mode} if self.event_id: result['event_id'] = self.event_id if self.name: result['name'] = self.name if self.event_type: result['event_type'] = self.event_type return result
[docs] @dataclass class FileInfo: """Information about an uploaded file.""" name: str mimetype: str size: int
[docs] @dataclass class ProcessingStatus: """Status of audio processing operations.""" audio_processed: bool = False melograph_generated: bool = False spectrogram_generated: bool = False
[docs] @dataclass class AudioUploadResult: """Result of an audio upload operation.""" audio_id: str success: bool file_info: FileInfo processing_status: ProcessingStatus
[docs] @classmethod def from_api_response(cls, response_data: Dict[str, Any]) -> 'AudioUploadResult': """Create from API response.""" file_info_data = response_data.get('file_info', {}) file_info = FileInfo( name=file_info_data.get('name', ''), mimetype=file_info_data.get('mimetype', ''), size=file_info_data.get('size', 0) ) processing_data = response_data.get('processing_status', {}) processing_status = ProcessingStatus( audio_processed=processing_data.get('audio_processed', False), melograph_generated=processing_data.get('melograph_generated', False), spectrogram_generated=processing_data.get('spectrogram_generated', False) ) return cls( audio_id=response_data.get('audio_id', ''), success=response_data.get('success', False), file_info=file_info, processing_status=processing_status )
[docs] @dataclass class ValidationResult: """Result of metadata validation.""" is_valid: bool errors: List[str] = field(default_factory=list) warnings: List[str] = field(default_factory=list)
[docs] @dataclass class LocationHierarchy: """Geographic location hierarchy.""" data: Dict[str, Dict[str, List[str]]] # continent -> country -> cities
[docs] def get_continents(self) -> List[str]: """Get available continents.""" return list(self.data.keys())
[docs] def get_countries(self, continent: str) -> List[str]: """Get countries for a continent.""" return list(self.data.get(continent, {}).keys())
[docs] def get_cities(self, continent: str, country: str) -> List[str]: """Get cities for a country.""" return self.data.get(continent, {}).get(country, [])
[docs] @classmethod def from_api_response(cls, data: Dict[str, Any]) -> 'LocationHierarchy': """Create from API response.""" return cls(data=data)