rairo commited on
Commit
c100c20
·
verified ·
1 Parent(s): 7651a82

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +64 -40
main.py CHANGED
@@ -891,7 +891,7 @@ 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']) # Changed to POST since it creates resources
895
  def speak_notes(notes_id):
896
  user, error = verify_token(request.headers.get('Authorization'))
897
  if error:
@@ -901,81 +901,103 @@ def speak_notes(notes_id):
901
  return jsonify({'error': 'Backend service unavailable'}), 503
902
 
903
  try:
 
 
 
 
 
 
 
 
 
 
 
904
  # Check user status and credits
905
- profile_res = supabase.table('profiles').select('credits', 'suspended').eq('id', user.id).single().execute()
 
 
 
 
906
  if profile_res.data['suspended']:
907
  return jsonify({'error': 'Account suspended'}), 403
908
  if profile_res.data['credits'] < 5:
909
  return jsonify({'error': 'Insufficient credits (Need 5)'}), 402
910
 
911
- # Fetch notes content
912
- notes_res = supabase.table('notes').select('content, tts_audio_url').eq('id', notes_id).single().execute()
913
- if not notes_res.data:
914
- return jsonify({'error': 'Notes not found'}), 404
915
-
916
  # Return existing audio if available
917
- if notes_res.data.get('tts_audio_url'):
918
  return jsonify({
919
  'success': True,
920
- 'audio_url': notes_res.data['tts_audio_url'],
921
  'message': 'Using existing audio file'
922
  })
923
 
924
- notes_content = notes_res.data['content']
925
  if not notes_content:
926
  return jsonify({'error': 'Notes content is empty'}), 400
927
 
928
  # --- Generate TTS Audio with chunking ---
929
- start_time = time.time()
930
- logging.info(f"Generating TTS for user {user.id}, notes: {notes_id}")
931
-
932
- # Chunk text if too long (ElevenLabs limit is ~2500 chars for best quality)
933
- CHUNK_SIZE = 2000 # Conservative chunk size
934
  chunks = [notes_content[i:i+CHUNK_SIZE] for i in range(0, len(notes_content), CHUNK_SIZE)]
935
 
936
  audio_bytes = b""
937
  for chunk in chunks:
938
  try:
939
- # Using ElevenLabs Studio API with streaming
940
  chunk_audio = elevenlabs_client.generate(
941
  text=chunk,
942
- voice="Rachel", # Default voice
943
  model="eleven_multilingual_v2",
944
  stream=True
945
  )
946
  audio_bytes += b"".join(chunk_audio)
947
  except Exception as e:
948
  logging.error(f"Error generating chunk: {str(e)}")
949
- raise RuntimeError(f"Failed to generate audio chunk: {str(e)}")
950
 
951
  if not audio_bytes:
952
  raise RuntimeError("Generated empty audio file")
953
 
954
- logging.info(f"TTS generation took {time.time() - start_time:.2f}s")
955
-
956
- # --- Save to Supabase Storage ---
957
  bucket_name = 'notes-audio'
958
- destination_path = f'users/{user.id}/{notes_id}.mp3'
959
 
960
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
961
- tmp_file.write(audio_bytes)
962
- tmp_path = tmp_file.name
963
-
964
  try:
965
- # Upload with proper content type
966
- audio_url = upload_to_supabase_storage(
967
- bucket_name,
968
- tmp_path,
969
- destination_path,
970
- "audio/mpeg"
 
 
 
 
971
  )
972
-
973
- # Update database
974
- supabase.table('notes').update({'tts_audio_url': audio_url}).eq('id', notes_id).execute()
975
 
976
- # Deduct credits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
977
  new_credits = profile_res.data['credits'] - 5
978
- supabase.table('profiles').update({'credits': new_credits}).eq('id', user.id).execute()
 
 
 
 
 
 
979
 
980
  return jsonify({
981
  'success': True,
@@ -983,11 +1005,13 @@ def speak_notes(notes_id):
983
  'remaining_credits': new_credits
984
  })
985
 
986
- finally:
987
- os.remove(tmp_path)
 
 
988
 
989
  except Exception as e:
990
- logging.error(f"Error in speak_notes: {traceback.format_exc()}")
991
  return jsonify({'error': str(e)}), 500
992
 
993
  # New endpoint to view existing audio URL
 
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
  user, error = verify_token(request.headers.get('Authorization'))
897
  if error:
 
901
  return jsonify({'error': 'Backend service unavailable'}), 503
902
 
903
  try:
904
+ # Verify note ownership first
905
+ note_res = supabase.table('notes') \
906
+ .select('user_id, content, tts_audio_url') \
907
+ .eq('id', notes_id) \
908
+ .eq('user_id', user.id) \ # Critical RLS check
909
+ .maybe_single() \
910
+ .execute()
911
+
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().execute()
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,
930
+ 'audio_url': note_res.data['tts_audio_url'],
931
  'message': 'Using existing audio file'
932
  })
933
 
934
+ notes_content = note_res.data['content']
935
  if not notes_content:
936
  return jsonify({'error': 'Notes content is empty'}), 400
937
 
938
  # --- Generate TTS Audio with chunking ---
939
+ CHUNK_SIZE = 2000
 
 
 
 
940
  chunks = [notes_content[i:i+CHUNK_SIZE] for i in range(0, len(notes_content), CHUNK_SIZE)]
941
 
942
  audio_bytes = b""
943
  for chunk in chunks:
944
  try:
 
945
  chunk_audio = elevenlabs_client.generate(
946
  text=chunk,
947
+ voice="Rachel",
948
  model="eleven_multilingual_v2",
949
  stream=True
950
  )
951
  audio_bytes += b"".join(chunk_audio)
952
  except Exception as e:
953
  logging.error(f"Error generating chunk: {str(e)}")
954
+ raise RuntimeError(f"Audio generation failed: {str(e)}")
955
 
956
  if not audio_bytes:
957
  raise RuntimeError("Generated empty audio file")
958
 
959
+ # --- Save to Supabase Storage with RLS compliance ---
 
 
960
  bucket_name = 'notes-audio'
961
+ file_path = f'{user.id}/{notes_id}.mp3'
962
 
 
 
 
 
963
  try:
964
+ # Upload with owner metadata for RLS
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 # Essential for RLS policies
973
+ }
974
  )
 
 
 
975
 
976
+ if upload_res.error:
977
+ raise ConnectionError(upload_res.error.message)
978
+
979
+ # Get public URL
980
+ audio_url = supabase.storage.from_(bucket_name).get_public_url(file_path)
981
+
982
+ # Update notes table with verification
983
+ update_res = supabase.table('notes') \
984
+ .update({'tts_audio_url': audio_url}) \
985
+ .eq('id', notes_id) \
986
+ .eq('user_id', user.id) \ # Double-check ownership
987
+ .execute()
988
+
989
+ if update_res.error:
990
+ raise ConnectionError(update_res.error.message)
991
+
992
+ # Deduct credits with verification
993
  new_credits = profile_res.data['credits'] - 5
994
+ credit_res = supabase.table('profiles') \
995
+ .update({'credits': new_credits}) \
996
+ .eq('id', user.id) \
997
+ .execute()
998
+
999
+ if credit_res.error:
1000
+ raise ConnectionError(credit_res.error.message)
1001
 
1002
  return jsonify({
1003
  'success': True,
 
1005
  'remaining_credits': new_credits
1006
  })
1007
 
1008
+ except Exception as upload_error:
1009
+ # Clean up failed upload
1010
+ supabase.storage.from_(bucket_name).remove([file_path])
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