rairo commited on
Commit
afe1f69
·
verified ·
1 Parent(s): 7ecf4f5

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +126 -48
main.py CHANGED
@@ -62,7 +62,7 @@ try:
62
  raise ValueError("Gemini API Key must be set in environment variables.")
63
  genai.configure(api_key=GEMINI_API_KEY)
64
  # Use a generally available model, adjust if you have access to specific previews
65
- gemini_model = genai.GenerativeModel('gemini-1.5-flash-latest')
66
  print("Gemini API initialized successfully.")
67
  except Exception as e:
68
  print(f"Error initializing Gemini API: {e}")
@@ -389,6 +389,7 @@ def generate_tts_audio(text_to_speak, voice_id="Rachel"): # Example voice, choos
389
 
390
  # === Authentication Endpoints ===
391
 
 
392
  @app.route('/api/auth/signup', methods=['POST'])
393
  def signup():
394
  if not supabase: return jsonify({'error': 'Service unavailable'}), 503
@@ -399,27 +400,27 @@ def signup():
399
  if not email or not password:
400
  return jsonify({'error': 'Email and password are required'}), 400
401
 
402
- # Create user in Supabase Auth
403
  res = supabase.auth.sign_up({"email": email, "password": password})
404
 
405
- # Supabase automatically triggers the function/trigger to create the profile row
406
- # If it didn't, you'd insert into 'profiles' here using res.user.id
 
 
 
 
407
 
408
- # Return minimal info on signup, client should usually sign in after verification
409
  return jsonify({
410
  'success': True,
411
  'message': 'Signup successful. Please check your email for verification.',
412
- # Avoid sending back full user object before verification/signin
413
  'user_id': res.user.id if res.user else None
414
  }), 201
415
 
416
  except Exception as e:
417
- # Handle Supabase specific errors if needed (e.g., duplicate email)
418
  error_message = str(e)
419
- status_code = 400 # Default bad request
420
  if "User already registered" in error_message:
421
  error_message = "Email already exists."
422
- status_code = 409 # Conflict
423
  logging.error(f"Signup error: {error_message}")
424
  return jsonify({'error': error_message}), status_code
425
 
@@ -464,13 +465,7 @@ def signin():
464
 
465
  @app.route('/api/auth/google-signin', methods=['POST'])
466
  def google_signin():
467
- # This endpoint is tricky without a frontend.
468
- # Typically, the frontend uses Supabase JS client to handle the Google OAuth flow.
469
- # The frontend receives an access_token and refresh_token from Supabase after Google redirects.
470
- # The frontend then sends the access_token (as Bearer token) to this backend.
471
- # The backend verifies the token using verify_token helper.
472
- # So, this endpoint might just be for *associating* data *after* frontend login,
473
- # or it could exchange an auth code (more complex server-side flow).
474
 
475
  # Assuming frontend handles OAuth and sends Supabase session token:
476
  user, error = verify_token(request.headers.get('Authorization'))
@@ -553,20 +548,15 @@ def get_user_profile():
553
 
554
  @app.route('/api/tutor/process_input', methods=['POST'])
555
  def process_input_and_generate_notes():
556
- """
557
- Handles various input types, extracts content, generates notes,
558
- and saves material & notes to DB.
559
- """
560
  user, error = verify_token(request.headers.get('Authorization'))
561
  if error: return jsonify({'error': error['error']}), error['status']
562
  if not supabase or not gemini_model: return jsonify({'error': 'Backend service unavailable'}), 503
563
 
564
- # --- Check Credits (Example) ---
565
  profile_res = supabase.table('profiles').select('credits', 'suspended').eq('id', user.id).single().execute()
566
  if profile_res.data['suspended']:
567
  return jsonify({'error': 'Account suspended'}), 403
568
- if profile_res.data['credits'] < 1:
569
- return jsonify({'error': 'Insufficient credits'}), 402
570
 
571
 
572
  try:
@@ -640,8 +630,9 @@ def process_input_and_generate_notes():
640
  notes_id = notes_res.data[0]['id']
641
 
642
  # --- Deduct Credits (Example) ---
643
- # supabase.table('profiles').update({'credits': profile_res.data['credits'] - 1}).eq('id', user.id).execute()
644
- # ---
 
645
 
