Update main.py
Browse files
main.py
CHANGED
|
@@ -25,6 +25,8 @@ from PyPDF2 import PdfReader
|
|
| 25 |
import wikipedia
|
| 26 |
from youtube_transcript_api import YouTubeTranscriptApi
|
| 27 |
import arxiv # For ArXiv
|
|
|
|
|
|
|
| 28 |
|
| 29 |
# --- Environment Variables ---
|
| 30 |
# Load environment variables if using a .env file (optional, good practice)
|
|
@@ -883,7 +885,9 @@ def submit_quiz_attempt(quiz_id):
|
|
| 883 |
return jsonify({'error': f'An unexpected error occurred: {e}'}), 500
|
| 884 |
|
| 885 |
|
| 886 |
-
|
|
|
|
|
|
|
| 887 |
def speak_notes(notes_id):
|
| 888 |
user, error = verify_token(request.headers.get('Authorization'))
|
| 889 |
if error:
|
|
@@ -893,63 +897,122 @@ def speak_notes(notes_id):
|
|
| 893 |
return jsonify({'error': 'Backend service unavailable'}), 503
|
| 894 |
|
| 895 |
try:
|
|
|
|
| 896 |
profile_res = supabase.table('profiles').select('credits', 'suspended').eq('id', user.id).single().execute()
|
| 897 |
-
|
| 898 |
if profile_res.data['suspended']:
|
| 899 |
return jsonify({'error': 'Account suspended'}), 403
|
| 900 |
if profile_res.data['credits'] < 5:
|
| 901 |
return jsonify({'error': 'Insufficient credits (Need 5)'}), 402
|
| 902 |
|
| 903 |
# Fetch notes content
|
| 904 |
-
notes_res = supabase.table('notes').select('content').eq('id', notes_id).single().execute()
|
| 905 |
-
if not notes_res.data
|
| 906 |
-
return jsonify({'error': 'Notes not found
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
|
| 908 |
notes_content = notes_res.data['content']
|
| 909 |
if not notes_content:
|
| 910 |
-
return jsonify({'error': 'Notes content is empty
|
| 911 |
|
| 912 |
-
# --- Generate TTS Audio ---
|
| 913 |
start_time = time.time()
|
| 914 |
logging.info(f"Generating TTS for user {user.id}, notes: {notes_id}")
|
| 915 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
logging.info(f"TTS generation took {time.time() - start_time:.2f}s")
|
| 917 |
|
| 918 |
-
# --- Save
|
| 919 |
-
bucket_name = 'notes-audio'
|
| 920 |
destination_path = f'users/{user.id}/{notes_id}.mp3'
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
tmp_audio_file.write(audio_bytes)
|
| 926 |
-
tmp_file_path = tmp_audio_file.name
|
| 927 |
|
| 928 |
try:
|
| 929 |
-
|
| 930 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
|
| 932 |
-
#
|
| 933 |
supabase.table('notes').update({'tts_audio_url': audio_url}).eq('id', notes_id).execute()
|
| 934 |
-
|
| 935 |
-
# Deduct
|
| 936 |
new_credits = profile_res.data['credits'] - 5
|
| 937 |
supabase.table('profiles').update({'credits': new_credits}).eq('id', user.id).execute()
|
| 938 |
|
| 939 |
-
return jsonify({
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
|
| 941 |
finally:
|
| 942 |
-
os.remove(
|
| 943 |
|
| 944 |
-
except
|
| 945 |
-
logging.error(f"
|
| 946 |
-
return jsonify({'error': f'A backend service is unavailable: {e}'}), 503
|
| 947 |
-
except RuntimeError as e: # AI generation errors
|
| 948 |
-
logging.error(f"RuntimeError during TTS generation for user {user.id}: {e}")
|
| 949 |
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 950 |
except Exception as e:
|
| 951 |
-
logging.error(f"
|
| 952 |
-
return jsonify({'error':
|
| 953 |
|
| 954 |
# ---------- View Notes and Quizzes Endpoints ----------
|
| 955 |
|
|
|
|
| 25 |
import wikipedia
|
| 26 |
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)
|
|
|
|
| 885 |
return jsonify({'error': f'An unexpected error occurred: {e}'}), 500
|
| 886 |
|
| 887 |
|
| 888 |
+
|
| 889 |
+
# Modified speak_notes endpoint with ElevenLabs Studio API and chunking
|
| 890 |
+
@app.route('/api/tutor/notes/<uuid:notes_id>/speak', methods=['POST']) # Changed to POST since it creates resources
|
| 891 |
def speak_notes(notes_id):
|
| 892 |
user, error = verify_token(request.headers.get('Authorization'))
|
| 893 |
if error:
|
|
|
|
| 897 |
return jsonify({'error': 'Backend service unavailable'}), 503
|
| 898 |
|
| 899 |
try:
|
| 900 |
+
# Check user status and credits
|
| 901 |
profile_res = supabase.table('profiles').select('credits', 'suspended').eq('id', user.id).single().execute()
|
|
|
|
| 902 |
if profile_res.data['suspended']:
|
| 903 |
return jsonify({'error': 'Account suspended'}), 403
|
| 904 |
if profile_res.data['credits'] < 5:
|
| 905 |
return jsonify({'error': 'Insufficient credits (Need 5)'}), 402
|
| 906 |
|
| 907 |
# Fetch notes content
|
| 908 |
+
notes_res = supabase.table('notes').select('content, tts_audio_url').eq('id', notes_id).single().execute()
|
| 909 |
+
if not notes_res.data:
|
| 910 |
+
return jsonify({'error': 'Notes not found'}), 404
|
| 911 |
+
|
| 912 |
+
# Return existing audio if available
|
| 913 |
+
if notes_res.data.get('tts_audio_url'):
|
| 914 |
+
return jsonify({
|
| 915 |
+
'success': True,
|
| 916 |
+
'audio_url': notes_res.data['tts_audio_url'],
|
| 917 |
+
'message': 'Using existing audio file'
|
| 918 |
+
})
|
| 919 |
|
| 920 |
notes_content = notes_res.data['content']
|
| 921 |
if not notes_content:
|
| 922 |
+
return jsonify({'error': 'Notes content is empty'}), 400
|
| 923 |
|
| 924 |
+
# --- Generate TTS Audio with chunking ---
|
| 925 |
start_time = time.time()
|
| 926 |
logging.info(f"Generating TTS for user {user.id}, notes: {notes_id}")
|
| 927 |
+
|
| 928 |
+
# Chunk text if too long (ElevenLabs limit is ~2500 chars for best quality)
|
| 929 |
+
CHUNK_SIZE = 2000 # Conservative chunk size
|
| 930 |
+
chunks = [notes_content[i:i+CHUNK_SIZE] for i in range(0, len(notes_content), CHUNK_SIZE)]
|
| 931 |
+
|
| 932 |
+
audio_bytes = b""
|
| 933 |
+
for chunk in chunks:
|
| 934 |
+
try:
|
| 935 |
+
# Using ElevenLabs Studio API with streaming
|
| 936 |
+
chunk_audio = elevenlabs_client.generate(
|
| 937 |
+
text=chunk,
|
| 938 |
+
voice="Rachel", # Default voice
|
| 939 |
+
model="eleven_multilingual_v2",
|
| 940 |
+
stream=True
|
| 941 |
+
)
|
| 942 |
+
audio_bytes += b"".join(chunk_audio)
|
| 943 |
+
except Exception as e:
|
| 944 |
+
logging.error(f"Error generating chunk: {str(e)}")
|
| 945 |
+
raise RuntimeError(f"Failed to generate audio chunk: {str(e)}")
|
| 946 |
+
|
| 947 |
+
if not audio_bytes:
|
| 948 |
+
raise RuntimeError("Generated empty audio file")
|
| 949 |
+
|
| 950 |
logging.info(f"TTS generation took {time.time() - start_time:.2f}s")
|
| 951 |
|
| 952 |
+
# --- Save to Supabase Storage ---
|
| 953 |
+
bucket_name = 'notes-audio'
|
| 954 |
destination_path = f'users/{user.id}/{notes_id}.mp3'
|
| 955 |
+
|
| 956 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
|
| 957 |
+
tmp_file.write(audio_bytes)
|
| 958 |
+
tmp_path = tmp_file.name
|
|
|
|
|
|
|
| 959 |
|
| 960 |
try:
|
| 961 |
+
# Upload with proper content type
|
| 962 |
+
audio_url = upload_to_supabase_storage(
|
| 963 |
+
bucket_name,
|
| 964 |
+
tmp_path,
|
| 965 |
+
destination_path,
|
| 966 |
+
"audio/mpeg"
|
| 967 |
+
)
|
| 968 |
|
| 969 |
+
# Update database
|
| 970 |
supabase.table('notes').update({'tts_audio_url': audio_url}).eq('id', notes_id).execute()
|
| 971 |
+
|
| 972 |
+
# Deduct credits
|
| 973 |
new_credits = profile_res.data['credits'] - 5
|
| 974 |
supabase.table('profiles').update({'credits': new_credits}).eq('id', user.id).execute()
|
| 975 |
|
| 976 |
+
return jsonify({
|
| 977 |
+
'success': True,
|
| 978 |
+
'audio_url': audio_url,
|
| 979 |
+
'remaining_credits': new_credits
|
| 980 |
+
})
|
| 981 |
|
| 982 |
finally:
|
| 983 |
+
os.remove(tmp_path)
|
| 984 |
|
| 985 |
+
except Exception as e:
|
| 986 |
+
logging.error(f"Error in speak_notes: {traceback.format_exc()}")
|
|
|
|
|
|
|
|
|
|
| 987 |
return jsonify({'error': str(e)}), 500
|
| 988 |
+
|
| 989 |
+
# New endpoint to view existing audio URL
|
| 990 |
+
@app.route('/api/tutor/notes/<uuid:notes_id>/audio', methods=['GET'])
|
| 991 |
+
def get_note_audio(notes_id):
|
| 992 |
+
user, error = verify_token(request.headers.get('Authorization'))
|
| 993 |
+
if error:
|
| 994 |
+
return jsonify({'error': error['error']}), error['status']
|
| 995 |
+
|
| 996 |
+
try:
|
| 997 |
+
notes_res = supabase.table('notes').select('tts_audio_url, user_id').eq('id', notes_id).single().execute()
|
| 998 |
+
|
| 999 |
+
if not notes_res.data:
|
| 1000 |
+
return jsonify({'error': 'Notes not found'}), 404
|
| 1001 |
+
|
| 1002 |
+
if notes_res.data['user_id'] != user.id:
|
| 1003 |
+
return jsonify({'error': 'Unauthorized access'}), 403
|
| 1004 |
+
|
| 1005 |
+
if not notes_res.data['tts_audio_url']:
|
| 1006 |
+
return jsonify({'error': 'No audio available for these notes'}), 404
|
| 1007 |
+
|
| 1008 |
+
return jsonify({
|
| 1009 |
+
'success': True,
|
| 1010 |
+
'audio_url': notes_res.data['tts_audio_url']
|
| 1011 |
+
})
|
| 1012 |
+
|
| 1013 |
except Exception as e:
|
| 1014 |
+
logging.error(f"Error getting audio URL: {str(e)}")
|
| 1015 |
+
return jsonify({'error': str(e)}), 500
|
| 1016 |
|
| 1017 |
# ---------- View Notes and Quizzes Endpoints ----------
|
| 1018 |
|