Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions application/single_app/config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -122,11 +122,49 @@
CLIENTS = {}
CLIENTS_LOCK = threading.Lock()

ALLOWED_EXTENSIONS = {
'txt', 'pdf', 'doc', 'docm', 'docx', 'xlsx', 'xlsm', 'xls', 'csv', 'pptx', 'html', 'jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'heif', 'md', 'json',
'mp4', 'mov', 'avi', 'mkv', 'flv', 'mxf', 'gxf', 'ts', 'ps', '3gp', '3gpp', 'mpg', 'wmv', 'asf', 'm4a', 'm4v', 'isma', 'ismv',
'dvr-ms', 'wav', 'xml', 'yaml', 'yml', 'log'
# Base allowed extensions (always available)
BASE_ALLOWED_EXTENSIONS = {'txt', 'doc', 'docm', 'html', 'md', 'json', 'xml', 'yaml', 'yml', 'log'}
DOCUMENT_EXTENSIONS = {'pdf', 'docx', 'pptx', 'ppt'}
TABULAR_EXTENSIONS = {'csv', 'xlsx', 'xls', 'xlsm'}

# Updates to image, video, or audio extensions should also be made in static/js/chat/chat-enhanced-citations.js if the new file types can be natively rendered in the browser.
IMAGE_EXTENSIONS = {'jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'heif', 'heic'}

# Optional extensions by feature
VIDEO_EXTENSIONS = {
'mp4', 'mov', 'avi', 'mkv', 'flv', 'mxf', 'gxf', 'ts', 'ps', '3gp', '3gpp',
'mpg', 'wmv', 'asf', 'm4v', 'isma', 'ismv', 'dvr-ms', 'webm', 'mpeg'
}

AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'aac', 'flac', 'm4a'}

def get_allowed_extensions(enable_video=False, enable_audio=False):
"""
Get allowed file extensions based on feature flags.

Args:
enable_video: Whether video file support is enabled
enable_audio: Whether audio file support is enabled

Returns:
set: Allowed file extensions
"""
extensions = BASE_ALLOWED_EXTENSIONS.copy()
extensions.update(DOCUMENT_EXTENSIONS)
extensions.update(IMAGE_EXTENSIONS)
extensions.update(TABULAR_EXTENSIONS)

if enable_video:
extensions.update(VIDEO_EXTENSIONS)

if enable_audio:
extensions.update(AUDIO_EXTENSIONS)

return extensions

ALLOWED_EXTENSIONS = get_allowed_extensions(enable_video=True, enable_audio=True)

# Admin UI specific extensions (for logo/favicon uploads)
ALLOWED_EXTENSIONS_IMG = {'png', 'jpg', 'jpeg'}
MAX_CONTENT_LENGTH = 5000 * 1024 * 1024 # 5000 MB AKA 5 GB

Expand Down
159 changes: 138 additions & 21 deletions application/single_app/functions_documents.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4851,7 +4851,7 @@ def process_di_document(document_id, user_id, temp_file_path, original_filename,
is_pdf = file_ext == '.pdf'
is_word = file_ext in ('.docx', '.doc')
is_ppt = file_ext in ('.pptx', '.ppt')
is_image = file_ext in ('.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.heif')
is_image = file_ext in tuple('.' + ext for ext in IMAGE_EXTENSIONS)

try:
if is_pdf:
Expand DownExpand Up@@ -5233,23 +5233,138 @@ def process_audio_document(
print(f"[Debug] Transcribing chunk {idx}: {chunk_path}")

# Get fresh config (tokens expire after ~1 hour)
speech_config = _get_speech_config(settings, endpoint, locale)
try:
speech_config = _get_speech_config(settings, endpoint, locale)
except Exception as e:
print(f"[Error] Failed to get speech config for chunk {idx}: {e}")
raise RuntimeError(f"Speech configuration failed for chunk {idx}: {e}")

audio_config = speechsdk.AudioConfig(filename=chunk_path)
speech_recognizer = speechsdk.SpeechRecognizer(
speech_config=speech_config,
audio_config=audio_config
)
try:
audio_config = speechsdk.AudioConfig(filename=chunk_path)
except Exception as e:
print(f"[Error] Failed to load audio file {chunk_path}: {e}")
raise RuntimeError(f"Audio file loading failed: {e}")

result = speech_recognizer.recognize_once()
if result.reason == speechsdk.ResultReason.RecognizedSpeech:
print(f"[Debug] Recognized: {result.text}")
all_phrases.append(result.text)
elif result.reason == speechsdk.ResultReason.NoMatch:
print(f"[Warning] No speech in {chunk_path}")
elif result.reason == speechsdk.ResultReason.Canceled:
print(f"[Error] {result.cancellation_details.reason}: {result.cancellation_details.error_details}")
raise RuntimeError(f"Transcription canceled for {chunk_path}: {result.cancellation_details.error_details}")
try:
speech_recognizer = speechsdk.SpeechRecognizer(
speech_config=speech_config,
audio_config=audio_config
)
except Exception as e:
print(f"[Error] Failed to create speech recognizer for chunk {idx}: {e}")
raise RuntimeError(f"Speech recognizer creation failed: {e}")

# Use continuous recognition instead of recognize_once
all_results = []
done = False
error_occurred = False
error_message = None

def stop_cb(evt):
nonlocal done
print(f"[Debug] Session stopped for chunk {idx}")
done = True

def recognized_cb(evt):
try:
if evt.result.reason == speechsdk.ResultReason.RecognizedSpeech:
all_results.append(evt.result.text)
print(f"[Debug] Recognized: {evt.result.text}")
elif evt.result.reason == speechsdk.ResultReason.NoMatch:
print(f"[Debug] No speech recognized in segment")
except Exception as e:
print(f"[Error] Error in recognized callback: {e}")
# Don't fail on individual recognition errors

def canceled_cb(evt):
nonlocal done, error_occurred, error_message
print(f"[Debug] Recognition canceled for chunk {idx}: {evt.cancellation_details.reason}")

if evt.cancellation_details.reason == speechsdk.CancellationReason.Error:
error_occurred = True
error_message = evt.cancellation_details.error_details
print(f"[Error] Recognition error: {error_message}")
elif evt.cancellation_details.reason == speechsdk.CancellationReason.EndOfStream:
print(f"[Debug] End of audio stream reached")

done = True

try:
# Connect callbacks
speech_recognizer.recognized.connect(recognized_cb)
speech_recognizer.session_stopped.connect(stop_cb)
speech_recognizer.canceled.connect(canceled_cb)

# Start continuous recognition
print(f"[Debug] Starting continuous recognition for chunk {idx}")
speech_recognizer.start_continuous_recognition()

# Wait for completion with timeout
import time
timeout_seconds = 600 # 10 minutes max per chunk
start_time = time.time()

while not done:
if time.time() - start_time > timeout_seconds:
print(f"[Error] Recognition timeout for chunk {idx}")
error_occurred = True
error_message = f"Recognition timed out after {timeout_seconds} seconds"
break
time.sleep(0.5)

# Stop recognition
try:
speech_recognizer.stop_continuous_recognition()
print(f"[Debug] Stopped continuous recognition for chunk {idx}")
except Exception as e:
print(f"[Warning] Error stopping recognition for chunk {idx}: {e}")
# Continue even if stop fails

# Check for errors after completion
if error_occurred:
raise RuntimeError(f"Recognition failed for chunk {idx}: {error_message}")

# Add all recognized phrases to the overall list
if all_results:
all_phrases.extend(all_results)
print(f"[Debug] Total phrases from chunk {idx}: {len(all_results)}")
else:
print(f"[Warning] No speech recognized in {chunk_path}")
# Continue to next chunk - empty result is not necessarily an error

except RuntimeError as e:
# Re-raise runtime errors (these are our custom errors)
raise
except Exception as e:
print(f"[Error] Unexpected error during recognition for chunk {idx}: {e}")
raise RuntimeError(f"Recognition failed unexpectedly for chunk {idx}: {e}")
finally:
# Cleanup: disconnect callbacks and dispose recognizer
try:
speech_recognizer.recognized.disconnect_all()
speech_recognizer.session_stopped.disconnect_all()
speech_recognizer.canceled.disconnect_all()
except Exception as e:
print(f"[Warning] Error disconnecting callbacks for chunk {idx}: {e}")

# # Get fresh config (tokens expire after ~1 hour)
# speech_config = _get_speech_config(settings, endpoint, locale)

# audio_config = speechsdk.AudioConfig(filename=chunk_path)
# speech_recognizer = speechsdk.SpeechRecognizer(
# speech_config=speech_config,
# audio_config=audio_config
# )

# result = speech_recognizer.recognize_once()
# if result.reason == speechsdk.ResultReason.RecognizedSpeech:
# print(f"[Debug] Recognized: {result.text}")
# all_phrases.append(result.text)
# elif result.reason == speechsdk.ResultReason.NoMatch:
# print(f"[Warning] No speech in {chunk_path}")
# elif result.reason == speechsdk.ResultReason.Canceled:
# print(f"[Error] {result.cancellation_details.reason}: {result.cancellation_details.error_details}")
# raise RuntimeError(f"Transcription canceled for {chunk_path}: {result.cancellation_details.error_details}")

else:
# Use the fast-transcription API if not in sovereign or custom cloud
Expand DownExpand Up@@ -5357,8 +5472,12 @@ def process_document_upload_background(document_id, user_id, temp_file_path, ori
enable_extract_meta_data = settings.get('enable_extract_meta_data', False) # Used by DI flow
max_file_size_bytes = settings.get('max_file_size_mb', 16) * 1024 * 1024

video_extensions = ('.mp4', '.mov', '.avi', '.mkv', '.flv')
audio_extensions = ('.mp3', '.wav', '.ogg', '.aac', '.flac', '.m4a')
# Get allowed extensions from config.py to determine which processing function to call
tabular_extensions = tuple('.' + ext for ext in TABULAR_EXTENSIONS)
image_extensions = tuple('.' + ext for ext in IMAGE_EXTENSIONS)
di_supported_extensions = tuple('.' + ext for ext in DOCUMENT_EXTENSIONS | IMAGE_EXTENSIONS)
video_extensions = tuple('.' + ext for ext in VIDEO_EXTENSIONS)
audio_extensions = tuple('.' + ext for ext in AUDIO_EXTENSIONS)

# --- Define update_document callback wrapper ---
# This makes it easier to pass the update function to helpers without repeating args
Expand DownExpand Up@@ -5402,8 +5521,6 @@ def update_doc_callback(**kwargs):

# --- 1. Dispatch to appropriate handler based on file type ---
# Note: .doc and .docm are handled separately by process_doc() using docx2txt
di_supported_extensions = ('.pdf', '.docx', '.pptx', '.ppt', '.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.heif')
tabular_extensions = ('.csv', '.xlsx', '.xls', '.xlsm')

is_group = group_id is not None

Expand DownExpand Up@@ -5512,7 +5629,7 @@ def update_doc_callback(**kwargs):
final_status = "Processing complete"
if total_chunks_saved == 0:
# Provide more specific status if no chunks were saved
if file_ext in ('.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif', '.heif'):
if file_ext in image_extensions:
final_status = "Processing complete - no text found in image"
elif file_ext in tabular_extensions:
final_status = "Processing complete - no data rows found or file empty"
Expand Down
20 changes: 17 additions & 3 deletions application/single_app/route_backend_documents.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -535,9 +535,23 @@ def api_patch_user_document(document_id):
try:
# Log the metadata update transaction if any fields were updated
if updated_fields:
# Get document details for logging
doc = get_document(user_id, document_id)
if doc:
# Get document details for logging - handle tuple return
doc_response = get_document(user_id, document_id)
doc = None

# Handle tuple return (response, status_code)
if isinstance(doc_response, tuple):
resp, status_code = doc_response
if hasattr(resp, "get_json"):
doc = resp.get_json()
else:
doc = resp
elif hasattr(doc_response, "get_json"):
doc = doc_response.get_json()
else:
doc = doc_response

if doc and isinstance(doc, dict):
log_document_metadata_update_transaction(
user_id=user_id,
document_id=document_id,
Expand Down
53 changes: 36 additions & 17 deletions application/single_app/route_backend_group_documents.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -416,24 +416,43 @@ def api_patch_group_document(document_id):
)
updated_fields['authors'] = authors_list

# Log the metadata update transaction if any fields were updated
if updated_fields:
# Save updates back to Cosmos
try:
# Log the metadata update transaction if any fields were updated
if updated_fields:
# Get document details for logging - handle tuple return
# Get document details for logging
from functions_documents import get_document
doc = get_document(user_id, document_id, group_id=active_group_id)
if doc:
from functions_activity_logging import log_document_metadata_update_transaction
log_document_metadata_update_transaction(
user_id=user_id,
document_id=document_id,
workspace_type='group',
file_name=doc.get('file_name', 'Unknown'),
updated_fields=updated_fields,
file_type=doc.get('file_type'),
group_id=active_group_id
)

return jsonify({'message': 'Group document metadata updated successfully'}), 200
from functions_documents import get_document
doc_response = get_document(user_id, document_id, group_id=active_group_id)
doc = None

# Handle tuple return (response, status_code)
if isinstance(doc_response, tuple):
resp, status_code = doc_response
if hasattr(resp, "get_json"):
doc = resp.get_json()
else:
doc = resp
elif hasattr(doc_response, "get_json"):
doc = doc_response.get_json()
else:
doc = doc_response

if doc and isinstance(doc, dict):
from functions_activity_logging import log_document_metadata_update_transaction
log_document_metadata_update_transaction(
user_id=user_id,
document_id=document_id,
workspace_type='group',
file_name=doc.get('file_name', 'Unknown'),
updated_fields=updated_fields,
file_type=doc.get('file_type'),
group_id=active_group_id
)

return jsonify({'message': 'Group document metadata updated successfully'}), 200
except Exception as e:
return jsonify({'Error updating Group document metadata': str(e)}), 500
except Exception as e:
return jsonify({'error': str(e)}), 500

Expand Down
Loading