Skip to content

Commit 04b5c12

Browse files
Xeelee33paullizerCopilot
authored
Feature/speech managed identity (#543)
* Bugfix - deleted duplicate enable_external_healthcheck entry * Feature - updated Speech Service to use Managed Identity in addition to the key, added MAG functionality via Azure Speech SDK since the Fast Transcription API is not available in MAG, updated Admin Setup Walkthrough so it goes to the right place in the settings when Next is clicked, updated Speech requirements in Walkthrough, rewrote Admin Configuration docs, updated/corrected Managed Identity roles in Setup Instructions Special docs. * Update application/single_app/templates/admin_settings.html Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update application/single_app/functions_settings.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update application/single_app/functions_documents.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update application/single_app/functions_documents.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Paul Lizer <paullizer@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent ef2a2a7 commit 04b5c12

8 files changed

Lines changed: 379 additions & 84 deletions

File tree

application/single_app/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from io import BytesIO
6565
from typing import List
6666

67+
import azure.cognitiveservices.speech as speechsdk
6768
from azure.cosmos import CosmosClient, PartitionKey, exceptions
6869
from azure.cosmos.exceptions import CosmosResourceNotFoundError
6970
from azure.core.credentials import AzureKeyCredential

application/single_app/functions_documents.py

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4765,6 +4765,24 @@ def _split_audio_file(input_path: str, chunk_seconds: int = 540) -> List[str]:
47654765
print(f"Produced {len(chunks)} WAV chunks: {chunks}")
47664766
return chunks
47674767

4768+
# Azure Speech SDK helper to get speech config with fresh token
4769+
def _get_speech_config(settings, endpoint: str, locale: str):
4770+
"""Get speech config with fresh token"""
4771+
if settings.get("speech_service_authentication_type") == "managed_identity":
4772+
credential = DefaultAzureCredential()
4773+
token = credential.get_token(cognitive_services_scope)
4774+
speech_config = speechsdk.SpeechConfig(endpoint=endpoint)
4775+
4776+
# Set the authorization token AFTER creating the config
4777+
speech_config.authorization_token = token.token
4778+
else:
4779+
key = settings.get("speech_service_key", "")
4780+
speech_config = speechsdk.SpeechConfig(endpoint=endpoint, subscription=key)
4781+
4782+
speech_config.speech_recognition_language = locale
4783+
print(f"[Debug] Speech config obtained successfully", flush=True)
4784+
return speech_config
4785+
47684786
def process_audio_document(
47694787
document_id: str,
47704788
user_id: str,
@@ -4804,32 +4822,65 @@ def process_audio_document(
48044822
# 3) transcribe each WAV chunk
48054823
settings = get_settings()
48064824
endpoint = settings.get("speech_service_endpoint", "").rstrip('/')
4807-
key = settings.get("speech_service_key", "")
48084825
locale = settings.get("speech_service_locale", "en-US")
4809-
url = f"{endpoint}/speechtotext/transcriptions:transcribe?api-version=2024-11-15"
48104826

48114827
all_phrases: List[str] = []
4812-
for idx, chunk_path in enumerate(chunk_paths, start=1):
4813-
update_callback(current_file_chunk=idx, status=f"Transcribing chunk {idx}/{len(chunk_paths)}…")
4814-
print(f"Transcribing WAV chunk: {chunk_path}")
4815-
4816-
with open(chunk_path, 'rb') as audio_f:
4817-
files = {
4818-
'audio': (os.path.basename(chunk_path), audio_f, 'audio/wav'),
4819-
'definition': (None, json.dumps({'locales':[locale]}), 'application/json')
4820-
}
4821-
headers = {'Ocp-Apim-Subscription-Key': key}
4822-
resp = requests.post(url, headers=headers, files=files)
4823-
try:
4824-
resp.raise_for_status()
4825-
except Exception as e:
4826-
print(f"[Error] HTTP error for {chunk_path}: {e}")
4827-
raise
48284828

4829-
result = resp.json()
4830-
phrases = result.get('combinedPhrases', [])
4831-
print(f"Received {len(phrases)} phrases")
4832-
all_phrases += [p.get('text','').strip() for p in phrases if p.get('text')]
4829+
# Fast Transcription API not yet available in sovereign clouds, so use SDK
4830+
if AZURE_ENVIRONMENT in ("usgovernment", "custom"):
4831+
for idx, chunk_path in enumerate(chunk_paths, start=1):
4832+
print(f"[Debug] Transcribing chunk {idx}: {chunk_path}")
4833+
4834+
# Get fresh config (tokens expire after ~1 hour)
4835+
speech_config = _get_speech_config(settings, endpoint, locale)
4836+
4837+
audio_config = speechsdk.AudioConfig(filename=chunk_path)
4838+
speech_recognizer = speechsdk.SpeechRecognizer(
4839+
speech_config=speech_config,
4840+
audio_config=audio_config
4841+
)
4842+
4843+
result = speech_recognizer.recognize_once()
4844+
if result.reason == speechsdk.ResultReason.RecognizedSpeech:
4845+
print(f"[Debug] Recognized: {result.text}")
4846+
all_phrases.append(result.text)
4847+
elif result.reason == speechsdk.ResultReason.NoMatch:
4848+
print(f"[Warning] No speech in {chunk_path}")
4849+
elif result.reason == speechsdk.ResultReason.Canceled:
4850+
print(f"[Error] {result.cancellation_details.reason}: {result.cancellation_details.error_details}")
4851+
raise RuntimeError(f"Transcription canceled for {chunk_path}: {result.cancellation_details.error_details}")
4852+
4853+
else:
4854+
# Use the fast-transcription API if not in sovereign or custom cloud
4855+
url = f"{endpoint}/speechtotext/transcriptions:transcribe?api-version=2024-11-15"
4856+
for idx, chunk_path in enumerate(chunk_paths, start=1):
4857+
update_callback(current_file_chunk=idx, status=f"Transcribing chunk {idx}/{len(chunk_paths)}…")
4858+
print(f"[Debug] Transcribing WAV chunk: {chunk_path}")
4859+
4860+
with open(chunk_path, 'rb') as audio_f:
4861+
files = {
4862+
'audio': (os.path.basename(chunk_path), audio_f, 'audio/wav'),
4863+
'definition': (None, json.dumps({'locales':[locale]}), 'application/json')
4864+
}
4865+
if settings.get("speech_service_authentication_type") == "managed_identity":
4866+
credential = DefaultAzureCredential()
4867+
token = credential.get_token(cognitive_services_scope)
4868+
headers = {'Authorization': f'Bearer {token.token}'}
4869+
else:
4870+
key = settings.get("speech_service_key", "")
4871+
headers = {'Ocp-Apim-Subscription-Key': key}
4872+
4873+
resp = requests.post(url, headers=headers, files=files)
4874+
try:
4875+
resp.raise_for_status()
4876+
except Exception as e:
4877+
print(f"[Error] HTTP error for {chunk_path}: {e}")
4878+
raise
4879+
4880+
result = resp.json()
4881+
phrases = result.get('combinedPhrases', [])
4882+
print(f"[Debug] Received {len(phrases)} phrases")
4883+
all_phrases += [p.get('text','').strip() for p in phrases if p.get('text')]
48334884

48344885
# 4) cleanup WAV chunks
48354886
for p in chunk_paths:

application/single_app/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@ cython
5252
pyyaml==6.0.2
5353
aiohttp==3.12.15
5454
html2text==2025.4.15
55-
matplotlib==3.10.7
55+
matplotlib==3.10.7
56+
azure-cognitiveservices-speech==1.47.0

application/single_app/route_frontend_admin_settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ def is_valid_url(url):
668668
'speech_service_endpoint': form_data.get('speech_service_endpoint', '').strip(),
669669
'speech_service_location': form_data.get('speech_service_location', '').strip(),
670670
'speech_service_locale': form_data.get('speech_service_locale', '').strip(),
671+
'speech_service_authentication_type': form_data.get('speech_service_authentication_type', 'key'),
671672
'speech_service_key': form_data.get('speech_service_key', '').strip(),
672673

673674
'metadata_extraction_model': form_data.get('metadata_extraction_model', '').strip(),

application/single_app/static/js/admin/admin_settings.js

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,6 +1654,15 @@ function setupToggles() {
16541654
});
16551655
}
16561656

1657+
const speechAuthType = document.getElementById('speech_service_authentication_type');
1658+
if (speechAuthType) {
1659+
speechAuthType.addEventListener('change', function () {
1660+
document.getElementById('speech_service_key_container').style.display =
1661+
(this.value === 'key') ? 'block' : 'none';
1662+
markFormAsModified();
1663+
});
1664+
}
1665+
16571666
const officeAuthType = document.getElementById('office_docs_authentication_type');
16581667
const connStrGroup = document.getElementById('office_docs_storage_conn_str_group');
16591668
const urlGroup = document.getElementById('office_docs_storage_url_group');
@@ -3113,28 +3122,41 @@ function handleTabNavigation(stepNumber) {
31133122
5: 'ai-models-tab', // Embedding settings (now in AI Models tab)
31143123
6: 'search-extract-tab', // AI Search settings
31153124
7: 'search-extract-tab', // Document Intelligence settings
3116-
8: 'workspaces-tab', // Video support
3117-
9: 'workspaces-tab', // Audio support
3125+
8: 'search-extract-tab', // Video support
3126+
9: 'search-extract-tab', // Audio support
31183127
10: 'safety-tab', // Content safety
3119-
11: 'system-tab', // User feedback and archiving (renamed from other-tab)
3128+
11: 'safety-tab', // User feedback and archiving (changed from system-tab)
31203129
12: 'citation-tab' // Enhanced Citations and Image Generation
31213130
};
31223131

31233132
// Activate the appropriate tab
31243133
const tabId = stepToTab[stepNumber];
31253134
if (tabId) {
3126-
const tab = document.getElementById(tabId);
3127-
if (tab) {
3128-
// Use bootstrap Tab to show the tab
3129-
const bootstrapTab = new bootstrap.Tab(tab);
3130-
bootstrapTab.show();
3131-
3132-
// Scroll to the relevant section after a small delay to allow tab to switch
3133-
setTimeout(() => {
3134-
// For tabs that need to jump to specific sections
3135-
scrollToRelevantSection(stepNumber, tabId);
3136-
}, 300);
3135+
// Check if we're using sidebar navigation or tab navigation
3136+
const sidebarToggle = document.getElementById('admin-settings-toggle');
3137+
3138+
if (sidebarToggle) {
3139+
// Using sidebar navigation - call showAdminTab function
3140+
const tabName = tabId.replace('-tab', ''); // Remove '-tab' suffix
3141+
if (typeof showAdminTab === 'function') {
3142+
showAdminTab(tabName);
3143+
} else if (typeof window.showAdminTab === 'function') {
3144+
window.showAdminTab(tabName);
3145+
}
3146+
} else {
3147+
// Using Bootstrap tabs
3148+
const tab = document.getElementById(tabId);
3149+
if (tab) {
3150+
// Use bootstrap Tab to show the tab
3151+
const bootstrapTab = new bootstrap.Tab(tab);
3152+
bootstrapTab.show();
3153+
}
31373154
}
3155+
3156+
// Scroll to the relevant section after a small delay to allow tab to switch
3157+
setTimeout(() => {
3158+
scrollToRelevantSection(stepNumber, tabId);
3159+
}, 300);
31383160
}
31393161
}
31403162

@@ -3148,23 +3170,50 @@ function scrollToRelevantSection(stepNumber, tabId) {
31483170
let targetElement = null;
31493171

31503172
switch (stepNumber) {
3173+
case 1: // App title and logo
3174+
targetElement = document.getElementById('branding-section');
3175+
break;
3176+
case 2: // GPT settings
3177+
targetElement = document.getElementById('gpt-configuration');
3178+
break;
3179+
case 3: // GPT model selection
3180+
targetElement = document.getElementById('gpt_models_list')?.closest('.mb-3');
3181+
break;
31513182
case 4: // Workspaces toggle section
3152-
targetElement = document.getElementById('enable_user_workspace')?.closest('.card');
3183+
targetElement = document.getElementById('personal-workspaces-section');
3184+
break;
3185+
case 5: // Embedding settings
3186+
targetElement = document.getElementById('embeddings-configuration');
3187+
break;
3188+
case 6: // AI Search settings
3189+
targetElement = document.getElementById('azure-ai-search-section');
3190+
break;
3191+
case 7: // Document Intelligence settings
3192+
targetElement = document.getElementById('document-intelligence-section');
31533193
break;
31543194
case 8: // Video file support
31553195
targetElement = document.getElementById('enable_video_file_support')?.closest('.form-group');
31563196
break;
31573197
case 9: // Audio file support
31583198
targetElement = document.getElementById('enable_audio_file_support')?.closest('.form-group');
31593199
break;
3200+
case 10: // Content safety
3201+
targetElement = document.getElementById('content-safety-section');
3202+
break;
3203+
case 11: // User feedback and archiving
3204+
targetElement = document.getElementById('user-feedback-section');
3205+
break;
3206+
case 12: // Enhanced citations and image generation
3207+
targetElement = document.getElementById('enhanced-citations-section');
3208+
break;
31603209
default:
31613210
// For other steps, no specific scrolling
31623211
break;
31633212
}
31643213

31653214
// If we found a target element, scroll to it
31663215
if (targetElement) {
3167-
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
3216+
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
31683217
}
31693218
}
31703219

@@ -3302,9 +3351,14 @@ function isStepComplete(stepNumber) {
33023351

33033352
// Otherwise check settings
33043353
const speechEndpoint = document.getElementById('speech_service_endpoint')?.value;
3305-
const speechKey = document.getElementById('speech_service_key')?.value;
3354+
const authType = document.getElementById('speech_service_authentication_type').value;
3355+
const key = document.getElementById('speech_service_key').value;
33063356

3307-
return speechEndpoint && speechKey;
3357+
if (!speechEndpoint || (authType === 'key' && !key)) {
3358+
return false;
3359+
} else {
3360+
return true;
3361+
}
33083362

33093363
case 10: // Content safety - always complete (optional)
33103364
case 11: // User feedback and archiving - always complete (optional)

application/single_app/templates/admin_settings.html

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -369,18 +369,24 @@ <h4>9. Enable Audio File Support</h4>
369369
<strong>Required:</strong> Audio support configuration is required if workspaces are enabled.
370370
</div>
371371

372-
<p class="mb-3">Use the <strong>Audio Support</strong> settings in the Workspaces tab to enable and configure audio file processing.</p>
372+
<p class="mb-3">Use the <strong>Audio Support</strong> settings on the Search & Extract tab to enable and configure audio file processing.</p>
373373

374374
<ul class="list-group mb-4">
375375
<li class="list-group-item d-flex justify-content-between align-items-center">
376376
<span>Speech Service Endpoint</span>
377377
<span id="audio-endpoint-badge" class="badge bg-danger">Required</span>
378378
</li>
379379
<li class="list-group-item d-flex justify-content-between align-items-center">
380-
<span>Speech Service Key</span>
381-
<span id="audio-key-badge" class="badge bg-danger">Required</span>
380+
<span>Authentication</span>
381+
<span id="speech-auth-badge" class="badge bg-danger">Required</span>
382382
</li>
383383
</ul>
384+
385+
<div class="alert alert-info">
386+
<strong>Note:</strong> If using Managed Identity authentication ensure the Service Principal has been assigned the Cognitive Services Speech Contributor role on the Azure Speech Service and that the Speech endpoint is configured with a custom domain name.
387+
388+
<i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" title="When a Cognitive Service has a custom domain name, the endpoint will typically look like https://<resource-name>.cognitiveservices.azure.<com or us> instead of https://<location>.cognitiveservices.azure.<com or us>"></i>
389+
</div>
384390
</div>
385391

386392
<!-- Step 10: Content Safety -->
@@ -2831,7 +2837,7 @@ <h5>Speech Service Settings</h5>
28312837
<input type="text" class="form-control"
28322838
id="speech_service_endpoint" name="speech_service_endpoint"
28332839
value="{{ settings.speech_service_endpoint or '' }}"
2834-
placeholder="https://<location>.cognitiveservices.azure.<com or us>/">
2840+
placeholder="https://<location or custom domain>.cognitiveservices.azure.<com or us>/">
28352841
</div>
28362842

28372843
<div class="mb-3">
@@ -2849,17 +2855,41 @@ <h5>Speech Service Settings</h5>
28492855
</div>
28502856

28512857
<div class="mb-3">
2852-
<label for="speech_service_key" class="form-label">API Key</label>
2858+
<label for="speech_service_authentication_type" class="form-label">
2859+
Authentication Type
2860+
</label>
2861+
<select class="form-select" id="speech_service_authentication_type" name="speech_service_authentication_type">
2862+
<option value="key" {% if settings.speech_service_authentication_type == 'key' or not settings.speech_service_authentication_type %}selected{% endif %}>
2863+
Key
2864+
</option>
2865+
<option value="managed_identity" {% if settings.speech_service_authentication_type == 'managed_identity' %}selected{% endif %}>
2866+
Managed Identity
2867+
</option>
2868+
</select>
2869+
</div>
2870+
<div class="mb-3" id="speech_service_key_container" {% if settings.speech_service_authentication_type == 'managed_identity' %}style="display: none;"{% endif %}>
2871+
<label for="speech_service_key" class="form-label">
2872+
API Key
2873+
</label>
28532874
<div class="input-group">
2854-
<input type="password" class="form-control"
2855-
id="speech_service_key" name="speech_service_key"
2856-
value="{{ settings.speech_service_key or '' }}">
2857-
<button type="button" class="btn btn-outline-secondary"
2858-
id="toggle_speech_service_key">Show</button>
2875+
<input
2876+
type="password"
2877+
class="form-control"
2878+
id="speech_service_key"
2879+
name="speech_service_key"
2880+
value="{{ settings.speech_service_key or '' }}"
2881+
>
2882+
<button
2883+
type="button"
2884+
class="btn btn-outline-secondary"
2885+
id="toggle_speech_service_key"
2886+
>
2887+
Show
2888+
</button>
28592889
</div>
28602890
</div>
2861-
</div>
28622891

2892+
</div>
28632893
<p class="mt-2 mb-0">
28642894
<small class="text-muted">
28652895
<a href="#citation" onclick="switchTab(event, 'citation-tab')">

0 commit comments

Comments
 (0)