Update main.py
Browse files
main.py
CHANGED
|
@@ -893,6 +893,7 @@ def submit_quiz_attempt(quiz_id):
|
|
| 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 |
user, error = verify_token(request.headers.get('Authorization'))
|
| 897 |
if error:
|
| 898 |
return jsonify({'error': error['error']}), error['status']
|
|
@@ -901,7 +902,7 @@ def speak_notes(notes_id):
|
|
| 901 |
return jsonify({'error': 'Backend service unavailable'}), 503
|
| 902 |
|
| 903 |
try:
|
| 904 |
-
# Verify note ownership
|
| 905 |
note_res = supabase.table('notes') \
|
| 906 |
.select('user_id, content, tts_audio_url') \
|
| 907 |
.eq('id', notes_id) \
|
|
@@ -912,18 +913,19 @@ def speak_notes(notes_id):
|
|
| 912 |
if not note_res.data:
|
| 913 |
return jsonify({'error': 'Note not found or unauthorized'}), 404
|
| 914 |
|
| 915 |
-
# Check user status and credits
|
| 916 |
profile_res = supabase.table('profiles') \
|
| 917 |
.select('credits, suspended') \
|
| 918 |
.eq('id', user.id) \
|
| 919 |
-
.single()
|
| 920 |
-
|
|
|
|
| 921 |
if profile_res.data['suspended']:
|
| 922 |
return jsonify({'error': 'Account suspended'}), 403
|
| 923 |
if profile_res.data['credits'] < 5:
|
| 924 |
return jsonify({'error': 'Insufficient credits (Need 5)'}), 402
|
| 925 |
|
| 926 |
-
# Return existing audio if available
|
| 927 |
if note_res.data.get('tts_audio_url'):
|
| 928 |
return jsonify({
|
| 929 |
'success': True,
|
|
@@ -935,7 +937,7 @@ def speak_notes(notes_id):
|
|
| 935 |
if not notes_content:
|
| 936 |
return jsonify({'error': 'Notes content is empty'}), 400
|
| 937 |
|
| 938 |
-
#
|
| 939 |
CHUNK_SIZE = 2000
|
| 940 |
chunks = [notes_content[i:i+CHUNK_SIZE] for i in range(0, len(notes_content), CHUNK_SIZE)]
|
| 941 |
|
|
@@ -956,30 +958,30 @@ def speak_notes(notes_id):
|
|
| 956 |
if not audio_bytes:
|
| 957 |
raise RuntimeError("Generated empty audio file")
|
| 958 |
|
| 959 |
-
#
|
| 960 |
bucket_name = 'notes-audio'
|
| 961 |
file_path = f'{user.id}/{notes_id}.mp3'
|
| 962 |
|
| 963 |
try:
|
| 964 |
-
# Upload
|
| 965 |
upload_res = supabase.storage.from_(bucket_name).upload(
|
| 966 |
path=file_path,
|
| 967 |
file=audio_bytes,
|
| 968 |
file_options={
|
| 969 |
'content-type': 'audio/mpeg',
|
| 970 |
'cache-control': '3600',
|
| 971 |
-
'upsert': 'true'
|
| 972 |
-
'owner': user.id
|
| 973 |
}
|
| 974 |
)
|
| 975 |
|
| 976 |
-
|
| 977 |
-
|
|
|
|
| 978 |
|
| 979 |
# Get public URL
|
| 980 |
audio_url = supabase.storage.from_(bucket_name).get_public_url(file_path)
|
| 981 |
|
| 982 |
-
# Update
|
| 983 |
update_res = supabase.table('notes') \
|
| 984 |
.update({'tts_audio_url': audio_url}) \
|
| 985 |
.eq('id', notes_id) \
|
|
@@ -989,7 +991,7 @@ def speak_notes(notes_id):
|
|
| 989 |
if update_res.error:
|
| 990 |
raise ConnectionError(update_res.error.message)
|
| 991 |
|
| 992 |
-
# Deduct credits
|
| 993 |
new_credits = profile_res.data['credits'] - 5
|
| 994 |
credit_res = supabase.table('profiles') \
|
| 995 |
.update({'credits': new_credits}) \
|
|
@@ -1007,11 +1009,16 @@ def speak_notes(notes_id):
|
|
| 1007 |
|
| 1008 |
except Exception as upload_error:
|
| 1009 |
# Clean up failed upload
|
| 1010 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1011 |
raise upload_error
|
| 1012 |
|
| 1013 |
except Exception as e:
|
| 1014 |
-
logging.error(f"Speak error: {traceback.format_exc()}")
|
| 1015 |
return jsonify({'error': str(e)}), 500
|
| 1016 |
|
| 1017 |
# New endpoint to view existing audio URL
|
|
|
|
| 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 |
+
"""Generate TTS audio for notes and store in Supabase Storage"""
|
| 897 |
user, error = verify_token(request.headers.get('Authorization'))
|
| 898 |
if error:
|
| 899 |
return jsonify({'error': error['error']}), error['status']
|
|
|
|
| 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', notes_id) \
|
|
|
|
| 913 |
if not note_res.data:
|
| 914 |
return jsonify({'error': 'Note not found or unauthorized'}), 404
|
| 915 |
|
| 916 |
+
# 2. Check user status and credits
|
| 917 |
profile_res = supabase.table('profiles') \
|
| 918 |
.select('credits, suspended') \
|
| 919 |
.eq('id', user.id) \
|
| 920 |
+
.single() \
|
| 921 |
+
.execute()
|
| 922 |
+
|
| 923 |
if profile_res.data['suspended']:
|
| 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 |
+
# 3. Return existing audio if available
|
| 929 |
if note_res.data.get('tts_audio_url'):
|
| 930 |
return jsonify({
|
| 931 |
'success': True,
|
|
|
|
| 937 |
if not notes_content:
|
| 938 |
return jsonify({'error': 'Notes content is empty'}), 400
|
| 939 |
|
| 940 |
+
# 4. Generate TTS Audio with chunking
|
| 941 |
CHUNK_SIZE = 2000
|
| 942 |
chunks = [notes_content[i:i+CHUNK_SIZE] for i in range(0, len(notes_content), CHUNK_SIZE)]
|
| 943 |
|
|
|
|
| 958 |
if not audio_bytes:
|
| 959 |
raise RuntimeError("Generated empty audio file")
|
| 960 |
|
| 961 |
+
# 5. Save to Supabase Storage
|
| 962 |
bucket_name = 'notes-audio'
|
| 963 |
file_path = f'{user.id}/{notes_id}.mp3'
|
| 964 |
|
| 965 |
try:
|
| 966 |
+
# Upload audio file
|
| 967 |
upload_res = supabase.storage.from_(bucket_name).upload(
|
| 968 |
path=file_path,
|
| 969 |
file=audio_bytes,
|
| 970 |
file_options={
|
| 971 |
'content-type': 'audio/mpeg',
|
| 972 |
'cache-control': '3600',
|
| 973 |
+
'upsert': 'true'
|
|
|
|
| 974 |
}
|
| 975 |
)
|
| 976 |
|
| 977 |
+
# Verify upload success (storage upload returns None on success)
|
| 978 |
+
if upload_res is not None:
|
| 979 |
+
raise ConnectionError("Audio upload failed")
|
| 980 |
|
| 981 |
# Get public URL
|
| 982 |
audio_url = supabase.storage.from_(bucket_name).get_public_url(file_path)
|
| 983 |
|
| 984 |
+
# 6. Update database records
|
| 985 |
update_res = supabase.table('notes') \
|
| 986 |
.update({'tts_audio_url': audio_url}) \
|
| 987 |
.eq('id', notes_id) \
|
|
|
|
| 991 |
if update_res.error:
|
| 992 |
raise ConnectionError(update_res.error.message)
|
| 993 |
|
| 994 |
+
# 7. Deduct credits
|
| 995 |
new_credits = profile_res.data['credits'] - 5
|
| 996 |
credit_res = supabase.table('profiles') \
|
| 997 |
.update({'credits': new_credits}) \
|
|
|
|
| 1009 |
|
| 1010 |
except Exception as upload_error:
|
| 1011 |
# Clean up failed upload
|
| 1012 |
+
try:
|
| 1013 |
+
supabase.storage.from_(bucket_name).remove([file_path])
|
| 1014 |
+
except Exception as cleanup_error:
|
| 1015 |
+
logging.error(f"Cleanup failed: {cleanup_error}")
|
| 1016 |
+
|
| 1017 |
+
logging.error(f"Upload failed: {str(upload_error)}")
|
| 1018 |
raise upload_error
|
| 1019 |
|
| 1020 |
except Exception as e:
|
| 1021 |
+
logging.error(f"Speak endpoint error: {traceback.format_exc()}")
|
| 1022 |
return jsonify({'error': str(e)}), 500
|
| 1023 |
|
| 1024 |
# New endpoint to view existing audio URL
|