From 095d740da2ba171508402213d1142e22f3630a35 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Thu, 9 Apr 2026 12:30:21 -0400 Subject: [PATCH 1/3] fixed logic in tabular for if statements --- application/single_app/route_backend_chats.py | 4 +- ...ESULT_COVERAGE_REDUNDANT_COMPARISON_FIX.md | 40 ++++++++++++++ ...tabular_exhaustive_result_synthesis_fix.py | 52 +++++++++++++++++-- 3 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 docs/explanation/fixes/TABULAR_RESULT_COVERAGE_REDUNDANT_COMPARISON_FIX.md diff --git a/application/single_app/route_backend_chats.py b/application/single_app/route_backend_chats.py index 005fb67d..e16d7242 100644 --- a/application/single_app/route_backend_chats.py +++ b/application/single_app/route_backend_chats.py @@ -3922,7 +3922,7 @@ def get_tabular_result_coverage_summary(invocations): if total_matches is not None and returned_rows is not None: if returned_rows >= total_matches: coverage_summary['has_full_result_coverage'] = True - elif returned_rows < total_matches: + else: coverage_summary['has_partial_result_coverage'] = True distinct_count = parse_tabular_result_count(result_payload.get('distinct_count')) @@ -3930,7 +3930,7 @@ def get_tabular_result_coverage_summary(invocations): if distinct_count is not None and returned_values is not None: if returned_values >= distinct_count: coverage_summary['has_full_result_coverage'] = True - elif returned_values < distinct_count: + else: coverage_summary['has_partial_result_coverage'] = True if result_payload.get('full_rows_included') or result_payload.get('full_values_included'): diff --git a/docs/explanation/fixes/TABULAR_RESULT_COVERAGE_REDUNDANT_COMPARISON_FIX.md b/docs/explanation/fixes/TABULAR_RESULT_COVERAGE_REDUNDANT_COMPARISON_FIX.md new file mode 100644 index 00000000..4cdf10bc --- /dev/null +++ b/docs/explanation/fixes/TABULAR_RESULT_COVERAGE_REDUNDANT_COMPARISON_FIX.md @@ -0,0 +1,40 @@ +# Tabular Result Coverage Redundant Comparison Fix + +Fixed/Implemented in version: **0.241.007** + +## Issue Description + +The tabular result coverage helper in `application/single_app/route_backend_chats.py` used complementary `elif` comparisons immediately after `>=` checks when determining whether returned rows or distinct values covered the full result set. + +## Root Cause Analysis + +`parse_tabular_result_count()` normalizes these metadata fields to non-negative integers or `None`, and the helper already guards against `None` before comparing. Under those preconditions, `returned_rows >= total_matches` and `returned_values >= distinct_count` fully partition the remaining numeric cases, so the follow-up `<` tests were redundant and triggered static-analysis noise. + +## Technical Details + +### Files Modified + +- `application/single_app/route_backend_chats.py` +- `functional_tests/test_tabular_exhaustive_result_synthesis_fix.py` +- `application/single_app/config.py` + +### Code Changes Summary + +- Replaced the redundant complementary `elif` comparisons with `else` branches in `get_tabular_result_coverage_summary()`. +- Added regression coverage for partial distinct-value result slices so the touched branch remains explicitly exercised. +- Updated the application version to `0.241.007`. + +### Testing Approach + +- Extended the existing tabular exhaustive-result synthesis functional test to verify partial distinct-value coverage is still detected. + +## Validation + +### Expected Improvement + +- CodeQL no longer reports the redundant comparison finding for the tabular result coverage helper. +- Runtime behavior remains unchanged for valid parsed numeric counts. + +### Related Version Update + +- `application/single_app/config.py` updated to `0.241.007`. \ No newline at end of file diff --git a/functional_tests/test_tabular_exhaustive_result_synthesis_fix.py b/functional_tests/test_tabular_exhaustive_result_synthesis_fix.py index 564e6ec7..e976f120 100644 --- a/functional_tests/test_tabular_exhaustive_result_synthesis_fix.py +++ b/functional_tests/test_tabular_exhaustive_result_synthesis_fix.py @@ -2,13 +2,13 @@ # test_tabular_exhaustive_result_synthesis_fix.py """ Functional test for tabular exhaustive-result synthesis retry. -Version: 0.241.006 +Version: 0.241.007 Implemented in: 0.241.006 This test ensures exhaustive tabular requests retry when successful analytical -tool calls already returned the full matching result set or only a partial -slice, but the synthesis response still behaves as though only schema samples -are available. +tool calls already returned the full matching result set or only a partial row +or distinct-value slice, but the synthesis response still behaves as though +only schema samples are available. """ import ast @@ -178,10 +178,54 @@ def test_exhaustive_tabular_retry_detects_partial_result_slice(): return False +def test_result_coverage_summary_marks_partial_distinct_value_slices(): + """Verify distinct-value counts below the available total mark partial coverage.""" + print('πŸ” Testing tabular result coverage summary for partial distinct-value slices...') + + try: + helpers, _ = load_helpers() + get_tabular_result_coverage_summary = helpers['get_tabular_result_coverage_summary'] + + invocations = [ + SimpleNamespace( + function_name='get_distinct_tabular_values', + parameters={ + 'filename': 'sp800-53r5-control-catalog.xlsx', + 'column': 'Control Identifier', + 'max_values': '25', + }, + result=json.dumps({ + 'filename': 'sp800-53r5-control-catalog.xlsx', + 'selected_sheet': 'SP 800-53 Revision 5', + 'column': 'Control Identifier', + 'distinct_count': 1189, + 'returned_values': 25, + 'values': ['AC-1', 'AC-2'], + }), + error_message=None, + ) + ] + + coverage_summary = get_tabular_result_coverage_summary(invocations) + + assert coverage_summary['has_full_result_coverage'] is False, coverage_summary + assert coverage_summary['has_partial_result_coverage'] is True, coverage_summary + + print('βœ… Tabular result coverage summary marks partial distinct-value slices') + return True + + except Exception as exc: + print(f'❌ Test failed: {exc}') + import traceback + traceback.print_exc() + return False + + if __name__ == '__main__': tests = [ test_exhaustive_tabular_retry_detects_full_result_access_gap, test_exhaustive_tabular_retry_detects_partial_result_slice, + test_result_coverage_summary_marks_partial_distinct_value_slices, ] results = [] From a0770ec436c3c9a2fadaa8feb77c244fbf716122 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Thu, 9 Apr 2026 13:10:49 -0400 Subject: [PATCH 2/3] speech and video indexer config documentation updates --- .../artifacts/admin_settings.json | 4 +- application/single_app/config.py | 2 +- .../single_app/functions_authentication.py | 2 +- application/single_app/functions_documents.py | 28 + application/single_app/functions_settings.py | 1 + application/single_app/route_backend_tts.py | 34 +- .../route_frontend_admin_settings.py | 6 +- .../static/js/admin/admin_settings.js | 593 +++++++++++------- .../templates/_video_indexer_info.html | 49 +- .../single_app/templates/admin_settings.html | 147 ++++- docs/admin_configuration.md | 20 +- .../SPEECH_VIDEO_INDEXER_GUIDANCE_FIX.md | 75 +++ ...ure_speech_managed_identity_manul_setup.md | 20 +- docs/reference/admin_configuration.md | 16 +- docs/setup_instructions_special.md | 5 +- .../test_multimedia_support_reorganization.py | 206 +++--- ...deo_indexer_dual_authentication_support.py | 302 +++++---- ui_tests/test_admin_multimedia_guidance.py | 90 +++ 18 files changed, 1031 insertions(+), 569 deletions(-) create mode 100644 docs/explanation/fixes/v0.241.007/SPEECH_VIDEO_INDEXER_GUIDANCE_FIX.md create mode 100644 ui_tests/test_admin_multimedia_guidance.py diff --git a/application/external_apps/databaseseeder/artifacts/admin_settings.json b/application/external_apps/databaseseeder/artifacts/admin_settings.json index 897285cd..c0e6337f 100644 --- a/application/external_apps/databaseseeder/artifacts/admin_settings.json +++ b/application/external_apps/databaseseeder/artifacts/admin_settings.json @@ -119,14 +119,14 @@ "video_indexer_endpoint": "https://api.videoindexer.ai", "video_indexer_location": "", "video_indexer_account_id": "", - "video_indexer_api_key": "", "video_indexer_resource_group": "", "video_indexer_subscription_id": "", "video_indexer_account_name": "", - "video_indexer_arm_api_version": "2021-11-10-preview", + "video_indexer_arm_api_version": "2025-04-01", "video_index_timeout": 600, "speech_service_endpoint": "https://eastus.api.cognitive.microsoft.com", "speech_service_location": "eastus", + "speech_service_resource_id": "", "speech_service_locale": "en-US", "speech_service_key": "", "classification_banner_enabled": true, diff --git a/application/single_app/config.py b/application/single_app/config.py index 7196cfe8..3ccb6ca9 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.241.006" +VERSION = "0.241.007" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/functions_authentication.py b/application/single_app/functions_authentication.py index 8bdf4b5c..a0ecde0a 100644 --- a/application/single_app/functions_authentication.py +++ b/application/single_app/functions_authentication.py @@ -385,7 +385,7 @@ def get_video_indexer_managed_identity_token(settings, video_id=None): rg = settings["video_indexer_resource_group"] sub = settings["video_indexer_subscription_id"] acct = settings["video_indexer_account_name"] - api_ver = settings.get("video_indexer_arm_api_version", "2021-11-10-preview") + api_ver = settings.get("video_indexer_arm_api_version", DEFAULT_VIDEO_INDEXER_ARM_API_VERSION) debug_print(f"[VIDEO INDEXER AUTH] Settings extracted - Subscription: {sub}, Resource Group: {rg}, Account: {acct}, API Version: {api_ver}") diff --git a/application/single_app/functions_documents.py b/application/single_app/functions_documents.py index 7bff48d8..2f2f46e3 100644 --- a/application/single_app/functions_documents.py +++ b/application/single_app/functions_documents.py @@ -6242,6 +6242,34 @@ def _get_speech_config(settings, endpoint: str, locale: str): print(f"[Debug] Speech config obtained successfully", flush=True) return speech_config + +def get_speech_synthesis_config(settings, endpoint: str, location: str): + """Get speech synthesis config for either key or managed identity auth.""" + auth_type = settings.get("speech_service_authentication_type") + + if auth_type == "managed_identity": + resource_id = (settings.get("speech_service_resource_id") or "").strip() + if not location: + raise ValueError("Speech service location is required for text-to-speech with managed identity.") + if not resource_id: + raise ValueError("Speech service resource ID is required for text-to-speech with managed identity.") + + credential = DefaultAzureCredential() + token = credential.get_token(cognitive_services_scope) + authorization_token = f"aad#{resource_id}#{token.token}" + speech_config = speechsdk.SpeechConfig(auth_token=authorization_token, region=location) + else: + key = (settings.get("speech_service_key") or "").strip() + if not endpoint: + raise ValueError("Speech service endpoint is required for text-to-speech.") + if not key: + raise ValueError("Speech service key is required for text-to-speech when using key authentication.") + + speech_config = speechsdk.SpeechConfig(endpoint=endpoint, subscription=key) + + print(f"[Debug] Speech synthesis config obtained successfully", flush=True) + return speech_config + def process_audio_document( document_id: str, user_id: str, diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py index 324f82fc..271caa5f 100644 --- a/application/single_app/functions_settings.py +++ b/application/single_app/functions_settings.py @@ -372,6 +372,7 @@ def get_settings(use_cosmos=False, include_source=False): # Audio file settings with Azure speech service "speech_service_endpoint": '', "speech_service_location": '', + "speech_service_resource_id": '', "speech_service_locale": "en-US", "speech_service_key": "", "speech_service_authentication_type": "key", # 'key' or 'managed_identity' diff --git a/application/single_app/route_backend_tts.py b/application/single_app/route_backend_tts.py index 11d14cc3..61830490 100644 --- a/application/single_app/route_backend_tts.py +++ b/application/single_app/route_backend_tts.py @@ -2,6 +2,8 @@ from config import * from functions_authentication import * +from functions_appinsights import log_event +from functions_documents import get_speech_synthesis_config from functions_settings import * from functions_debug import debug_print from swagger_wrapper import swagger_route, get_auth_security @@ -41,14 +43,26 @@ def synthesize_speech(): return jsonify({"error": "Text-to-speech is not enabled"}), 403 # Validate speech service configuration - speech_key = settings.get('speech_service_key', '') - speech_region = settings.get('speech_service_location', '') + speech_endpoint = (settings.get('speech_service_endpoint') or '').strip().rstrip('/') + speech_region = (settings.get('speech_service_location') or '').strip() + speech_auth_type = settings.get('speech_service_authentication_type', 'key') - if not speech_key or not speech_region: - debug_print("[TTS] Speech service not configured - missing key or region") + if not speech_endpoint: + debug_print("[TTS] Speech service not configured - missing endpoint") + return jsonify({"error": "Speech service not configured"}), 500 + + if speech_auth_type == 'key' and not (settings.get('speech_service_key') or '').strip(): + debug_print("[TTS] Speech service not configured - missing key for key authentication") + return jsonify({"error": "Speech service not configured"}), 500 + + if speech_auth_type == 'managed_identity' and not speech_region: + debug_print("[TTS] Speech service not configured - missing location for managed identity") return jsonify({"error": "Speech service not configured"}), 500 - debug_print(f"[TTS] Speech service configured - region: {speech_region}") + debug_print( + f"[TTS] Speech service configured - auth_type: {speech_auth_type}, " + f"endpoint: {speech_endpoint}, location: {speech_region or 'n/a'}" + ) # Parse request data data = request.get_json() @@ -71,10 +85,12 @@ def synthesize_speech(): debug_print(f"[TTS] Request params - voice: {voice}, speed: {speed}, text_length: {len(text)}") # Configure speech service - speech_config = speechsdk.SpeechConfig( - subscription=speech_key, - region=speech_region - ) + try: + speech_config = get_speech_synthesis_config(settings, speech_endpoint, speech_region) + except ValueError as config_error: + debug_print(f"[TTS] Speech service configuration invalid: {str(config_error)}") + return jsonify({"error": str(config_error)}), 500 + speech_config.speech_synthesis_voice_name = voice # Set output format to high quality diff --git a/application/single_app/route_frontend_admin_settings.py b/application/single_app/route_frontend_admin_settings.py index 94053752..28d706f3 100644 --- a/application/single_app/route_frontend_admin_settings.py +++ b/application/single_app/route_frontend_admin_settings.py @@ -367,6 +367,9 @@ def admin_settings(): 'admin_settings.html', app_settings=settings_for_template, settings=settings_for_template, + azure_environment=AZURE_ENVIRONMENT, + default_video_indexer_endpoint=video_indexer_endpoint, + default_video_indexer_arm_api_version=DEFAULT_VIDEO_INDEXER_ARM_API_VERSION, user_settings=user_settings, update_available=update_available, latest_version=latest_version, @@ -1325,12 +1328,13 @@ def is_valid_url(url): 'video_indexer_resource_group': form_data.get('video_indexer_resource_group', '').strip(), 'video_indexer_subscription_id': form_data.get('video_indexer_subscription_id', '').strip(), 'video_indexer_account_name': form_data.get('video_indexer_account_name', '').strip(), - 'video_indexer_arm_api_version': form_data.get('video_indexer_arm_api_version', '2024-01-01').strip(), + 'video_indexer_arm_api_version': form_data.get('video_indexer_arm_api_version', DEFAULT_VIDEO_INDEXER_ARM_API_VERSION).strip(), 'video_index_timeout': int(form_data.get('video_index_timeout', 600)), # Audio file settings with Azure speech service 'speech_service_endpoint': form_data.get('speech_service_endpoint', '').strip(), 'speech_service_location': form_data.get('speech_service_location', '').strip(), + 'speech_service_resource_id': form_data.get('speech_service_resource_id', '').strip(), 'speech_service_locale': form_data.get('speech_service_locale', '').strip(), 'speech_service_authentication_type': form_data.get('speech_service_authentication_type', 'key'), 'speech_service_key': form_data.get('speech_service_key', '').strip(), diff --git a/application/single_app/static/js/admin/admin_settings.js b/application/single_app/static/js/admin/admin_settings.js index 896bf6b3..2d93fb65 100644 --- a/application/single_app/static/js/admin/admin_settings.js +++ b/application/single_app/static/js/admin/admin_settings.js @@ -1994,10 +1994,18 @@ function setupToggles() { } const speechAuthType = document.getElementById('speech_service_authentication_type'); + const speechKeyContainer = document.getElementById('speech_service_key_container'); + const speechResourceIdContainer = document.getElementById('speech_service_resource_id_container'); if (speechAuthType) { + const updateSpeechAuthFields = function () { + const usingKeyAuth = this.value === 'key'; + setSectionVisibility(speechKeyContainer, usingKeyAuth); + setSectionVisibility(speechResourceIdContainer, !usingKeyAuth); + }; + + updateSpeechAuthFields.call(speechAuthType); speechAuthType.addEventListener('change', function () { - document.getElementById('speech_service_key_container').style.display = - (this.value === 'key') ? 'block' : 'none'; + updateSpeechAuthFields.call(this); markFormAsModified(); }); } @@ -3434,29 +3442,104 @@ function togglePassword(btnId, inputId) { } } +function setSectionVisibility(element, visible) { + if (!element) { + return; + } + + element.classList.toggle('d-none', !visible); +} + // --- Video Indexer Settings toggle --- const videoSupportToggle = document.getElementById('enable_video_file_support'); -const videoIndexerDiv = document.getElementById('video_indexer_settings'); +const videoIndexerDiv = document.getElementById('video_indexer_settings'); +const videoIndexerCloudSelect = document.getElementById('video_indexer_cloud'); +const videoIndexerEndpointInput = document.getElementById('video_indexer_endpoint'); +const videoIndexerEndpointDisplay = document.getElementById('video_indexer_endpoint_display'); +const videoIndexerCustomEndpointGroup = document.getElementById('video_indexer_custom_endpoint_group'); +const videoIndexerCustomEndpointInput = document.getElementById('video_indexer_custom_endpoint'); +const videoIndexerCloudMismatchAlert = document.getElementById('video_indexer_cloud_mismatch_alert'); + +function updateVideoIndexerEndpointSelection() { + if (!videoIndexerCloudSelect || !videoIndexerEndpointInput) { + return; + } + + const selectedCloud = videoIndexerCloudSelect.value; + const publicEndpoint = videoIndexerCloudSelect.dataset.publicEndpoint || 'https://api.videoindexer.ai'; + const governmentEndpoint = videoIndexerCloudSelect.dataset.governmentEndpoint || 'https://api.videoindexer.ai.azure.us'; + const runtimeCloud = videoIndexerCloudSelect.dataset.runtimeCloud || 'public'; + + let endpointValue = publicEndpoint; + if (selectedCloud === 'usgovernment') { + endpointValue = governmentEndpoint; + } else if (selectedCloud === 'custom') { + endpointValue = videoIndexerCustomEndpointInput?.value?.trim() || ''; + } + + videoIndexerEndpointInput.value = endpointValue; + + if (videoIndexerEndpointDisplay) { + videoIndexerEndpointDisplay.value = endpointValue; + } + + setSectionVisibility(videoIndexerCustomEndpointGroup, selectedCloud === 'custom'); + setSectionVisibility(videoIndexerCloudMismatchAlert, selectedCloud !== runtimeCloud); + + if (typeof updateVideoIndexerModalInfo === 'function') { + updateVideoIndexerModalInfo(); + } +} + if (videoSupportToggle && videoIndexerDiv) { - // on load - videoIndexerDiv.style.display = videoSupportToggle.checked ? 'block' : 'none'; - // on change - videoSupportToggle.addEventListener('change', () => { - videoIndexerDiv.style.display = videoSupportToggle.checked ? 'block' : 'none'; - markFormAsModified(); - }); + setSectionVisibility(videoIndexerDiv, videoSupportToggle.checked); + videoSupportToggle.addEventListener('change', () => { + setSectionVisibility(videoIndexerDiv, videoSupportToggle.checked); + markFormAsModified(); + }); +} + +if (videoIndexerCloudSelect) { + updateVideoIndexerEndpointSelection(); + videoIndexerCloudSelect.addEventListener('change', () => { + updateVideoIndexerEndpointSelection(); + markFormAsModified(); + }); +} + +if (videoIndexerCustomEndpointInput) { + videoIndexerCustomEndpointInput.addEventListener('input', () => { + updateVideoIndexerEndpointSelection(); + markFormAsModified(); + }); } // --- Speech Service Settings toggle --- -const audioSupportToggle = document.getElementById('enable_audio_file_support'); -const audioServiceDiv = document.getElementById('audio_service_settings'); -if (audioSupportToggle && audioServiceDiv) { - // initial visibility - audioServiceDiv.style.display = audioSupportToggle.checked ? 'block' : 'none'; - audioSupportToggle.addEventListener('change', () => { - audioServiceDiv.style.display = audioSupportToggle.checked ? 'block' : 'none'; - markFormAsModified(); - }); +const audioSupportToggle = document.getElementById('enable_audio_file_support'); +const speechToTextToggle = document.getElementById('enable_speech_to_text_input'); +const textToSpeechToggle = document.getElementById('enable_text_to_speech'); +const audioServiceDiv = document.getElementById('audio_service_settings'); + +function areAnySpeechFeaturesEnabled() { + return [audioSupportToggle, speechToTextToggle, textToSpeechToggle].some((toggle) => Boolean(toggle?.checked)); +} + +function updateSpeechServiceSettingsVisibility() { + setSectionVisibility(audioServiceDiv, areAnySpeechFeaturesEnabled()); +} + +if (audioServiceDiv) { + updateSpeechServiceSettingsVisibility(); + [audioSupportToggle, speechToTextToggle, textToSpeechToggle].forEach((toggle) => { + if (!toggle) { + return; + } + + toggle.addEventListener('change', () => { + updateSpeechServiceSettingsVisibility(); + markFormAsModified(); + }); + }); } // Metadata Extraction UI @@ -3495,12 +3578,12 @@ function populateExtractionModels() { } if (extractToggle) { - // show/hide the model dropdown - extractModelDiv.style.display = extractToggle.checked ? 'block' : 'none'; - extractToggle.addEventListener('change', () => { + // show/hide the model dropdown extractModelDiv.style.display = extractToggle.checked ? 'block' : 'none'; - markFormAsModified(); - }); + extractToggle.addEventListener('change', () => { + extractModelDiv.style.display = extractToggle.checked ? 'block' : 'none'; + markFormAsModified(); + }); } // Multi-Modal Vision UI @@ -3509,232 +3592,232 @@ const visionModelDiv = document.getElementById('multimodal_vision_model_settings const visionSelect = document.getElementById('multimodal_vision_model'); function populateVisionModels() { - if (!visionSelect) return; + if (!visionSelect) return; - // remember previously chosen value - const prev = visionSelect.getAttribute('data-prev') || ''; - - // clear out old options (except the placeholder) - visionSelect.innerHTML = ''; - - if (document.getElementById('enable_gpt_apim').checked) { - // use comma-separated APIM deployments - const text = document.getElementById('azure_apim_gpt_deployment').value || ''; - text.split(',') - .map(s => s.trim()) - .filter(s => s) - .forEach(d => { - const opt = new Option(d, d); - visionSelect.add(opt); + // remember previously chosen value + const prev = visionSelect.getAttribute('data-prev') || ''; + + // clear out old options (except the placeholder) + visionSelect.innerHTML = ''; + + if (document.getElementById('enable_gpt_apim').checked) { + // use comma-separated APIM deployments + const text = document.getElementById('azure_apim_gpt_deployment').value || ''; + text.split(',') + .map(s => s.trim()) + .filter(s => s) + .forEach(d => { + const opt = new Option(d, d); + visionSelect.add(opt); + }); + } else { + // use direct GPT selected deployments - filter for vision-capable models + (window.gptSelected || []).forEach(m => { + // Only include models with vision capabilities + // Vision-enabled models per Azure OpenAI docs: + // - o-series reasoning models (o1, o3, etc.) + // - GPT-5 series + // - GPT-4.1 series + // - GPT-4.5 + // - GPT-4o series (gpt-4o, gpt-4o-mini) + // - GPT-4 vision models (gpt-4-vision, gpt-4-turbo-vision) + const modelNameLower = (m.modelName || '').toLowerCase(); + const isVisionCapable = + modelNameLower.includes('vision') || + modelNameLower.includes('gpt-4o') || + modelNameLower.includes('gpt-4.1') || + modelNameLower.includes('gpt-4.5') || + modelNameLower.includes('gpt-5') || + modelNameLower.match(/^o\d+/) || + modelNameLower.includes('o1-') || + modelNameLower.includes('o3-'); + + if (isVisionCapable) { + const label = `${m.deploymentName} (${m.modelName})`; + const opt = new Option(label, m.deploymentName); + visionSelect.add(opt); + } }); - } else { - // use direct GPT selected deployments - filter for vision-capable models - (window.gptSelected || []).forEach(m => { - // Only include models with vision capabilities - // Vision-enabled models per Azure OpenAI docs: - // - o-series reasoning models (o1, o3, etc.) - // - GPT-5 series - // - GPT-4.1 series - // - GPT-4.5 - // - GPT-4o series (gpt-4o, gpt-4o-mini) - // - GPT-4 vision models (gpt-4-vision, gpt-4-turbo-vision) - const modelNameLower = (m.modelName || '').toLowerCase(); - const isVisionCapable = - modelNameLower.includes('vision') || // gpt-4-vision, gpt-4-turbo-vision - modelNameLower.includes('gpt-4o') || // gpt-4o, gpt-4o-mini - modelNameLower.includes('gpt-4.1') || // gpt-4.1 series - modelNameLower.includes('gpt-4.5') || // gpt-4.5 - modelNameLower.includes('gpt-5') || // gpt-5 series - modelNameLower.match(/^o\d+/) || // o1, o3, etc. (o-series) - modelNameLower.includes('o1-') || // o1-preview, o1-mini - modelNameLower.includes('o3-'); // o3-mini, etc. - - if (isVisionCapable) { - const label = `${m.deploymentName} (${m.modelName})`; - const opt = new Option(label, m.deploymentName); - visionSelect.add(opt); - } - }); - } + } - // restore previous - if (prev) { - visionSelect.value = prev; - } + // restore previous + if (prev) { + visionSelect.value = prev; + } } if (visionToggle && visionModelDiv) { - // show/hide the model dropdown - visionModelDiv.style.display = visionToggle.checked ? 'block' : 'none'; - visionToggle.addEventListener('change', () => { + // show/hide the model dropdown visionModelDiv.style.display = visionToggle.checked ? 'block' : 'none'; - markFormAsModified(); - }); + visionToggle.addEventListener('change', () => { + visionModelDiv.style.display = visionToggle.checked ? 'block' : 'none'; + markFormAsModified(); + }); } // Listen for vision model selection changes if (visionSelect) { - visionSelect.addEventListener('change', () => { - // Update data-prev to remember the selection - visionSelect.setAttribute('data-prev', visionSelect.value); - markFormAsModified(); - }); + visionSelect.addEventListener('change', () => { + // Update data-prev to remember the selection + visionSelect.setAttribute('data-prev', visionSelect.value); + markFormAsModified(); + }); } -// when APIM‐toggle flips, repopulate +// when APIM-toggle flips, repopulate const apimToggle = document.getElementById('enable_gpt_apim'); if (apimToggle) { - apimToggle.addEventListener('change', () => { - populateExtractionModels(); - populateVisionModels(); - }); + apimToggle.addEventListener('change', () => { + populateExtractionModels(); + populateVisionModels(); + }); } // on load, stash previous & populate document.addEventListener('DOMContentLoaded', () => { - if (extractSelect) { - extractSelect.setAttribute('data-prev', extractSelect.value); - populateExtractionModels(); - } - if (visionSelect) { - visionSelect.setAttribute('data-prev', visionSelect.value); - populateVisionModels(); - } + if (extractSelect) { + extractSelect.setAttribute('data-prev', extractSelect.value); + populateExtractionModels(); + } + if (visionSelect) { + visionSelect.setAttribute('data-prev', visionSelect.value); + populateVisionModels(); + } }); document.addEventListener('DOMContentLoaded', () => { - ['user','group','public'].forEach(type => { - const warnDiv = document.getElementById(`index-warning-${type}`); - const missingSpan = document.getElementById(`missing-fields-${type}`); - const fixBtn = document.getElementById(`fix-${type}-index-btn`); + ['user','group','public'].forEach(type => { + const warnDiv = document.getElementById(`index-warning-${type}`); + const missingSpan = document.getElementById(`missing-fields-${type}`); + const fixBtn = document.getElementById(`fix-${type}-index-btn`); - // 1) check for missing fields - fetch('/api/admin/settings/check_index_fields', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin', - body: JSON.stringify({ indexType: type }) - }) - .then(r => { - if (!r.ok) { - return r.json().then(errorData => { - throw new Error(errorData.error || `HTTP ${r.status}: ${r.statusText}`); - }); - } - return r.json(); - }) - .then(response => { - if (response.autoFixed) { - // Fields were automatically fixed - console.log(`βœ… Auto-fixed ${type} index: added ${response.fieldsAdded.length} field(s):`, response.fieldsAdded.join(', ')); - if (warnDiv) { - warnDiv.className = 'alert alert-success'; - missingSpan.textContent = `Automatically added ${response.fieldsAdded.length} field(s): ${response.fieldsAdded.join(', ')}`; - warnDiv.style.display = 'block'; - if (fixBtn) fixBtn.style.display = 'none'; - - // Hide success message after 5 seconds - setTimeout(() => { - warnDiv.style.display = 'none'; - }, 5000); - } - } else if (response.autoFixFailed) { - // Auto-fix failed, show manual button - console.warn(`Auto-fix failed for ${type} index:`, response.error); - missingSpan.textContent = response.missingFields.join(', ') + ' (Auto-fix failed - please fix manually)'; - warnDiv.className = 'alert alert-warning'; - warnDiv.style.display = 'block'; - if (fixBtn) { - fixBtn.textContent = `Fix ${type} Index Fields`; - fixBtn.style.display = 'inline-block'; - } - } else if (response.missingFields && response.missingFields.length > 0) { - // Missing fields but auto-fix was disabled - missingSpan.textContent = response.missingFields.join(', '); - warnDiv.className = 'alert alert-warning'; - warnDiv.style.display = 'block'; - if (fixBtn) { - fixBtn.textContent = `Fix ${type} Index Fields`; - fixBtn.style.display = 'inline-block'; - } - } else if (response.indexExists) { - // Index exists and is complete - if (warnDiv) warnDiv.style.display = 'none'; - console.log(`${type} index is properly configured`); - } - }) - .catch(err => { - console.warn(`Checking ${type} index fields:`, err.message); + // 1) check for missing fields + fetch('/api/admin/settings/check_index_fields', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'same-origin', + body: JSON.stringify({ indexType: type }) + }) + .then(r => { + if (!r.ok) { + return r.json().then(errorData => { + throw new Error(errorData.error || `HTTP ${r.status}: ${r.statusText}`); + }); + } + return r.json(); + }) + .then(response => { + if (response.autoFixed) { + // Fields were automatically fixed + console.log(`βœ… Auto-fixed ${type} index: added ${response.fieldsAdded.length} field(s):`, response.fieldsAdded.join(', ')); + if (warnDiv) { + warnDiv.className = 'alert alert-success'; + missingSpan.textContent = `Automatically added ${response.fieldsAdded.length} field(s): ${response.fieldsAdded.join(', ')}`; + warnDiv.style.display = 'block'; + if (fixBtn) fixBtn.style.display = 'none'; + + // Hide success message after 5 seconds + setTimeout(() => { + warnDiv.style.display = 'none'; + }, 5000); + } + } else if (response.autoFixFailed) { + // Auto-fix failed, show manual button + console.warn(`Auto-fix failed for ${type} index:`, response.error); + missingSpan.textContent = response.missingFields.join(', ') + ' (Auto-fix failed - please fix manually)'; + warnDiv.className = 'alert alert-warning'; + warnDiv.style.display = 'block'; + if (fixBtn) { + fixBtn.textContent = `Fix ${type} Index Fields`; + fixBtn.style.display = 'inline-block'; + } + } else if (response.missingFields && response.missingFields.length > 0) { + // Missing fields but auto-fix was disabled + missingSpan.textContent = response.missingFields.join(', '); + warnDiv.className = 'alert alert-warning'; + warnDiv.style.display = 'block'; + if (fixBtn) { + fixBtn.textContent = `Fix ${type} Index Fields`; + fixBtn.style.display = 'inline-block'; + } + } else if (response.indexExists) { + // Index exists and is complete + if (warnDiv) warnDiv.style.display = 'none'; + console.log(`${type} index is properly configured`); + } + }) + .catch(err => { + console.warn(`Checking ${type} index fields:`, err.message); - // Check if this is an index not found error - if (err.message.includes('does not exist yet') || err.message.includes('not found')) { - // Show a different message for missing index - if (warnDiv && missingSpan && fixBtn) { - missingSpan.textContent = `Index "${type}" does not exist yet`; - warnDiv.style.display = 'block'; - fixBtn.textContent = `Create ${type} Index`; - fixBtn.style.display = 'inline-block'; - fixBtn.dataset.action = 'create'; - } - } else if (err.message.includes('not configured')) { - // Azure AI Search not configured - if (warnDiv && missingSpan) { - missingSpan.textContent = 'Azure AI Search not configured'; - warnDiv.style.display = 'block'; - if (fixBtn) fixBtn.style.display = 'none'; - } - } else { - // Hide the warning div for other errors - if (warnDiv) warnDiv.style.display = 'none'; - } - }); + // Check if this is an index not found error + if (err.message.includes('does not exist yet') || err.message.includes('not found')) { + // Show a different message for missing index + if (warnDiv && missingSpan && fixBtn) { + missingSpan.textContent = `Index "${type}" does not exist yet`; + warnDiv.style.display = 'block'; + fixBtn.textContent = `Create ${type} Index`; + fixBtn.style.display = 'inline-block'; + fixBtn.dataset.action = 'create'; + } + } else if (err.message.includes('not configured')) { + // Azure AI Search not configured + if (warnDiv && missingSpan) { + missingSpan.textContent = 'Azure AI Search not configured'; + warnDiv.style.display = 'block'; + if (fixBtn) fixBtn.style.display = 'none'; + } + } else { + // Hide the warning div for other errors + if (warnDiv) warnDiv.style.display = 'none'; + } + }); - // 2) wire up the β€œfix” button - fixBtn.addEventListener('click', () => { - fixBtn.disabled = true; - const action = fixBtn.dataset.action || 'fix'; - const endpoint = action === 'create' ? '/api/admin/settings/create_index' : '/api/admin/settings/fix_index_fields'; - const actionText = action === 'create' ? 'Creating' : 'Fixing'; + // 2) wire up the fix button + fixBtn.addEventListener('click', () => { + fixBtn.disabled = true; + const action = fixBtn.dataset.action || 'fix'; + const endpoint = action === 'create' ? '/api/admin/settings/create_index' : '/api/admin/settings/fix_index_fields'; + const actionText = action === 'create' ? 'Creating' : 'Fixing'; - fixBtn.textContent = `${actionText}...`; + fixBtn.textContent = `${actionText}...`; - fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - credentials: 'same-origin', - body: JSON.stringify({ indexType: type }) - }) - .then(r => { - if (!r.ok) { - return r.json().then(errorData => { - throw new Error(errorData.error || `HTTP ${r.status}: ${r.statusText}`); + fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'same-origin', + body: JSON.stringify({ indexType: type }) + }) + .then(r => { + if (!r.ok) { + return r.json().then(errorData => { + throw new Error(errorData.error || `HTTP ${r.status}: ${r.statusText}`); + }); + } + return r.json(); + }) + .then(resp => { + if (resp.status === 'success') { + alert(resp.message || `Successfully ${action === 'create' ? 'created' : 'fixed'} ${type} index!`); + window.location.reload(); + } else { + alert(`Failed to ${action} ${type} index: ${resp.error}`); + fixBtn.disabled = false; + fixBtn.textContent = `${action === 'create' ? 'Create' : 'Fix'} ${type} Index`; + } + }) + .catch(err => { + alert(`Error ${action === 'create' ? 'creating' : 'fixing'} ${type} index: ${err.message || err}`); + fixBtn.disabled = false; + fixBtn.textContent = `${action === 'create' ? 'Create' : 'Fix'} ${type} Index`; + }); }); - } - return r.json(); - }) - .then(resp => { - if (resp.status === 'success') { - alert(resp.message || `Successfully ${action === 'create' ? 'created' : 'fixed'} ${type} index!`); - window.location.reload(); - } else { - alert(`Failed to ${action} ${type} index: ${resp.error}`); - fixBtn.disabled = false; - fixBtn.textContent = `${action === 'create' ? 'Create' : 'Fix'} ${type} Index`; - } - }) - .catch(err => { - alert(`Error ${action === 'create' ? 'creating' : 'fixing'} ${type} index: ${err.message || err}`); - fixBtn.disabled = false; - fixBtn.textContent = `${action === 'create' ? 'Create' : 'Fix'} ${type} Index`; }); - }); }); - }); togglePassword('toggle_gpt_key', 'azure_openai_gpt_key'); @@ -3756,7 +3839,6 @@ togglePassword('toggle_audio_files_key', 'audio_files_key'); togglePassword('toggle_office_conn_str', 'office_docs_storage_account_blob_endpoint'); togglePassword('toggle_video_conn_str', 'video_files_storage_account_url'); togglePassword('toggle_audio_conn_str', 'audio_files_storage_account_url'); -togglePassword('toggle_video_indexer_api_key', 'video_indexer_api_key'); togglePassword('toggle_speech_service_key', 'speech_service_key'); togglePassword('toggle_redis_key', 'redis_key'); togglePassword('toggle_azure_apim_redis_subscription_key', 'azure_apim_redis_subscription_key'); @@ -4050,6 +4132,9 @@ function calculateAvailableWalkthroughSteps() { const videoEnabled = document.getElementById('enable_video_file_support')?.checked || false; const audioEnabled = document.getElementById('enable_audio_file_support')?.checked || false; + const speechToTextEnabled = document.getElementById('enable_speech_to_text_input')?.checked || false; + const textToSpeechEnabled = document.getElementById('enable_text_to_speech')?.checked || false; + const speechFeaturesEnabled = audioEnabled || speechToTextEnabled || textToSpeechEnabled; const availableSteps = [1, 2, 3, 4]; // Base steps always available @@ -4060,10 +4145,10 @@ function calculateAvailableWalkthroughSteps() { if (videoEnabled) { availableSteps.push(8); // Video support } - - if (audioEnabled) { - availableSteps.push(9); // Audio support - } + } + + if (speechFeaturesEnabled) { + availableSteps.push(9); // Shared Speech Service } // Optional steps always available @@ -4123,8 +4208,10 @@ function findNextApplicableStep(currentStep) { case 9: // Audio support const audioEnabled = document.getElementById('enable_audio_file_support')?.checked || false; - if (!workspacesEnabled || !audioEnabled) { - // Skip this step if workspaces not enabled or audio not enabled + const speechToTextEnabled = document.getElementById('enable_speech_to_text_input')?.checked || false; + const textToSpeechEnabled = document.getElementById('enable_text_to_speech')?.checked || false; + if (!(audioEnabled || speechToTextEnabled || textToSpeechEnabled)) { + // Skip this step if no speech features are enabled nextStep++; continue; } @@ -4390,25 +4477,48 @@ function isStepComplete(stepNumber) { const videoEndpoint = document.getElementById('video_indexer_endpoint')?.value; const videoLocation = document.getElementById('video_indexer_location')?.value; const videoAccountId = document.getElementById('video_indexer_account_id')?.value; - - return videoLocation && videoAccountId && videoEndpoint; + const videoResourceGroup = document.getElementById('video_indexer_resource_group')?.value; + const videoSubscriptionId = document.getElementById('video_indexer_subscription_id')?.value; + const videoAccountName = document.getElementById('video_indexer_account_name')?.value; + + return Boolean( + videoLocation && + videoAccountId && + videoEndpoint && + videoResourceGroup && + videoSubscriptionId && + videoAccountName + ); case 9: // Audio support const audioEnabled = document.getElementById('enable_audio_file_support').checked || false; + const speechToTextEnabled = document.getElementById('enable_speech_to_text_input')?.checked || false; + const textToSpeechEnabled = document.getElementById('enable_text_to_speech')?.checked || false; + const speechFeaturesEnabled = audioEnabled || speechToTextEnabled || textToSpeechEnabled; - // If workspaces not enabled or audio not enabled, it's always complete - if (!workspacesEnabled || !audioEnabled) return true; + // If no speech features are enabled, it's always complete + if (!speechFeaturesEnabled) return true; // Otherwise check settings const speechEndpoint = document.getElementById('speech_service_endpoint')?.value; const authType = document.getElementById('speech_service_authentication_type').value; const key = document.getElementById('speech_service_key').value; - - if (!speechEndpoint || (authType === 'key' && !key)) { - return false; - } else { - return true; + const speechLocation = document.getElementById('speech_service_location')?.value; + const speechResourceId = document.getElementById('speech_service_resource_id')?.value; + + if (!speechEndpoint) { + return false; } + + if (authType === 'key') { + return Boolean(key); + } + + if (textToSpeechEnabled) { + return Boolean(speechLocation && speechResourceId); + } + + return true; case 10: // Content safety - always complete (optional) case 11: // User feedback and archiving - always complete (optional) @@ -4608,14 +4718,23 @@ function setupWalkthroughFieldListeners() { ], 8: [ // Video settings {selector: '#enable_video_file_support', event: 'change'}, + {selector: '#video_indexer_cloud', event: 'change'}, + {selector: '#video_indexer_custom_endpoint', event: 'input'}, {selector: '#video_indexer_location', event: 'input'}, {selector: '#video_indexer_account_id', event: 'input'}, - {selector: '#video_indexer_api_key', event: 'input'} + {selector: '#video_indexer_resource_group', event: 'input'}, + {selector: '#video_indexer_subscription_id', event: 'input'}, + {selector: '#video_indexer_account_name', event: 'input'} ], 9: [ // Audio settings {selector: '#enable_audio_file_support', event: 'change'}, + {selector: '#enable_speech_to_text_input', event: 'change'}, + {selector: '#enable_text_to_speech', event: 'change'}, {selector: '#speech_service_endpoint', event: 'input'}, - {selector: '#speech_service_key', event: 'input'} + {selector: '#speech_service_authentication_type', event: 'change'}, + {selector: '#speech_service_key', event: 'input'}, + {selector: '#speech_service_location', event: 'input'}, + {selector: '#speech_service_resource_id', event: 'input'} ] }; diff --git a/application/single_app/templates/_video_indexer_info.html b/application/single_app/templates/_video_indexer_info.html index 904ef900..cd806e30 100644 --- a/application/single_app/templates/_video_indexer_info.html +++ b/application/single_app/templates/_video_indexer_info.html @@ -12,12 +12,12 @@