Update main.py
Browse files
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-
|
| 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 |
-
#
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 420 |
if "User already registered" in error_message:
|
| 421 |
error_message = "Email already exists."
|
| 422 |
-
status_code = 409
|
| 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 |
-
|
| 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'] <
|
| 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 |
-
|
| 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 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 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 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ===
|