646
  return jsonify({
647
  'success': True,
@@ -666,11 +657,17 @@ def process_input_and_generate_notes():
666
 
667
  @app.route('/api/tutor/notes/<uuid:notes_id>/generate_quiz', methods=['POST'])
668
  def generate_quiz_for_notes(notes_id):
669
- """Generates a quiz based on existing notes."""
670
  user, error = verify_token(request.headers.get('Authorization'))
671
  if error: return jsonify({'error': error['error']}), error['status']
672
  if not supabase or not gemini_model: return jsonify({'error': 'Backend service unavailable'}), 503
673
 
 
 
 
 
 
 
 
674
  try:
675
  data = request.get_json()
676
  difficulty = data.get('difficulty', 'medium').lower()
@@ -707,6 +704,9 @@ def generate_quiz_for_notes(notes_id):
707
  if not quiz_res.data: raise Exception(f"Failed to save generated quiz: {quiz_res.error}")
708
  quiz_id = quiz_res.data[0]['id']
709
 
 
 
 
710
  return jsonify({
711
  'success': True,
712
  'quiz_id': quiz_id,
@@ -801,30 +801,15 @@ def submit_quiz_attempt(quiz_id):
801
 
802
  @app.route('/api/tutor/notes/<uuid:notes_id>/speak', methods=['GET'])
803
  def speak_notes(notes_id):
804
- """Generates TTS audio for notes and returns it or its URL."""
805
  user, error = verify_token(request.headers.get('Authorization'))
806
  if error: return jsonify({'error': error['error']}), error['status']
807
  if not supabase or not elevenlabs_client: return jsonify({'error': 'Backend service unavailable'}), 503
808
 
809
- try:
810
- # --- Fetch Notes Content ---
811
- notes_res = supabase.table('notes').select('content, user_id, tts_audio_url').eq('id', notes_id).maybe_single().execute()
812
- if not notes_res.data:
813
- return jsonify({'error': 'Notes not found'}), 404
814
- if notes_res.data['user_id'] != user.id:
815
- return jsonify({'error': 'You do not have permission to access these notes'}), 403
816
-
817
- # --- Check if audio already exists ---
818
- # existing_url = notes_res.data.get('tts_audio_url')
819
- # if existing_url:
820
- # # Optional: Check if the URL is still valid or regenerate
821
- # # For simplicity, we'll just return the existing one if present
822
- # # To force regeneration, add a query param like ?force=true
823
- # if not request.args.get('force'):
824
- # print(f"Returning existing TTS URL for notes {notes_id}: {existing_url}")
825
- # # You might want to redirect or return the URL itself
826
- # return jsonify({'success': True, 'audio_url': existing_url})
827
-
828
 
829
  notes_content = notes_res.data['content']
830
  if not notes_content:
@@ -853,17 +838,22 @@ def speak_notes(notes_id):
853
 
854
  # --- Update notes table with the URL ---
855
  supabase.table('notes').update({'tts_audio_url': audio_url}).eq('id', notes_id).execute()
 
 
 
856
 
857
  # --- Return the audio file directly ---
858
  # Set headers for browser playback/download
 
859
  return send_file(
860
  io.BytesIO(audio_bytes),
861
  mimetype=content_type,
862
  as_attachment=False, # Play inline if possible
863
  download_name=f'notes_{notes_id}.mp3'
864
  )
 
865
  # OR: Return the URL
866
- # return jsonify({'success': True, 'audio_url': audio_url})
867
 
868
  finally:
869
  os.remove(tmp_file_path) # Clean up temporary file
@@ -995,7 +985,95 @@ def admin_suspend_user(target_user_id):
995
  return jsonify({'error': str(e)}), 500
996
 
997
  # Add other admin endpoints (update credits, view specific data) similarly,
998
- # using Supabase table methods (.select, .update, .delete, .rpc for database functions).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
999
 
1000
 
1001
  # === Main Execution ===
 
62
  raise ValueError("Gemini API Key must be set in environment variables.")
63
  genai.configure(api_key=GEMINI_API_KEY)
64
  # Use a generally available model, adjust if you have access to specific previews
65
+ gemini_model = genai.GenerativeModel('gemini-2.0-flash-thinking-exp')
66
  print("Gemini API initialized successfully.")
67
  except Exception as e:
68
  print(f"Error initializing Gemini API: {e}")
 
389
 
390
  # === Authentication Endpoints ===
391
 
392
+
393
  @app.route('/api/auth/signup', methods=['POST'])
394
  def signup():
395
  if not supabase: return jsonify({'error': 'Service unavailable'}), 503
 
400
  if not email or not password:
401
  return jsonify({'error': 'Email and password are required'}), 400
402
 
 
403
  res = supabase.auth.sign_up({"email": email, "password": password})
404
 
405
+ # Ensure profile is created with 20 credits
406
+ supabase.table('profiles').upsert({
407
+ 'id': res.user.id,
408
+ 'email': email,
409
+ 'credits': 20
410
+ }).execute()
411
 
 
412
  return jsonify({
413
  'success': True,
414
  'message': 'Signup successful. Please check your email for verification.',
 
415
  'user_id': res.user.id if res.user else None
416
  }), 201
417
 
418
  except Exception as e:
 
419
  error_message = str(e)
420
+ status_code = 400
421
  if "User already registered" in error_message:
422
  error_message = "Email already exists."
423
+ status_code = 409
424
  logging.error(f"Signup error: {error_message}")
425
  return jsonify({'error': error_message}), status_code
426
 
 
465
 
466
  @app.route('/api/auth/google-signin', methods=['POST'])
467
  def google_signin():
468
+
 
 
 
 
 
 
469
 
470
  # Assuming frontend handles OAuth and sends Supabase session token:
471
  user, error = verify_token(request.headers.get('Authorization'))
 
548
 
549
  @app.route('/api/tutor/process_input', methods=['POST'])
550
  def process_input_and_generate_notes():
 
 
 
 
551
  user, error = verify_token(request.headers.get('Authorization'))
552
  if error: return jsonify({'error': error['error']}), error['status']
553
  if not supabase or not gemini_model: return jsonify({'error': 'Backend service unavailable'}), 503
554
 
 
555
  profile_res = supabase.table('profiles').select('credits', 'suspended').eq('id', user.id).single().execute()
556
  if profile_res.data['suspended']:
557
  return jsonify({'error': 'Account suspended'}), 403
558
+ if profile_res.data['credits'] < 2:
559
+ return jsonify({'error': 'Insufficient credits (Need 2)'}), 402
560
 
561
 
562
  try:
 
630
  notes_id = notes_res.data[0]['id']
631
 
632
  # --- Deduct Credits (Example) ---
633
+ new_credits = profile_res.data['credits'] - 2
634
+ supabase.table('profiles').update({'credits': new_credits}).eq('id', user.id).execute()
635
+
636
 
637
  return jsonify({
638
  'success': True,
 
657
 
658
  @app.route('/api/tutor/notes/<uuid:notes_id>/generate_quiz', methods=['POST'])
659
  def generate_quiz_for_notes(notes_id):
 
660
  user, error = verify_token(request.headers.get('Authorization'))
661
  if error: return jsonify({'error': error['error']}), error['status']
662
  if not supabase or not gemini_model: return jsonify({'error': 'Backend service unavailable'}), 503
663
 
664
+ profile_res = supabase.table('profiles').select('credits', 'suspended').eq('id', user.id).single().execute()
665
+ if profile_res.data['suspended']:
666
+ return jsonify({'error': 'Account suspended'}), 403
667
+ if profile_res.data['credits'] < 2:
668
+ return jsonify({'error': 'Insufficient credits (Need 2)'}), 402
669
+
670
+
671
  try:
672
  data = request.get_json()
673
  difficulty = data.get('difficulty', 'medium').lower()
 
704
  if not quiz_res.data: raise Exception(f"Failed to save generated quiz: {quiz_res.error}")
705
  quiz_id = quiz_res.data[0]['id']
706
 
707
+ new_credits = profile_res.data['credits'] - 2
708
+ supabase.table('profiles').update({'credits': new_credits}).eq('id', user.id).execute()
709
+
710
  return jsonify({
711
  'success': True,
712
  'quiz_id': quiz_id,
 
801
 
802
  @app.route('/api/tutor/notes/<uuid:notes_id>/speak', methods=['GET'])
803
  def speak_notes(notes_id):
 
804
  user, error = verify_token(request.headers.get('Authorization'))
805
  if error: return jsonify({'error': error['error']}), error['status']
806
  if not supabase or not elevenlabs_client: return jsonify({'error': 'Backend service unavailable'}), 503
807
 
808
+ profile_res = supabase.table('profiles').select('credits', 'suspended').eq('id', user.id).single().execute()
809
+ if profile_res.data['suspended']:
810
+ return jsonify({'error': 'Account suspended'}), 403
811
+ if profile_res.data['credits'] < 5:
812
+ return jsonify({'error': 'Insufficient credits (Need 5)'}), 402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
 
814
  notes_content = notes_res.data['content']
815
  if not notes_content:
 
838
 
839
  # --- Update notes table with the URL ---
840
  supabase.table('notes').update({'tts_audio_url': audio_url}).eq('id', notes_id).execute()
841
+ # Deduct 5 credits after successful generation
842
+ new_credits = profile_res.data['credits'] - 5
843
+ supabase.table('profiles').update({'credits': new_credits}).eq('id', user.id).execute()
844
 
845
  # --- Return the audio file directly ---
846
  # Set headers for browser playback/download
847
+ """
848
  return send_file(
849
  io.BytesIO(audio_bytes),
850
  mimetype=content_type,
851
  as_attachment=False, # Play inline if possible
852
  download_name=f'notes_{notes_id}.mp3'
853
  )
854
+ """
855
  # OR: Return the URL
856
+ return jsonify({'success': True, 'audio_url': audio_url})
857
 
858
  finally:
859
  os.remove(tmp_file_path) # Clean up temporary file
 
985
  return jsonify({'error': str(e)}), 500
986
 
987
  # Add other admin endpoints (update credits, view specific data) similarly,
988
+ # === Credit Management Endpoints ===
989
+
990
+ @app.route('/api/user/credits/request', methods=['POST'])
991
+ def request_credits():
992
+ user, error = verify_token(request.headers.get('Authorization'))
993
+ if error: return jsonify({'error': error['error']}), error['status']
994
+
995
+ try:
996
+ data = request.get_json()
997
+ amount = data.get('amount')
998
+ note = data.get('note', '')
999
+
1000
+ if not amount or not isinstance(amount, int) or amount <= 0:
1001
+ return jsonify({'error': 'Invalid amount (must be positive integer)'}), 400
1002
+
1003
+ res = supabase.table('credit_requests').insert({
1004
+ 'user_id': user.id,
1005
+ 'amount': amount,
1006
+ 'status': 'pending',
1007
+ 'note': note,
1008
+ 'created_at': datetime.now().isoformat()
1009
+ }).execute()
1010
+
1011
+ return jsonify({
1012
+ 'success': True,
1013
+ 'request_id': res.data[0]['id']
1014
+ }), 201
1015
+
1016
+ except Exception as e:
1017
+ logging.error(f"Credit request failed for user {user.id}: {e}")
1018
+ return jsonify({'error': str(e)}), 500
1019
+
1020
+ @app.route('/api/admin/credit-requests', methods=['GET'])
1021
+ def admin_get_credit_requests():
1022
+ user, error = verify_token(request.headers.get('Authorization'))
1023
+ if error: return jsonify({'error': error['error']}), error['status']
1024
+ is_admin, admin_error = verify_admin(user)
1025
+ if admin_error: return jsonify({'error': admin_error['error']}), admin_error['status']
1026
+
1027
+ try:
1028
+ status = request.args.get('status', 'pending')
1029
+ res = supabase.table('credit_requests').select('*').eq('status', status).execute()
1030
+ return jsonify(res.data), 200
1031
+ except Exception as e:
1032
+ logging.error(f"Admin credit requests fetch failed: {e}")
1033
+ return jsonify({'error': str(e)}), 500
1034
+
1035
+ @app.route('/api/admin/credit-requests/<uuid:request_id>', methods=['PUT'])
1036
+ def admin_review_credit_request(request_id):
1037
+ user, error = verify_token(request.headers.get('Authorization'))
1038
+ if error: return jsonify({'error': error['error']}), error['status']
1039
+ is_admin, admin_error = verify_admin(user)
1040
+ if admin_error: return jsonify({'error': admin_error['error']}), admin_error['status']
1041
+
1042
+ try:
1043
+ data = request.get_json()
1044
+ action = data.get('action')
1045
+ admin_note = data.get('note', '')
1046
+
1047
+ if action not in ['approve', 'decline']:
1048
+ return jsonify({'error': 'Invalid action'}), 400
1049
+
1050
+ req_res = supabase.table('credit_requests').select('*').eq('id', request_id).maybe_single().execute()
1051
+ if not req_res.data:
1052
+ return jsonify({'error': 'Request not found'}), 404
1053
+
1054
+ req = req_res.data
1055
+ if req['status'] != 'pending':
1056
+ return jsonify({'error': 'Request already processed'}), 400
1057
+
1058
+ update_data = {
1059
+ 'status': 'approved' if action == 'approve' else 'declined',
1060
+ 'reviewed_at': datetime.now().isoformat(),
1061
+ 'reviewed_by': user.id,
1062
+ 'admin_note': admin_note
1063
+ }
1064
+
1065
+ if action == 'approve':
1066
+ supabase.table('profiles').update(
1067
+ {'credits': supabase.table('profiles').credits + req['amount']}
1068
+ ).eq('id', req['user_id']).execute()
1069
+
1070
+ supabase.table('credit_requests').update(update_data).eq('id', request_id).execute()
1071
+
1072
+ return jsonify({'success': True}), 200
1073
+
1074
+ except Exception as e:
1075
+ logging.error(f"Credit request processing failed: {e}")
1076
+ return jsonify({'error': str(e)}), 500
1077
 
1078
 
1079
  # === Main Execution ===