Update main.py
Browse files
main.py
CHANGED
|
@@ -27,6 +27,11 @@ from youtube_transcript_api import YouTubeTranscriptApi
|
|
| 27 |
import arxiv # For ArXiv
|
| 28 |
from elevenlabs import play, stream, save
|
| 29 |
import math
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# --- Environment Variables ---
|
| 32 |
# Load environment variables if using a .env file (optional, good practice)
|
|
@@ -891,18 +896,47 @@ def submit_quiz_attempt(quiz_id):
|
|
| 891 |
|
| 892 |
|
| 893 |
# Modified speak_notes endpoint with ElevenLabs Studio API and chunking
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
@app.route('/api/tutor/notes/<uuid:notes_id>/speak', methods=['POST'])
|
| 895 |
def speak_notes(notes_id):
|
| 896 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 897 |
user, error = verify_token(request.headers.get('Authorization'))
|
| 898 |
if error:
|
| 899 |
return jsonify({'error': error['error']}), error['status']
|
| 900 |
|
| 901 |
if not supabase or not elevenlabs_client:
|
|
|
|
| 902 |
return jsonify({'error': 'Backend service unavailable'}), 503
|
| 903 |
|
| 904 |
try:
|
| 905 |
# 1. Verify note ownership and get content
|
|
|
|
| 906 |
note_res = supabase.table('notes') \
|
| 907 |
.select('user_id, content, tts_audio_url') \
|
| 908 |
.eq('id', str(notes_id)) \
|
|
@@ -911,6 +945,7 @@ def speak_notes(notes_id):
|
|
| 911 |
.execute()
|
| 912 |
|
| 913 |
if not note_res.data:
|
|
|
|
| 914 |
return jsonify({'error': 'Note not found or unauthorized'}), 404
|
| 915 |
|
| 916 |
# 2. Check user status and credits
|
|
@@ -920,98 +955,206 @@ def speak_notes(notes_id):
|
|
| 920 |
.single() \
|
| 921 |
.execute()
|
| 922 |
|
| 923 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 924 |
return jsonify({'error': 'Account suspended'}), 403
|
| 925 |
-
if profile_res.data['credits'] < 5:
|
| 926 |
-
return jsonify({'error': 'Insufficient credits (Need 5)'}), 402
|
| 927 |
|
| 928 |
-
|
| 929 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
return jsonify({
|
| 931 |
'success': True,
|
| 932 |
-
'audio_url':
|
| 933 |
-
'message': 'Using existing audio file'
|
|
|
|
| 934 |
})
|
| 935 |
|
| 936 |
-
notes_content = note_res.data
|
| 937 |
-
if not notes_content:
|
|
|
|
| 938 |
return jsonify({'error': 'Notes content is empty'}), 400
|
| 939 |
|
| 940 |
-
# 4. Generate TTS Audio with chunking
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
|
|
|
|
|
|
|
|
|
| 946 |
try:
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
|
|
|
| 952 |
)
|
| 953 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
except Exception as e:
|
| 955 |
-
logging.error(f"Error generating chunk: {str(e)}")
|
| 956 |
-
|
|
|
|
| 957 |
|
| 958 |
-
if
|
| 959 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 960 |
|
| 961 |
# 5. Save to Supabase Storage
|
| 962 |
-
bucket_name = 'notes-audio'
|
|
|
|
| 963 |
file_path = f'{user.id}/{str(notes_id)}.mp3'
|
| 964 |
-
|
|
|
|
| 965 |
try:
|
| 966 |
-
|
| 967 |
-
|
|
|
|
| 968 |
path=file_path,
|
| 969 |
-
file=
|
| 970 |
-
file_options={"content-type": "audio/mpeg"}
|
| 971 |
)
|
| 972 |
-
|
| 973 |
-
#
|
| 974 |
-
audio_url = supabase.storage.from_(bucket_name).get_public_url(file_path)
|
| 975 |
-
|
| 976 |
-
# 6. Update database records
|
| 977 |
-
update_res = supabase.table('notes') \
|
| 978 |
-
.update({'tts_audio_url': audio_url}) \
|
| 979 |
-
.eq('id', str(notes_id)) \
|
| 980 |
-
.eq('user_id', user.id) \
|
| 981 |
-
.execute()
|
| 982 |
-
|
| 983 |
-
if update_res.error:
|
| 984 |
-
raise ConnectionError(update_res.error.message)
|
| 985 |
-
|
| 986 |
-
# 7. Deduct credits
|
| 987 |
-
new_credits = profile_res.data['credits'] - 5
|
| 988 |
-
credit_res = supabase.table('profiles') \
|
| 989 |
-
.update({'credits': new_credits}) \
|
| 990 |
-
.eq('id', user.id) \
|
| 991 |
-
.execute()
|
| 992 |
-
|
| 993 |
-
if credit_res.error:
|
| 994 |
-
raise ConnectionError(credit_res.error.message)
|
| 995 |
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
})
|
| 1001 |
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
except Exception as cleanup_error:
|
| 1007 |
-
logging.error(f"Cleanup failed: {cleanup_error}")
|
| 1008 |
-
|
| 1009 |
-
logging.error(f"Upload failed: {str(upload_error)}")
|
| 1010 |
-
raise upload_error
|
| 1011 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1012 |
except Exception as e:
|
| 1013 |
-
|
| 1014 |
-
|
|
|
|
|
|
|
|
|
|
| 1015 |
|
| 1016 |
# New endpoint to view existing audio URL
|
| 1017 |
@app.route('/api/tutor/notes/<uuid:notes_id>/audio', methods=['GET'])
|
|
|
|
| 27 |
import arxiv # For ArXiv
|
| 28 |
from elevenlabs import play, stream, save
|
| 29 |
import math
|
| 30 |
+
import pydub
|
| 31 |
+
import logging
|
| 32 |
+
import traceback
|
| 33 |
+
import uuid
|
| 34 |
+
from io import BytesIO # To handle in-memory bytes
|
| 35 |
|
| 36 |
# --- Environment Variables ---
|
| 37 |
# Load environment variables if using a .env file (optional, good practice)
|
|
|
|
| 896 |
|
| 897 |
|
| 898 |
# Modified speak_notes endpoint with ElevenLabs Studio API and chunking
|
| 899 |
+
|
| 900 |
+
try:
|
| 901 |
+
from pydub import AudioSegment
|
| 902 |
+
PYDUB_AVAILABLE = True
|
| 903 |
+
except ImportError:
|
| 904 |
+
PYDUB_AVAILABLE = False
|
| 905 |
+
logging.warning("pydub library not found or ffmpeg might be missing. Audio chunk concatenation will fail. Please install pydub and ensure ffmpeg is in your system's PATH.")
|
| 906 |
+
# Define a dummy AudioSegment class if pydub is not installed to avoid NameError later
|
| 907 |
+
class AudioSegment:
|
| 908 |
+
@staticmethod
|
| 909 |
+
def from_file(*args, **kwargs):
|
| 910 |
+
raise ImportError("pydub/ffmpeg not installed or accessible")
|
| 911 |
+
def __add__(self, other):
|
| 912 |
+
raise ImportError("pydub/ffmpeg not installed or accessible")
|
| 913 |
+
def export(self, *args, **kwargs):
|
| 914 |
+
raise ImportError("pydub/ffmpeg not installed or accessible")
|
| 915 |
+
|
| 916 |
+
|
| 917 |
@app.route('/api/tutor/notes/<uuid:notes_id>/speak', methods=['POST'])
|
| 918 |
def speak_notes(notes_id):
|
| 919 |
+
"""
|
| 920 |
+
Generate TTS audio for notes using ElevenLabs (non-streaming),
|
| 921 |
+
combine chunks using pydub, and store the final MP3 in Supabase Storage.
|
| 922 |
+
Updates the note record with the audio URL and deducts credits.
|
| 923 |
+
"""
|
| 924 |
+
if not PYDUB_AVAILABLE:
|
| 925 |
+
logging.error("Audio processing library (pydub/ffmpeg) check failed.")
|
| 926 |
+
return jsonify({'error': 'Server configuration error: Audio processing library not available.'}), 500
|
| 927 |
+
|
| 928 |
+
# 0. Authenticate User
|
| 929 |
user, error = verify_token(request.headers.get('Authorization'))
|
| 930 |
if error:
|
| 931 |
return jsonify({'error': error['error']}), error['status']
|
| 932 |
|
| 933 |
if not supabase or not elevenlabs_client:
|
| 934 |
+
logging.error("Backend service (Supabase or ElevenLabs client) not initialized.")
|
| 935 |
return jsonify({'error': 'Backend service unavailable'}), 503
|
| 936 |
|
| 937 |
try:
|
| 938 |
# 1. Verify note ownership and get content
|
| 939 |
+
logging.info(f"Processing speak request for note {notes_id} by user {user.id}")
|
| 940 |
note_res = supabase.table('notes') \
|
| 941 |
.select('user_id, content, tts_audio_url') \
|
| 942 |
.eq('id', str(notes_id)) \
|
|
|
|
| 945 |
.execute()
|
| 946 |
|
| 947 |
if not note_res.data:
|
| 948 |
+
logging.warning(f"Note {notes_id} not found or unauthorized for user {user.id}.")
|
| 949 |
return jsonify({'error': 'Note not found or unauthorized'}), 404
|
| 950 |
|
| 951 |
# 2. Check user status and credits
|
|
|
|
| 955 |
.single() \
|
| 956 |
.execute()
|
| 957 |
|
| 958 |
+
# Check for potential errors from profile fetch itself if needed
|
| 959 |
+
if not profile_res.data:
|
| 960 |
+
logging.error(f"Could not fetch profile for user {user.id}")
|
| 961 |
+
return jsonify({'error': 'Failed to retrieve user profile'}), 500
|
| 962 |
+
|
| 963 |
+
if profile_res.data.get('suspended'):
|
| 964 |
+
logging.warning(f"User {user.id} account is suspended.")
|
| 965 |
return jsonify({'error': 'Account suspended'}), 403
|
|
|
|
|
|
|
| 966 |
|
| 967 |
+
current_credits = profile_res.data.get('credits', 0)
|
| 968 |
+
CREDIT_COST = 5
|
| 969 |
+
if current_credits < CREDIT_COST:
|
| 970 |
+
logging.warning(f"User {user.id} has insufficient credits ({current_credits}/{CREDIT_COST}).")
|
| 971 |
+
return jsonify({'error': f'Insufficient credits (Need {CREDIT_COST})'}), 402
|
| 972 |
+
|
| 973 |
+
# 3. Return existing audio if available (and skip generation/deduction)
|
| 974 |
+
existing_audio_url = note_res.data.get('tts_audio_url')
|
| 975 |
+
if existing_audio_url:
|
| 976 |
+
logging.info(f"Using existing audio URL for note {notes_id}: {existing_audio_url}")
|
| 977 |
return jsonify({
|
| 978 |
'success': True,
|
| 979 |
+
'audio_url': existing_audio_url,
|
| 980 |
+
'message': 'Using existing audio file',
|
| 981 |
+
'remaining_credits': current_credits # Return current credits as none were deducted
|
| 982 |
})
|
| 983 |
|
| 984 |
+
notes_content = note_res.data.get('content')
|
| 985 |
+
if not notes_content or not notes_content.strip():
|
| 986 |
+
logging.warning(f"Note {notes_id} content is empty.")
|
| 987 |
return jsonify({'error': 'Notes content is empty'}), 400
|
| 988 |
|
| 989 |
+
# 4. Generate TTS Audio with chunking (Non-Streaming) and combine with pydub
|
| 990 |
+
# ElevenLabs v2 non-streaming limit is often around 2500 chars, but check docs.
|
| 991 |
+
CHUNK_SIZE = 2500
|
| 992 |
+
text_chunks = [notes_content[i:i+CHUNK_SIZE] for i in range(0, len(notes_content), CHUNK_SIZE)]
|
| 993 |
+
|
| 994 |
+
combined_audio_segment = None
|
| 995 |
+
logging.info(f"Generating audio for note {notes_id} in {len(text_chunks)} chunks.")
|
| 996 |
+
|
| 997 |
+
for i, chunk in enumerate(text_chunks):
|
| 998 |
try:
|
| 999 |
+
logging.debug(f"Generating audio for chunk {i+1}/{len(text_chunks)}...")
|
| 1000 |
+
# Use stream=False (default) for non-streaming generation
|
| 1001 |
+
chunk_audio_bytes = elevenlabs_client.generate(
|
| 1002 |
+
text=chunk.strip(), # Ensure no leading/trailing whitespace in chunk
|
| 1003 |
+
voice="Rachel", # Or your desired voice ID
|
| 1004 |
+
model="eleven_multilingual_v2" # Or your desired model ID
|
| 1005 |
)
|
| 1006 |
+
|
| 1007 |
+
if not chunk_audio_bytes:
|
| 1008 |
+
logging.warning(f"ElevenLabs returned empty audio for chunk {i+1} of note {notes_id}")
|
| 1009 |
+
continue # Skip this chunk, maybe log or handle differently if needed
|
| 1010 |
+
|
| 1011 |
+
# Load chunk audio bytes into pydub AudioSegment using BytesIO
|
| 1012 |
+
segment = AudioSegment.from_file(BytesIO(chunk_audio_bytes), format="mp3")
|
| 1013 |
+
|
| 1014 |
+
# Combine segments
|
| 1015 |
+
if combined_audio_segment is None:
|
| 1016 |
+
combined_audio_segment = segment
|
| 1017 |
+
else:
|
| 1018 |
+
combined_audio_segment += segment # Append segment
|
| 1019 |
+
logging.debug(f"Successfully processed chunk {i+1}/{len(text_chunks)}")
|
| 1020 |
+
|
| 1021 |
+
except ImportError as e:
|
| 1022 |
+
logging.error(f"pydub/ffmpeg error during chunk processing: {e}")
|
| 1023 |
+
raise e # Re-raise to be caught by the outer ImportError handler
|
| 1024 |
except Exception as e:
|
| 1025 |
+
logging.error(f"Error generating/processing audio chunk {i+1} for note {notes_id}: {str(e)}")
|
| 1026 |
+
# Stop the process if a chunk fails
|
| 1027 |
+
raise RuntimeError(f"Audio generation/processing failed for chunk {i+1}: {str(e)}")
|
| 1028 |
|
| 1029 |
+
if combined_audio_segment is None:
|
| 1030 |
+
# This could happen if all chunks failed or the content was only whitespace
|
| 1031 |
+
logging.error(f"Failed to generate any audio content for note {notes_id}.")
|
| 1032 |
+
raise RuntimeError("Failed to generate any audio content.")
|
| 1033 |
+
|
| 1034 |
+
# Export combined audio to final bytes
|
| 1035 |
+
output_bytes_io = BytesIO()
|
| 1036 |
+
combined_audio_segment.export(output_bytes_io, format="mp3")
|
| 1037 |
+
final_audio_bytes = output_bytes_io.getvalue() # Get the raw 'bytes' data
|
| 1038 |
+
|
| 1039 |
+
if not final_audio_bytes:
|
| 1040 |
+
logging.error(f"Generated empty final audio file after combining chunks for note {notes_id}.")
|
| 1041 |
+
raise RuntimeError("Generated empty final audio file after combining chunks.")
|
| 1042 |
+
|
| 1043 |
+
logging.info(f"Audio generation complete for note {notes_id}. Total size: {len(final_audio_bytes)} bytes.")
|
| 1044 |
|
| 1045 |
# 5. Save to Supabase Storage
|
| 1046 |
+
bucket_name = 'notes-audio' # Ensure this bucket exists and has correct policies
|
| 1047 |
+
# Use user ID and note ID for a unique, organized path
|
| 1048 |
file_path = f'{user.id}/{str(notes_id)}.mp3'
|
| 1049 |
+
audio_url = None # Initialize audio_url
|
| 1050 |
+
|
| 1051 |
try:
|
| 1052 |
+
logging.info(f"Uploading audio to Supabase Storage: {bucket_name}/{file_path}")
|
| 1053 |
+
# Upload the final combined audio bytes. Use upsert=true to overwrite if regenerating.
|
| 1054 |
+
supabase.storage.from_(bucket_name).upload(
|
| 1055 |
path=file_path,
|
| 1056 |
+
file=final_audio_bytes, # Pass the raw 'bytes' object
|
| 1057 |
+
file_options={"content-type": "audio/mpeg", "upsert": "true"}
|
| 1058 |
)
|
| 1059 |
+
# Note: supabase-py v1 might raise StorageException on failure.
|
| 1060 |
+
# v2 might return a response object to check. Adapt error checking if needed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1061 |
|
| 1062 |
+
# Get public URL (make sure RLS policies allow public reads or generate signed URL)
|
| 1063 |
+
public_url_data = supabase.storage.from_(bucket_name).get_public_url(file_path)
|
| 1064 |
+
# Assuming the URL is directly in the response data
|
| 1065 |
+
audio_url = public_url_data
|
|
|
|
| 1066 |
|
| 1067 |
+
if not audio_url:
|
| 1068 |
+
# This case indicates an issue with getting the URL after a successful upload
|
| 1069 |
+
logging.error(f"Upload to {file_path} seemed successful, but failed to get public URL.")
|
| 1070 |
+
raise ConnectionError("Failed to retrieve audio URL after upload.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1071 |
|
| 1072 |
+
logging.info(f"Audio uploaded successfully for note {notes_id}. URL: {audio_url}")
|
| 1073 |
+
|
| 1074 |
+
# --- Database Updates and Credit Deduction ---
|
| 1075 |
+
# Wrap these in a try/except block for potential rollback on failure
|
| 1076 |
+
try:
|
| 1077 |
+
# 6. Update notes table with the audio URL
|
| 1078 |
+
logging.debug(f"Updating notes table for note {notes_id} with URL.")
|
| 1079 |
+
update_res = supabase.table('notes') \
|
| 1080 |
+
.update({'tts_audio_url': audio_url}) \
|
| 1081 |
+
.eq('id', str(notes_id)) \
|
| 1082 |
+
.eq('user_id', user.id) \
|
| 1083 |
+
.execute()
|
| 1084 |
+
|
| 1085 |
+
# Basic check if response indicates data was modified (adapt based on client version)
|
| 1086 |
+
if not update_res.data:
|
| 1087 |
+
logging.warning(f"Note update query executed for {notes_id} but no data returned (might be ok, or indicate issue).")
|
| 1088 |
+
# Consider stronger checks based on specific client behavior on error/no-update
|
| 1089 |
+
|
| 1090 |
+
# 7. Deduct credits
|
| 1091 |
+
new_credits = current_credits - CREDIT_COST
|
| 1092 |
+
logging.debug(f"Deducting {CREDIT_COST} credits for user {user.id}. New balance: {new_credits}")
|
| 1093 |
+
credit_res = supabase.table('profiles') \
|
| 1094 |
+
.update({'credits': new_credits}) \
|
| 1095 |
+
.eq('id', user.id) \
|
| 1096 |
+
.execute()
|
| 1097 |
+
|
| 1098 |
+
# Basic check for credit update
|
| 1099 |
+
if not credit_res.data:
|
| 1100 |
+
# CRITICAL: Failed to deduct credits after upload/URL update.
|
| 1101 |
+
logging.error(f"CRITICAL: Failed to deduct credits for user {user.id} after audio generation for note {notes_id}.")
|
| 1102 |
+
# Decide handling: Log and proceed? Attempt rollback?
|
| 1103 |
+
# For now, log error and return success as audio is generated, but flag the inconsistency.
|
| 1104 |
+
# Ideally, implement transactional logic or robust cleanup.
|
| 1105 |
+
|
| 1106 |
+
logging.info(f"Successfully updated database and deducted credits for note {notes_id}")
|
| 1107 |
+
|
| 1108 |
+
return jsonify({
|
| 1109 |
+
'success': True,
|
| 1110 |
+
'audio_url': audio_url,
|
| 1111 |
+
'remaining_credits': new_credits
|
| 1112 |
+
})
|
| 1113 |
+
|
| 1114 |
+
except Exception as db_error:
|
| 1115 |
+
# Error occurred during DB update/credit deduction AFTER successful upload
|
| 1116 |
+
logging.error(f"Database update/credit deduction failed for note {notes_id} AFTER upload: {str(db_error)}. URL was {audio_url}")
|
| 1117 |
+
logging.info(f"Attempting to clean up uploaded file: {file_path}")
|
| 1118 |
+
# Attempt to clean up the uploaded file since DB update failed
|
| 1119 |
+
try:
|
| 1120 |
+
supabase.storage.from_(bucket_name).remove([file_path])
|
| 1121 |
+
logging.info(f"Successfully cleaned up orphaned file: {file_path}")
|
| 1122 |
+
except Exception as cleanup_error:
|
| 1123 |
+
logging.error(f"Failed to clean up orphaned file {file_path} after DB error: {cleanup_error}")
|
| 1124 |
+
# Re-raise the database error to signal the overall operation failed
|
| 1125 |
+
raise db_error
|
| 1126 |
+
|
| 1127 |
+
|
| 1128 |
+
except Exception as upload_db_error:
|
| 1129 |
+
# This catches errors during upload OR the subsequent DB operations block if re-raised
|
| 1130 |
+
logging.error(f"Error during upload or DB update phase for note {notes_id}: {str(upload_db_error)}")
|
| 1131 |
+
# Attempt cleanup if file might have been uploaded and URL obtained before the error
|
| 1132 |
+
if audio_url: # Check if upload likely succeeded before the error
|
| 1133 |
+
try:
|
| 1134 |
+
logging.info(f"Attempting cleanup for failed operation: {file_path}")
|
| 1135 |
+
supabase.storage.from_(bucket_name).remove([file_path])
|
| 1136 |
+
logging.info(f"Cleanup successful for {file_path}")
|
| 1137 |
+
except Exception as cleanup_error:
|
| 1138 |
+
# Log if cleanup also fails, but report the original error
|
| 1139 |
+
logging.error(f"Upload/DB error occurred, AND cleanup failed for {file_path}: {cleanup_error}")
|
| 1140 |
+
|
| 1141 |
+
# Re-raise the original error that caused the failure
|
| 1142 |
+
raise upload_db_error
|
| 1143 |
+
|
| 1144 |
+
except ImportError as e:
|
| 1145 |
+
# Catch the specific ImportError from the pydub check/usage
|
| 1146 |
+
logging.error(f"Missing dependency error: {e}")
|
| 1147 |
+
return jsonify({'error': 'Server configuration error: Audio library (pydub/ffmpeg) missing or failed.'}), 500
|
| 1148 |
+
except (RuntimeError, ConnectionError) as e:
|
| 1149 |
+
# Catch specific errors we raised for generation/upload/db issues
|
| 1150 |
+
logging.error(f"Operation failed for note {notes_id}: {str(e)}")
|
| 1151 |
+
return jsonify({'error': str(e)}), 500 # Return the specific error message
|
| 1152 |
except Exception as e:
|
| 1153 |
+
# Catch any other unexpected errors
|
| 1154 |
+
logging.error(f"Unexpected speak endpoint error for note {notes_id}: {traceback.format_exc()}")
|
| 1155 |
+
# Return a generic error message to the client for unknown errors
|
| 1156 |
+
return jsonify({'error': 'An unexpected error occurred during audio generation.'}), 500
|
| 1157 |
+
|
| 1158 |
|
| 1159 |
# New endpoint to view existing audio URL
|
| 1160 |
@app.route('/api/tutor/notes/<uuid:notes_id>/audio', methods=['GET'])
|