Skip to content

Commit ccfc9a2

Browse files
authored
FEAT: Deprecate use_entra_auth and add auto-detect auth for Azure Speech converters (microsoft#1634)
1 parent 6b479f4 commit ccfc9a2

11 files changed

Lines changed: 582 additions & 83 deletions

.env_example

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
# other OpenAI-compatible endpoint. See doc/setup/populating_secrets.md
1515
# for provider-specific examples.
1616
#
17-
# If you are using Entra authentication for Azure resources
18-
# (use_entra_auth = True in PyRIT), keys for those resources are not needed.
17+
# If you are using Entra authentication for Azure resources,
18+
# keys for those resources are not needed. PyRIT auto-detects: if an API key
19+
# is set, it uses key auth; otherwise it falls back to Entra ID automatically.
1920
#
2021
# ============================================================================
2122

pyrit/auth/azure_auth.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,53 @@ def get_speech_config(resource_id: Union[str, None], key: Union[str, None], regi
433433
raise ValueError("Insufficient information provided for Azure Speech service.")
434434

435435

436+
async def get_speech_config_async(
437+
*,
438+
token_provider: Callable[[], str | Awaitable[str]] | None,
439+
resource_id: Union[str, None],
440+
key: Union[str, None],
441+
region: str,
442+
) -> speechsdk.SpeechConfig:
443+
"""
444+
Get the speech config, resolving a callable token provider if one is provided.
445+
446+
This is the async counterpart to :func:`get_speech_config`. When a callable
447+
``token_provider`` is supplied, it is invoked (and awaited if async) to obtain
448+
a token, which is then used with the ``aad#{resource_id}#{token}`` auth format.
449+
Otherwise, it delegates to the synchronous :func:`get_speech_config`.
450+
451+
Args:
452+
token_provider (Callable | None): An optional sync or async callable that returns a token string.
453+
resource_id (str | None): The resource ID for Entra ID auth.
454+
key (str | None): The Azure Speech API key.
455+
region (str): The Azure region.
456+
457+
Returns:
458+
speechsdk.SpeechConfig: The speech config based on passed in args.
459+
460+
Raises:
461+
ModuleNotFoundError: If azure.cognitiveservices.speech is not installed.
462+
ValueError: If neither key/region nor resource_id/region is provided and no token_provider is given.
463+
"""
464+
if token_provider:
465+
try:
466+
import azure.cognitiveservices.speech as speechsdk # noqa: F811
467+
except ModuleNotFoundError as e:
468+
logger.error(
469+
"Could not import azure.cognitiveservices.speech. "
470+
"You may need to install it via 'pip install pyrit[speech]'"
471+
)
472+
raise e
473+
474+
token = token_provider()
475+
if inspect.isawaitable(token):
476+
token = await token
477+
auth_token = f"aad#{resource_id}#{token}"
478+
return speechsdk.SpeechConfig(auth_token=auth_token, region=region)
479+
480+
return get_speech_config(resource_id=resource_id, key=key, region=region)
481+
482+
436483
def get_speech_config_from_default_azure_credential(resource_id: str, region: str) -> speechsdk.SpeechConfig:
437484
"""
438485
Get the speech config for the given resource ID and region.

pyrit/prompt_converter/azure_speech_audio_to_text_converter.py

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33

44
import logging
55
import time
6+
import warnings
7+
from collections.abc import Awaitable, Callable
68
from typing import TYPE_CHECKING, Any, Optional
79

810
if TYPE_CHECKING:
911
import azure.cognitiveservices.speech as speechsdk # noqa: F401
1012

11-
from pyrit.auth.azure_auth import get_speech_config
13+
from pyrit.auth.azure_auth import get_speech_config, get_speech_config_async
1214
from pyrit.common import default_values
1315
from pyrit.identifiers import ComponentIdentifier
1416
from pyrit.models import PromptDataType, data_serializer_factory
@@ -21,6 +23,14 @@ class AzureSpeechAudioToTextConverter(PromptConverter):
2123
"""
2224
Transcribes a .wav audio file into text using Azure AI Speech service.
2325
26+
Authentication is auto-detected from the provided credentials, in priority order:
27+
28+
1. If ``azure_speech_key`` is a **callable** token provider, it takes highest priority — it is
29+
resolved at conversion time and used with Entra ID auth (``azure_speech_resource_id`` required).
30+
2. If ``azure_speech_key`` is a **string** (or the ``AZURE_SPEECH_KEY`` env var is set), API key auth is used.
31+
3. If **neither** is provided, Entra ID auth is used automatically via ``DefaultAzureCredential``
32+
and ``azure_speech_resource_id`` must be set.
33+
2434
https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-to-text
2535
"""
2636

@@ -36,52 +46,75 @@ class AzureSpeechAudioToTextConverter(PromptConverter):
3646

3747
def __init__(
3848
self,
49+
*,
3950
azure_speech_region: Optional[str] = None,
40-
azure_speech_key: Optional[str] = None,
51+
azure_speech_key: Optional[str | Callable[[], str | Awaitable[str]]] = None,
4152
azure_speech_resource_id: Optional[str] = None,
42-
use_entra_auth: bool = False,
53+
use_entra_auth: Optional[bool] = None,
4354
recognition_language: str = "en-US",
4455
) -> None:
4556
"""
4657
Initialize the converter with Azure Speech service credentials and recognition language.
4758
4859
Args:
4960
azure_speech_region (str, Optional): The name of the Azure region.
50-
azure_speech_key (str, Optional): The API key for accessing the service (if not using Entra ID auth).
61+
azure_speech_key (str | Callable[[], str | Awaitable[str]], Optional): The API key for accessing
62+
the service, or a sync/async callable that returns a token string.
63+
If a string key is provided (or the ``AZURE_SPEECH_KEY`` env var is set), key auth is used.
64+
If a callable token provider is provided, it is resolved at conversion time and used with
65+
Entra ID auth (``azure_speech_resource_id`` must also be set).
66+
If omitted, Entra ID auth via ``DefaultAzureCredential`` is used automatically.
5167
azure_speech_resource_id (str, Optional): The resource ID for accessing the service when using
52-
Entra ID auth. This can be found by selecting 'Properties' in the 'Resource Management'
53-
section of your Azure Speech resource in the Azure portal.
54-
use_entra_auth (bool): Whether to use Entra ID authentication. If True, azure_speech_resource_id
55-
must be provided. If False, azure_speech_key must be provided. Defaults to False.
68+
Entra ID auth. Required when using a callable token provider or when no API key is available.
69+
use_entra_auth (bool, Optional): **Deprecated.** Will be removed in v0.15.0.
70+
Authentication is now auto-detected from the provided credentials.
5671
recognition_language (str): Recognition voice language. Defaults to "en-US".
5772
For more on supported languages, see the following link:
5873
https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support
5974
6075
Raises:
61-
ValueError: If the required environment variables are not set, if azure_speech_key is passed in
62-
when use_entra_auth is True, or if azure_speech_resource_id is passed in when use_entra_auth
63-
is False.
76+
ValueError: If the required environment variables or parameters are not set.
6477
"""
78+
if use_entra_auth is not None:
79+
warnings.warn(
80+
"'use_entra_auth' is deprecated and will be removed in v0.15.0. "
81+
"Authentication is now auto-detected: pass a key string for key auth, "
82+
"a callable token provider for token auth, or omit for automatic Entra ID auth.",
83+
DeprecationWarning,
84+
stacklevel=2,
85+
)
86+
6587
self._azure_speech_region: str = default_values.get_required_value(
6688
env_var_name=self.AZURE_SPEECH_REGION_ENVIRONMENT_VARIABLE,
6789
passed_value=azure_speech_region,
6890
)
69-
if use_entra_auth:
70-
if azure_speech_key:
71-
raise ValueError("If using Entra ID auth, please do not specify azure_speech_key.")
91+
92+
self._token_provider: Callable[[], str | Awaitable[str]] | None = None
93+
self._azure_speech_key: str | None = None
94+
self._azure_speech_resource_id: str | None = None
95+
96+
if azure_speech_key is not None and callable(azure_speech_key):
97+
self._token_provider = azure_speech_key
7298
self._azure_speech_resource_id = default_values.get_required_value(
7399
env_var_name=self.AZURE_SPEECH_RESOURCE_ID_ENVIRONMENT_VARIABLE,
74100
passed_value=azure_speech_resource_id,
75101
)
76-
self._azure_speech_key = None
77102
else:
78-
if azure_speech_resource_id:
79-
raise ValueError("If using key auth, please do not specify azure_speech_resource_id.")
80-
self._azure_speech_key = default_values.get_required_value(
103+
key_value = default_values.get_non_required_value(
81104
env_var_name=self.AZURE_SPEECH_KEY_ENVIRONMENT_VARIABLE,
82105
passed_value=azure_speech_key,
83106
)
84-
self._azure_speech_resource_id = None
107+
if key_value:
108+
self._azure_speech_key = key_value
109+
else:
110+
logger.info(
111+
"No azure_speech_key provided. "
112+
"Entra ID authentication will be attempted via DefaultAzureCredential."
113+
)
114+
self._azure_speech_resource_id = default_values.get_required_value(
115+
env_var_name=self.AZURE_SPEECH_RESOURCE_ID_ENVIRONMENT_VARIABLE,
116+
passed_value=azure_speech_resource_id,
117+
)
85118

86119
self._recognition_language = recognition_language
87120
# Create a flag to indicate when recognition is finished
@@ -126,7 +159,13 @@ async def convert_async(self, *, prompt: str, input_type: PromptDataType = "audi
126159
audio_bytes = await audio_serializer.read_data()
127160

128161
try:
129-
transcript = self.recognize_audio(audio_bytes)
162+
speech_config = await get_speech_config_async(
163+
token_provider=self._token_provider,
164+
resource_id=self._azure_speech_resource_id,
165+
key=self._azure_speech_key,
166+
region=self._azure_speech_region,
167+
)
168+
transcript = self._recognize_audio(audio_bytes=audio_bytes, speech_config=speech_config)
130169
except Exception as e:
131170
logger.error("Failed to convert audio file to text: %s", str(e))
132171
raise
@@ -136,12 +175,44 @@ def recognize_audio(self, audio_bytes: bytes) -> str:
136175
"""
137176
Recognize audio file and return transcribed text.
138177
178+
.. deprecated::
179+
Use :meth:`convert_async` instead, which resolves token providers correctly.
180+
This method does not support callable token providers.
181+
139182
Args:
140183
audio_bytes (bytes): Audio bytes input.
141184
142185
Returns:
143186
str: Transcribed text.
144187
188+
Raises:
189+
ModuleNotFoundError: If the azure.cognitiveservices.speech module is not installed.
190+
"""
191+
if self._token_provider:
192+
warnings.warn(
193+
"recognize_audio() does not support callable token providers. "
194+
"Use convert_async() instead, which correctly resolves token providers.",
195+
DeprecationWarning,
196+
stacklevel=2,
197+
)
198+
speech_config = get_speech_config(
199+
resource_id=self._azure_speech_resource_id,
200+
key=self._azure_speech_key,
201+
region=self._azure_speech_region,
202+
)
203+
return self._recognize_audio(audio_bytes=audio_bytes, speech_config=speech_config)
204+
205+
def _recognize_audio(self, *, audio_bytes: bytes, speech_config: "speechsdk.SpeechConfig") -> str:
206+
"""
207+
Recognize audio from bytes using the given speech config.
208+
209+
Args:
210+
audio_bytes (bytes): Audio bytes input.
211+
speech_config (speechsdk.SpeechConfig): Pre-built speech configuration.
212+
213+
Returns:
214+
str: Transcribed text.
215+
145216
Raises:
146217
ModuleNotFoundError: If the azure.cognitiveservices.speech module is not installed.
147218
"""
@@ -154,36 +225,25 @@ def recognize_audio(self, audio_bytes: bytes) -> str:
154225
)
155226
raise e
156227

157-
speech_config = get_speech_config(
158-
resource_id=self._azure_speech_resource_id, key=self._azure_speech_key, region=self._azure_speech_region
159-
)
160228
speech_config.speech_recognition_language = self._recognition_language
161229

162-
# Create a PullAudioInputStream from the byte stream
163230
push_stream = speechsdk.audio.PushAudioInputStream()
164231
audio_config = speechsdk.audio.AudioConfig(stream=push_stream)
165232

166-
# Instantiate a speech recognizer object
167233
speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config, audio_config=audio_config)
168-
# Create an empty list to store recognized text
169234
transcribed_text: list[str] = []
170-
# Flag is set to False to indicate that recognition is not yet finished
171235
self.done = False
172236

173-
# Connect callbacks to the events fired by the speech recognizer
174237
speech_recognizer.recognized.connect(lambda evt: self.transcript_cb(evt, transcript=transcribed_text))
175238
speech_recognizer.recognizing.connect(lambda evt: logger.info(f"RECOGNIZING: {evt}"))
176239
speech_recognizer.recognized.connect(lambda evt: logger.info(f"RECOGNIZED: {evt}"))
177240
speech_recognizer.session_started.connect(lambda evt: logger.info(f"SESSION STARTED: {evt}"))
178241
speech_recognizer.session_stopped.connect(lambda evt: logger.info(f"SESSION STOPPED: {evt}"))
179-
# Stop continuous recognition when stopped or canceled event is fired
180242
speech_recognizer.canceled.connect(lambda evt: self.stop_cb(evt, recognizer=speech_recognizer))
181243
speech_recognizer.session_stopped.connect(lambda evt: self.stop_cb(evt, recognizer=speech_recognizer))
182244

183-
# Start continuous recognition
184245
speech_recognizer.start_continuous_recognition_async()
185246

186-
# Push the entire audio data into the stream
187247
push_stream.write(audio_bytes)
188248
push_stream.close()
189249

0 commit comments

Comments
 (0)