1616from enum import Enum
1717from typing import Any , Optional
1818
19+ # Import specific enum constants to avoid pylint no-member on dynamic attributes
1920from ucapi .api_definitions import CommandHandler
2021from ucapi .entity import Entity , EntityTypes
2122
23+ from ucapi .proto .ucr_integration_voice_pb2 import ( # pylint: disable=no-name-in-module # isort:skip # noqa
24+ F32 as PB_F32 ,
25+ I16 as PB_I16 ,
26+ I32 as PB_I32 ,
27+ U16 as PB_U16 ,
28+ U32 as PB_U32 ,
29+ )
30+
2231DEFAULT_AUDIO_CHANNELS = 1
2332DEFAULT_SAMPLE_RATE = 16000
2433
@@ -97,6 +106,48 @@ class SampleFormat(str, Enum):
97106 F32 = "F32"
98107 """Float 32 bit."""
99108
109+ @classmethod
110+ def from_proto (cls , value : Any ) -> Optional ["SampleFormat" ]:
111+ """Convert protobuf enum ``SampleFormat`` to Python enum.
112+
113+ Returns ``None`` when the value is unknown or not available in this
114+ Python enum (e.g., ``SAMPLE_FORMAT_UNKNOWN``, ``I8``, ``U8``).
115+
116+ Accepts the following inputs:
117+ - Protobuf enum value (``pb2.SampleFormat``)
118+ - Integer value of the protobuf enum
119+ - String value (e.g., "I16", "U32")
120+ - ``None``
121+ """
122+ if value is None :
123+ return None
124+
125+ # Map protobuf values (or their ints) to our Python enum
126+ mapping : dict [int , SampleFormat ] = {
127+ int (PB_I16 ): cls .I16 ,
128+ int (PB_I32 ): cls .I32 ,
129+ int (PB_U16 ): cls .U16 ,
130+ int (PB_U32 ): cls .U32 ,
131+ int (PB_F32 ): cls .F32 ,
132+ }
133+
134+ if isinstance (value , int ):
135+ return mapping .get (int (value ))
136+
137+ if isinstance (value , str ):
138+ key = value .strip ().upper ()
139+ # Only map to values that exist in this Python enum
140+ try :
141+ return cls [key ]
142+ except KeyError :
143+ return None
144+
145+ # Fallback for enum-like types (protobuf enum wrappers behave like ints)
146+ try :
147+ return mapping .get (int (value ))
148+ except (TypeError , ValueError ):
149+ return None
150+
100151
101152DEFAULT_SAMPLE_FORMAT = SampleFormat .I16
102153
@@ -121,6 +172,69 @@ class AudioConfiguration:
121172 sample_format : SampleFormat = DEFAULT_SAMPLE_FORMAT
122173 """Audio sample format."""
123174
175+ @staticmethod
176+ def _to_int (value : Any , default : int ) -> int :
177+ """Best-effort conversion to ``int`` with a sensible default.
178+
179+ Accepts ``int``/``str``/``None`` and returns ``default`` if conversion
180+ fails or value is falsy.
181+ """
182+ if value is None :
183+ return default
184+ try :
185+ if isinstance (value , bool ): # avoid bool being a subclass of int
186+ return default
187+ if isinstance (value , (int ,)):
188+ return int (value ) or default
189+ if isinstance (value , str ):
190+ s = value .strip ()
191+ return int (s ) if s else default
192+ except (TypeError , ValueError ):
193+ return default
194+ return default
195+
196+ @classmethod
197+ def from_proto (cls , value : Any ) -> Optional ["AudioConfiguration" ]:
198+ """Convert protobuf ``AudioConfiguration`` (or mapping) to Python model.
199+
200+ - ``None`` returns ``None``
201+ - Protobuf message: reads fields and converts types
202+ - ``dict``/``mapping``: accepts keys ``channels``, ``sample_rate``,
203+ ``sample_format`` (strings/ints acceptable)
204+
205+ The protobuf field ``format`` (``AudioFormat``) is currently ignored in
206+ the Python model.
207+ """
208+ if value is None :
209+ return None
210+
211+ # Extract raw field values from either a proto message or a dict-like
212+ if (
213+ hasattr (value , "__class__" )
214+ and value .__class__ .__name__ == "AudioConfiguration"
215+ ):
216+ # Likely a protobuf message instance
217+ ch = getattr (value , "channels" , DEFAULT_AUDIO_CHANNELS )
218+ sr = getattr (value , "sample_rate" , DEFAULT_SAMPLE_RATE )
219+ sf = getattr (value , "sample_format" , None )
220+ elif isinstance (value , dict ):
221+ ch = value .get ("channels" , DEFAULT_AUDIO_CHANNELS )
222+ sr = value .get ("sample_rate" , DEFAULT_SAMPLE_RATE )
223+ sf = value .get ("sample_format" , None )
224+ else :
225+ # Unsupported type
226+ return None
227+
228+ channels = cls ._to_int (ch , DEFAULT_AUDIO_CHANNELS )
229+ sample_rate = cls ._to_int (sr , DEFAULT_SAMPLE_RATE )
230+ sample_format = SampleFormat .from_proto (sf ) or DEFAULT_SAMPLE_FORMAT
231+
232+ return cls (
233+ channels = channels ,
234+ sample_rate = sample_rate ,
235+ sample_format = sample_format ,
236+ )
237+
124238
125239@dataclass (slots = True )
126240class VoiceAssistantProfile :
0 commit comments