rairo commited on
Commit
02784e4
·
verified ·
1 Parent(s): 7e846fa

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +44 -1004
main.py CHANGED
@@ -17,8 +17,8 @@ from io import BytesIO
17
  import requests
18
  from elevenlabs import ElevenLabs
19
  # Import and configure Google GenAI, matching the Streamlit app
20
- from google import genai
21
- from google.genai import types
22
 
23
  # -----------------------------------------------------------------------------
24
  # 1. CONFIGURATION & INITIALIZATION
@@ -30,6 +30,7 @@ CORS(app)
30
 
31
  # --- Firebase Initialization ---
32
  try:
 
33
  credentials_json_string = os.environ.get("FIREBASE")
34
  if not credentials_json_string:
35
  raise ValueError("The FIREBASE environment variable is not set.")
@@ -49,1042 +50,81 @@ try:
49
  print("Firebase Admin SDK initialized successfully.")
50
  except Exception as e:
51
  print(f"FATAL: Error initializing Firebase: {e}")
52
- exit(1)
 
53
 
54
  # Initialize Firebase services
55
  bucket = storage.bucket()
56
  db_ref = db.reference()
57
 
58
 
59
- # --- Google GenAI Client Initialization (as per Streamlit app) ---
60
- try:
61
- api_key = os.environ.get("Gemini")
62
- if not api_key:
63
- raise ValueError("The 'Gemini' environment variable for the API key is not set.")
64
-
65
- client = genai.Client(api_key=api_key)
66
- print("Google GenAI Client initialized successfully.")
67
- except Exception as e:
68
- print(f"FATAL: Error initializing GenAI Client: {e}")
69
- exit(1)
70
-
71
- # --- Model Constants (as per Streamlit app) ---
72
- CATEGORY_MODEL = "gemini-2.0-flash-exp"
73
- GENERATION_MODEL = "gemini-2.0-flash-exp-image-generation"
74
- #TTS_MODEL = "gemini-2.5-flash-preview-tts"
75
-
76
-
77
  # -----------------------------------------------------------------------------
78
- # 2. HELPER FUNCTIONS (Adapted directly from Streamlit App & Template)
79
  # -----------------------------------------------------------------------------
80
 
81
- def verify_token(auth_header):
82
- """Verifies the Firebase ID token from the Authorization header."""
83
- if not auth_header or not auth_header.startswith('Bearer '):
84
- return None
85
- token = auth_header.split('Bearer ')[1]
86
- try:
87
- decoded_token = auth.verify_id_token(token)
88
- return decoded_token['uid']
89
- except Exception as e:
90
- print(f"Token verification failed: {e}")
91
- return None
92
-
93
- def verify_admin(auth_header):
94
- """Verifies if the user is an admin."""
95
- uid = verify_token(auth_header)
96
- if not uid:
97
- raise PermissionError('Invalid or missing user token')
98
-
99
- user_ref = db_ref.child(f'users/{uid}')
100
- user_data = user_ref.get()
101
- if not user_data or not user_data.get('is_admin', False):
102
- raise PermissionError('Admin access required')
103
- return uid
104
-
105
- def upload_to_storage(data_bytes, destination_blob_name, content_type):
106
- """Uploads a bytes object to Firebase Storage and returns its public URL."""
107
- blob = bucket.blob(destination_blob_name)
108
- blob.upload_from_string(data_bytes, content_type=content_type)
109
- blob.make_public()
110
- return blob.public_url
111
-
112
- def parse_numbered_steps(text):
113
- """Helper to parse numbered steps out of Gemini text."""
114
- text = "\n" + text
115
- steps_found = re.findall(r"\n\s*(\d+)\.\s*(.*)", text, re.MULTILINE)
116
- return [{"stepNumber": int(num), "text": desc.strip()} for num, desc in steps_found]
117
-
118
- def _convert_pcm_to_wav(pcm_data, sample_rate=24000, channels=1, sample_width=2):
119
- """Wraps raw PCM audio data in a WAV container in memory."""
120
- audio_buffer = io.BytesIO()
121
- with wave.open(audio_buffer, 'wb') as wf:
122
- wf.setnchannels(channels)
123
- wf.setsampwidth(sample_width)
124
- wf.setframerate(sample_rate)
125
- wf.writeframes(pcm_data)
126
- audio_buffer.seek(0)
127
- return audio_buffer.getvalue()
128
-
129
-
130
- #Gemini tts implementation SOTA but slow
131
- '''
132
- def generate_tts_audio_and_upload(text_to_speak, uid, project_id, step_num):
133
- """Generates audio using the exact method from the Streamlit app and uploads it."""
134
- try:
135
- response = client.models.generate_content(
136
- model=TTS_MODEL,
137
- contents=f"""You are an articulate AI assistant — confident and precise like Jarvis.Rephrase the instruction naturally using simple expert language.
138
- Speak with a brisk, clear British accent.
139
- Avoid reading word for word — explain it like you know it.
140
- No quips or acknowledging the prompt just narrate this step:
141
- {text_to_speak}""",
142
- config=types.GenerateContentConfig(
143
- response_modalities=["AUDIO"],
144
- speech_config=types.SpeechConfig(
145
- voice_config=types.VoiceConfig(
146
- prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name='Sadaltager')
147
- )
148
- ),
149
- )
150
- )
151
- audio_part = response.candidates[0].content.parts[0]
152
- audio_data = audio_part.inline_data.data
153
- mime_type = audio_part.inline_data.mime_type
154
-
155
- final_audio_bytes = _convert_pcm_to_wav(audio_data) if 'pcm' in mime_type else audio_data
156
-
157
- audio_path = f"users/{uid}/projects/{project_id}/narrations/step_{step_num}.wav"
158
- return upload_to_storage(final_audio_bytes, audio_path, 'audio/wav')
159
- except Exception as e:
160
- print(f"Error during TTS generation for step {step_num}: {e}")
161
- return None
162
- '''
163
-
164
- # DeepGram faster and efficient
165
- def generate_tts_audio_and_upload(text_to_speak, uid, project_id, step_num):
166
- """
167
- Generates audio using the Deepgram TTS API and uploads it to Firebase Storage.
168
- This is a drop-in replacement for the previous Google GenAI TTS function.
169
- """
170
- try:
171
- # --- Step 1: Get the Deepgram API Key from environment variables ---
172
- api_key = os.environ.get("DEEPGRAM_API_KEY")
173
- if not api_key:
174
- print("FATAL: DEEPGRAM_API_KEY environment variable not set.")
175
- return None
176
 
177
- # --- Step 2: Define the API endpoint and headers ---
178
- # The model 'aura-2-draco-en' is specified as a query parameter in the URL.
179
- DEEPGRAM_URL = "https://api.deepgram.com/v1/speak?model=aura-2-draco-en"
180
-
181
- headers = {
182
- "Authorization": f"Token {api_key}",
183
- "Content-Type": "text/plain" # As per Deepgram's requirement for this type of request
184
- }
185
-
186
- # --- Step 3: Make the API call to Deepgram ---
187
- # Deepgram expects the raw text as the request body, not in a JSON object.
188
- # We send the text directly in the 'data' parameter.
189
- response = requests.post(DEEPGRAM_URL, headers=headers, data=text_to_speak.encode('utf-8'))
190
-
191
- # Raise an exception for bad status codes (4xx or 5xx)
192
- response.raise_for_status()
193
-
194
- # The raw audio data is in the response content
195
- audio_data = response.content
196
-
197
- # --- Step 4: Upload the received audio to Firebase Storage ---
198
- # The output format from this Deepgram model is MP3.
199
- audio_path = f"users/{uid}/projects/{project_id}/narrations/step_{step_num}.mp3"
200
-
201
- # The MIME type for MP3 is 'audio/mpeg'.
202
- narration_url = upload_to_storage(audio_data, audio_path, 'audio/mpeg')
203
-
204
- return narration_url
205
-
206
- except requests.exceptions.RequestException as e:
207
- print(f"Error during Deepgram API call for step {step_num}: {e}")
208
- # Log the response body if available for more detailed error info
209
- if e.response is not None:
210
- print(f"Deepgram Error Response: {e.response.text}")
211
- return None
212
- except Exception as e:
213
- print(f"An unexpected error occurred during TTS generation for step {step_num}: {e}")
214
- return None
215
-
216
-
217
- def send_text_request(model_name, prompt, image):
218
- """Helper to send requests that expect only a text response."""
219
- try:
220
- chat = client.chats.create(model=model_name)
221
- response = chat.send_message([prompt, image])
222
- response_text = "".join(part.text for part in response.candidates[0].content.parts if hasattr(part, 'text'))
223
- return response_text.strip()
224
- except Exception as e:
225
- print(f"Error with model {model_name}: {e}")
226
- return None
227
-
228
-
229
- import logging
230
-
231
- # Configure logging at the top of your file if not already done
232
- logging.basicConfig(level=logging.INFO)
233
- logger = logging.getLogger(__name__)
234
-
235
- # =============================================================================
236
- # OPEN IMAGE PROXY ENDPOINT (NO AUTHENTICATION)
237
- # =============================================================================
238
- @app.route('/api/image-proxy', methods=['GET'])
239
- def image_proxy():
240
- image_url = request.args.get('url')
241
- logger.info(f"[IMAGE PROXY] Received URL: {image_url}")
242
-
243
- if not image_url:
244
- logger.error("[IMAGE PROXY] ERROR: URL parameter is missing")
245
- return jsonify({'error': 'URL parameter is missing.'}), 400
246
-
247
- try:
248
- # Parse Firebase Storage URL
249
- # Expected format: https://storage.googleapis.com/bucket-name/path/to/file.ext
250
- if 'storage.googleapis.com' not in image_url:
251
- logger.error(f"[IMAGE PROXY] ERROR: Invalid Firebase Storage URL: {image_url}")
252
- return jsonify({'error': 'Invalid Firebase Storage URL.'}), 400
253
-
254
- logger.info(f"[IMAGE PROXY] Parsing URL: {image_url}")
255
-
256
- # Extract bucket name and blob path from the URL
257
- url_parts = image_url.split('storage.googleapis.com/')[1]
258
- logger.info(f"[IMAGE PROXY] URL parts after split: {url_parts}")
259
-
260
- # Remove query parameters if present
261
- url_parts = url_parts.split('?')[0]
262
- logger.info(f"[IMAGE PROXY] URL parts after removing query params: {url_parts}")
263
-
264
- # Split into bucket name and blob path
265
- path_components = url_parts.split('/', 1)
266
- logger.info(f"[IMAGE PROXY] Path components: {path_components}")
267
-
268
- if len(path_components) < 2:
269
- logger.error(f"[IMAGE PROXY] ERROR: Invalid URL format - path_components: {path_components}")
270
- return jsonify({'error': 'Invalid URL format.'}), 400
271
-
272
- url_bucket_name = path_components[0]
273
- blob_path = path_components[1]
274
-
275
- logger.info(f"[IMAGE PROXY] Extracted bucket name: {url_bucket_name}")
276
- logger.info(f"[IMAGE PROXY] Extracted blob path: {blob_path}")
277
-
278
- # Verify bucket name matches (optional security check)
279
- expected_bucket_name = bucket.name
280
- logger.info(f"[IMAGE PROXY] Expected bucket name: {expected_bucket_name}")
281
-
282
- if url_bucket_name != expected_bucket_name:
283
- logger.error(f"[IMAGE PROXY] ERROR: Bucket name mismatch - URL: {url_bucket_name}, Expected: {expected_bucket_name}")
284
- return jsonify({'error': 'Bucket name mismatch.'}), 403
285
-
286
- logger.info(f"[IMAGE PROXY] Creating blob object for path: {blob_path}")
287
-
288
- # Get the blob
289
- blob = bucket.blob(blob_path)
290
-
291
- logger.info(f"[IMAGE PROXY] Checking if blob exists...")
292
- if not blob.exists():
293
- logger.error(f"[IMAGE PROXY] ERROR: Image not found at path: {blob_path}")
294
- return jsonify({'error': 'Image not found.'}), 404
295
-
296
- logger.info(f"[IMAGE PROXY] Downloading blob...")
297
- # Download and return the image
298
- image_bytes = blob.download_as_bytes()
299
- content_type = blob.content_type or 'application/octet-stream'
300
-
301
- logger.info(f"[IMAGE PROXY] Successfully downloaded {len(image_bytes)} bytes, content-type: {content_type}")
302
-
303
- # Add cache headers for better performance
304
- response = Response(image_bytes, content_type=content_type)
305
- response.headers['Cache-Control'] = 'public, max-age=3600' # Cache for 1 hour
306
- return response
307
-
308
- except IndexError as e:
309
- logger.error(f"[IMAGE PROXY] URL parsing IndexError: {e}")
310
- logger.error(f"[IMAGE PROXY] URL was: {image_url}")
311
- return jsonify({'error': 'Invalid URL format.'}), 400
312
- except Exception as e:
313
- # This will catch parsing errors or other unexpected issues.
314
- logger.error(f"[IMAGE PROXY] Unexpected error: {e}")
315
- logger.error(f"[IMAGE PROXY] Error type: {type(e).__name__}")
316
- logger.error(f"[IMAGE PROXY] URL was: {image_url}")
317
- import traceback
318
- logger.error(f"[IMAGE PROXY] Full traceback: {traceback.format_exc()}")
319
- return jsonify({'error': 'Internal server error processing the image request.'}), 500
320
 
321
  # -----------------------------------------------------------------------------
322
  # 3. AUTHENTICATION & USER MANAGEMENT
323
  # -----------------------------------------------------------------------------
 
324
 
325
- @app.route('/api/auth/signup', methods=['POST'])
326
- def signup():
327
- try:
328
- data = request.get_json()
329
- email, password = data.get('email'), data.get('password')
330
- if not email or not password: return jsonify({'error': 'Email and password are required'}), 400
331
-
332
- user = auth.create_user(email=email, password=password)
333
- user_ref = db_ref.child(f'users/{user.uid}')
334
- user_data = {'email': email, 'credits': 15, 'is_admin': False, 'createdAt': datetime.utcnow().isoformat()}
335
- user_ref.set(user_data)
336
- return jsonify({'success': True, 'uid': user.uid, **user_data}), 201
337
- except Exception as e:
338
- return jsonify({'error': str(e)}), 400
339
-
340
- @app.route('/api/auth/social-signin', methods=['POST'])
341
- def social_signin():
342
- """
343
- Ensures a user record exists in the Realtime Database after a social login
344
- (like Google Sign-In). The client should call this endpoint immediately after
345
- a successful Firebase authentication on their side, sending the
346
- Firebase ID Token. This creates the user's profile in our database if it's
347
- their first time.
348
- """
349
- uid = verify_token(request.headers.get('Authorization'))
350
- if not uid:
351
- return jsonify({'error': 'Invalid or expired token'}), 401
352
-
353
- user_ref = db_ref.child(f'users/{uid}')
354
- user_data = user_ref.get()
355
-
356
- if user_data:
357
- # User already has a profile in our DB, return it.
358
- return jsonify({'uid': uid, **user_data}), 200
359
- else:
360
- # This is a new user (first social login), create their profile.
361
- try:
362
- # Get user details from Firebase Auth service. [8]
363
- firebase_user = auth.get_user(uid)
364
-
365
- # Create the user profile in our Realtime Database
366
- new_user_data = {
367
- 'email': firebase_user.email,
368
- 'credits': 15, # Standard starting credits
369
- 'is_admin': False,
370
- 'createdAt': datetime.utcnow().isoformat()
371
- }
372
- user_ref.set(new_user_data)
373
-
374
- # Return the newly created profile
375
- return jsonify({'success': True, 'uid': uid, **new_user_data}), 201
376
- except Exception as e:
377
- print(f"Error creating profile for new social user {uid}: {e}")
378
- return jsonify({'error': f'Failed to create user profile: {str(e)}'}), 500
379
-
380
- @app.route('/api/user/profile', methods=['GET'])
381
- def get_user_profile():
382
- uid = verify_token(request.headers.get('Authorization'))
383
- if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
384
-
385
- user_data = db_ref.child(f'users/{uid}').get()
386
- if not user_data: return jsonify({'error': 'User not found'}), 404
387
-
388
- return jsonify({'uid': uid, **user_data})
389
-
390
- # -----------------------------------------------------------------------------
391
- # 4. FEEDBACK AND CREDIT REQUESTS (USER-FACING)
392
- # -----------------------------------------------------------------------------
393
-
394
- @app.route('/api/feedback', methods=['POST'])
395
- def submit_feedback():
396
- uid = verify_token(request.headers.get('Authorization'))
397
- if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
398
-
399
- try:
400
- data = request.get_json()
401
- if not data or not data.get('message'): return jsonify({'error': 'Message is required'}), 400
402
-
403
- user_email = (db_ref.child(f'users/{uid}').get() or {}).get('email', 'unknown')
404
-
405
- feedback_ref = db_ref.child('feedback').push()
406
- feedback_record = {
407
- "feedbackId": feedback_ref.key,
408
- "userId": uid,
409
- "userEmail": user_email,
410
- "type": data.get('type', 'general'),
411
- "message": data.get('message'),
412
- "createdAt": datetime.utcnow().isoformat(),
413
- "status": "open"
414
- }
415
- feedback_ref.set(feedback_record)
416
- return jsonify({"success": True, "feedbackId": feedback_ref.key}), 201
417
- except Exception as e:
418
- return jsonify({'error': str(e)}), 500
419
-
420
- @app.route('/api/user/request-credits', methods=['POST'])
421
- def request_credits():
422
- uid = verify_token(request.headers.get('Authorization'))
423
- if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
424
-
425
- try:
426
- data = request.get_json()
427
- if not data or 'requested_credits' not in data: return jsonify({'error': 'requested_credits is required'}), 400
428
-
429
- request_ref = db_ref.child('credit_requests').push()
430
- request_ref.set({
431
- 'requestId': request_ref.key,
432
- 'userId': uid,
433
- 'requested_credits': data['requested_credits'],
434
- 'status': 'pending',
435
- 'requestedAt': datetime.utcnow().isoformat()
436
- })
437
- return jsonify({'success': True, 'requestId': request_ref.key})
438
- except Exception as e:
439
- return jsonify({'error': str(e)}), 500
440
 
441
  # -----------------------------------------------------------------------------
442
- # 5. ADMIN ENDPOINTS
443
  # -----------------------------------------------------------------------------
444
 
445
- @app.route('/api/admin/profile', methods=['GET'])
446
- def get_admin_profile():
447
- try:
448
- admin_uid = verify_admin(request.headers.get('Authorization'))
449
-
450
- # Fetch all necessary data from Firebase in one go
451
- all_users = db_ref.child('users').get() or {}
452
- all_projects = db_ref.child('projects').get() or {}
453
- all_feedback = db_ref.child('feedback').get() or {}
454
- all_credit_requests = db_ref.child('credit_requests').get() or {}
455
-
456
- # --- User Statistics Calculation ---
457
- total_users = len(all_users)
458
- admin_count = 0
459
- total_credits_in_system = 0
460
- new_users_last_7_days = 0
461
- seven_days_ago = datetime.utcnow() - timedelta(days=7)
462
-
463
- for user_data in all_users.values():
464
- if user_data.get('is_admin', False):
465
- admin_count += 1
466
- total_credits_in_system += user_data.get('credits', 0)
467
-
468
- # Check for new users
469
- try:
470
- created_at_str = user_data.get('createdAt')
471
- if created_at_str:
472
- # Accommodate different possible ISO formats
473
- user_created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
474
- if user_created_at.replace(tzinfo=None) > seven_days_ago:
475
- new_users_last_7_days += 1
476
- except (ValueError, TypeError):
477
- # Ignore if date format is invalid or missing
478
- pass
479
-
480
- # --- Project Statistics Calculation ---
481
- total_projects = len(all_projects)
482
- projects_by_status = {
483
- "awaiting_approval": 0,
484
- "awaiting_selection": 0,
485
- "ready": 0,
486
- "unknown": 0
487
- }
488
- projects_by_category = {}
489
-
490
- for project_data in all_projects.values():
491
- # Tally by status
492
- status = project_data.get('status', 'unknown')
493
- projects_by_status[status] = projects_by_status.get(status, 0) + 1
494
-
495
- # Tally by category
496
- category = project_data.get('category', 'N/A')
497
- projects_by_category[category] = projects_by_category.get(category, 0) + 1
498
-
499
- # --- System Health Calculation ---
500
- open_feedback_count = sum(1 for fb in all_feedback.values() if fb.get('status') == 'open')
501
- pending_requests_count = sum(1 for req in all_credit_requests.values() if req.get('status') == 'pending')
502
-
503
- # Assemble the final response object
504
- admin_personal_data = all_users.get(admin_uid, {})
505
-
506
- response_data = {
507
- 'uid': admin_uid,
508
- 'email': admin_personal_data.get('email'),
509
- 'credits': admin_personal_data.get('credits'),
510
- 'is_admin': True,
511
- 'dashboardStats': {
512
- 'users': {
513
- 'total': total_users,
514
- 'admins': admin_count,
515
- 'regular': total_users - admin_count,
516
- 'newLast7Days': new_users_last_7_days,
517
- 'totalCreditsInSystem': total_credits_in_system
518
- },
519
- 'projects': {
520
- 'total': total_projects,
521
- 'byStatus': projects_by_status,
522
- 'byCategory': projects_by_category
523
- },
524
- 'system': {
525
- 'openFeedback': open_feedback_count,
526
- 'pendingCreditRequests': pending_requests_count
527
- }
528
- }
529
- }
530
-
531
- return jsonify(response_data), 200
532
-
533
- except PermissionError as e:
534
- return jsonify({'error': str(e)}), 403 # Use 403 Forbidden for permission issues
535
- except Exception as e:
536
- print(traceback.format_exc())
537
- return jsonify({'error': f"An internal error occurred: {e}"}), 500
538
-
539
- @app.route('/api/admin/credit_requests', methods=['GET'])
540
- def list_credit_requests():
541
- try:
542
- verify_admin(request.headers.get('Authorization'))
543
- requests_data = db_ref.child('credit_requests').get() or {}
544
- return jsonify(list(requests_data.values()))
545
- except Exception as e:
546
- return jsonify({'error': str(e)}), 500
547
-
548
- @app.route('/api/admin/credit_requests/<string:request_id>', methods=['PUT'])
549
- def process_credit_request(request_id):
550
- try:
551
- admin_uid = verify_admin(request.headers.get('Authorization'))
552
- req_ref = db_ref.child(f'credit_requests/{request_id}')
553
- req_data = req_ref.get()
554
- if not req_data: return jsonify({'error': 'Credit request not found'}), 404
555
-
556
- decision = request.json.get('decision')
557
- if decision not in ['approved', 'declined']: return jsonify({'error': 'Decision must be "approved" or "declined"'}), 400
558
-
559
- if decision == 'approved':
560
- user_ref = db_ref.child(f'users/{req_data["userId"]}')
561
- user_data = user_ref.get()
562
- if user_data:
563
- new_total = user_data.get('credits', 0) + int(req_data.get('requested_credits', 0))
564
- user_ref.update({'credits': new_total})
565
-
566
- req_ref.update({'status': decision, 'processedBy': admin_uid, 'processedAt': datetime.utcnow().isoformat()})
567
- return jsonify({'success': True, 'message': f'Request {decision}.'})
568
- except Exception as e:
569
- return jsonify({'error': str(e)}), 500
570
-
571
- @app.route('/api/admin/feedback', methods=['GET'])
572
- def admin_view_feedback():
573
- try:
574
- verify_admin(request.headers.get('Authorization'))
575
- feedback_data = db_ref.child('feedback').get() or {}
576
- return jsonify(list(feedback_data.values()))
577
- except Exception as e:
578
- return jsonify({'error': str(e)}), 500
579
-
580
- @app.route('/api/admin/users', methods=['GET'])
581
- def admin_list_users():
582
- try:
583
- verify_admin(request.headers.get('Authorization'))
584
- all_users = db_ref.child('users').get() or {}
585
- user_list = [{'uid': uid, **data} for uid, data in all_users.items()]
586
- return jsonify(user_list)
587
- except Exception as e:
588
- return jsonify({'error': str(e)}), 500
589
-
590
- @app.route('/api/admin/users/<string:uid>/credits', methods=['PUT'])
591
- def admin_update_credits(uid):
592
- try:
593
- verify_admin(request.headers.get('Authorization'))
594
- add_credits = request.json.get('add_credits')
595
- if add_credits is None: return jsonify({'error': 'add_credits is required'}), 400
596
-
597
- user_ref = db_ref.child(f'users/{uid}')
598
- user_data = user_ref.get()
599
- if not user_data: return jsonify({'error': 'User not found'}), 404
600
-
601
- new_total = user_data.get('credits', 0) + float(add_credits)
602
- user_ref.update({'credits': new_total})
603
- return jsonify({'success': True, 'new_total_credits': new_total})
604
- except Exception as e:
605
- return jsonify({'error': str(e)}), 500
606
-
607
- # -----------------------------------------------------------------------------
608
- # 6. DIY PROJECT ENDPOINTS (Core Logic)
609
- # -----------------------------------------------------------------------------
610
- # (The project endpoints from the previous answer go here, unchanged)
611
- @app.route('/api/projects', methods=['POST'])
612
- def create_project():
613
- uid = verify_token(request.headers.get('Authorization'))
614
- if not uid: return jsonify({'error': 'Unauthorized'}), 401
615
-
616
- user_ref = db_ref.child(f'users/{uid}')
617
- user_data = user_ref.get()
618
- if not user_data or user_data.get('credits', 0) < 1:
619
- return jsonify({'error': 'Insufficient credits'}), 402
620
-
621
- if 'image' not in request.files:
622
- return jsonify({'error': 'Image file is required'}), 400
623
-
624
- image_file = request.files['image']
625
- context_text = request.form.get('contextText', '')
626
- image_bytes = image_file.read()
627
- pil_image = Image.open(io.BytesIO(image_bytes))
628
-
629
- try:
630
- category_prompt = (
631
- "You are an expert DIY assistant. Analyze the user's image and context. "
632
- f"Context: '{context_text}'. "
633
- "Categorize the project into ONE of the following: "
634
- "Home Appliance Repair, Automotive Maintenance, Gardening & Urban Farming, "
635
- "Upcycling & Sustainable Crafts, or DIY Project Creation. "
636
- "Reply with ONLY the category name."
637
- )
638
- category = send_text_request(CATEGORY_MODEL, category_prompt, pil_image)
639
- if not category: return jsonify({'error': 'Failed to get project category from AI.'}), 500
640
-
641
- plan_prompt = f"""
642
- You are an expert DIY assistant in the category: {category}.
643
- User Context: "{context_text if context_text else 'No context provided.'}"
644
- Based on the image and context, perform the following:
645
- 1. **Title:** Create a short, clear title for this project.
646
- 2. **Description:** Write a brief, one-paragraph description of the goal.
647
- 3. **Initial Plan:**
648
- - If 'Upcycling & Sustainable Crafts' AND no specific project is mentioned, propose three distinct project options as a numbered list under "UPCYCLING OPTIONS:".
649
- - For all other cases, briefly outline the main stages of the proposed solution.
650
- Structure your response EXACTLY like this:
651
- TITLE: [Your title]
652
- DESCRIPTION: [Your description]
653
- INITIAL PLAN:
654
- [Your plan or 3 options]
655
- """
656
- plan_response = send_text_request(GENERATION_MODEL, plan_prompt, pil_image)
657
- if not plan_response: return jsonify({'error': 'Failed to generate project plan from AI.'}), 500
658
-
659
- title = re.search(r"TITLE:\s*(.*)", plan_response).group(1).strip()
660
- description = re.search(r"DESCRIPTION:\s*(.*)", plan_response, re.DOTALL).group(1).strip()
661
- initial_plan_text = re.search(r"INITIAL PLAN:\s*(.*)", plan_response, re.DOTALL).group(1).strip()
662
-
663
- upcycling_options = re.findall(r"^\s*\d+\.\s*(.*)", initial_plan_text, re.MULTILINE) if "UPCYCLING OPTIONS:" in initial_plan_text else []
664
- initial_plan = initial_plan_text if not upcycling_options else ""
665
- status = "awaiting_selection" if upcycling_options else "awaiting_approval"
666
-
667
- project_id = str(uuid.uuid4())
668
- image_path = f"users/{uid}/projects/{project_id}/initial_image.png"
669
- image_url = upload_to_storage(image_bytes, image_path, content_type=image_file.content_type)
670
-
671
- project_data = {
672
- "uid": uid, "projectId": project_id, "status": status, "createdAt": datetime.utcnow().isoformat(),
673
- "userImageURL": image_url, "contextText": context_text, "projectTitle": title,
674
- "projectDescription": description, "category": category, "initialPlan": initial_plan,
675
- "upcyclingOptions": upcycling_options, "toolsList": [], "steps": []
676
- }
677
- db_ref.child(f'projects/{project_id}').set(project_data)
678
-
679
- user_ref.update({'credits': user_data.get('credits', 0) - 1})
680
- return jsonify(project_data), 201
681
-
682
- except Exception as e:
683
- print(traceback.format_exc())
684
- return jsonify({'error': f"An error occurred: {e}"}), 500
685
-
686
-
687
- @app.route('/api/projects/<string:project_id>/approve', methods=['PUT'])
688
- def approve_project_plan(project_id):
689
- start_time = time.time()
690
- logger.info(f"[PROJECT APPROVAL] Starting approval process for project: {project_id}")
691
-
692
- # Authorization timing
693
- auth_start = time.time()
694
- uid = verify_token(request.headers.get('Authorization'))
695
- if not uid:
696
- logger.error(f"[PROJECT APPROVAL] ERROR: Unauthorized access attempt for project: {project_id}")
697
- return jsonify({'error': 'Unauthorized'}), 401
698
- auth_time = time.time() - auth_start
699
- logger.info(f"[PROJECT APPROVAL] Authorization completed in {auth_time:.3f}s for user: {uid}")
700
-
701
- # User data fetch timing
702
- user_fetch_start = time.time()
703
- user_ref = db_ref.child(f'users/{uid}')
704
- user_data = user_ref.get()
705
- if not user_data or user_data.get('credits', 0) < 5:
706
- logger.error(f"[PROJECT APPROVAL] ERROR: Insufficient credits for user: {uid}, credits: {user_data.get('credits', 0) if user_data else 0}")
707
- return jsonify({'error': 'Insufficient credits'}), 402
708
- user_fetch_time = time.time() - user_fetch_start
709
- logger.info(f"[PROJECT APPROVAL] User data fetch completed in {user_fetch_time:.3f}s, credits: {user_data.get('credits', 0)}")
710
-
711
- # Project data fetch timing
712
- project_fetch_start = time.time()
713
- project_ref = db_ref.child(f'projects/{project_id}')
714
- project_data = project_ref.get()
715
- if not project_data or project_data.get('uid') != uid:
716
- logger.error(f"[PROJECT APPROVAL] ERROR: Project not found or access denied - project_id: {project_id}, uid: {uid}")
717
- return jsonify({'error': 'Project not found or access denied'}), 404
718
- project_fetch_time = time.time() - project_fetch_start
719
- logger.info(f"[PROJECT APPROVAL] Project data fetch completed in {project_fetch_time:.3f}s for project: {project_data.get('projectTitle', 'Unknown')}")
720
-
721
- # Image download and processing timing
722
- selected_option = request.json.get('selectedOption')
723
- logger.info(f"[PROJECT APPROVAL] Selected option: {selected_option}")
724
-
725
- image_download_start = time.time()
726
- response = requests.get(project_data['userImageURL'])
727
- image_download_time = time.time() - image_download_start
728
- logger.info(f"[PROJECT APPROVAL] Image download completed in {image_download_time:.3f}s, size: {len(response.content)} bytes")
729
-
730
- image_processing_start = time.time()
731
- pil_image = Image.open(io.BytesIO(response.content)).convert('RGB')
732
- image_processing_time = time.time() - image_processing_start
733
- logger.info(f"[PROJECT APPROVAL] Image processing completed in {image_processing_time:.3f}s")
734
-
735
- # Context preparation timing
736
- context_start = time.time()
737
- context = (
738
- f"The user chose the upcycling project: '{selected_option}'."
739
- if selected_option
740
- else f"The user has approved the plan for '{project_data['projectTitle']}'."
741
- )
742
-
743
- detailed_prompt = f"""
744
- You are a DIY expert. The user wants to proceed with the project titled "{project_data['projectTitle']}".
745
- {context}
746
- Provide a detailed guide. For each step, you MUST provide a simple, clear illustrative image.
747
- Format your response EXACTLY like this:
748
-
749
- TOOLS AND MATERIALS:
750
- - Tool A
751
- - Material B
752
-
753
- STEPS(Maximum 5 steps):
754
- 1. First step instructions.
755
- 2. Second step instructions...
756
- """
757
- context_time = time.time() - context_start
758
- logger.info(f"[PROJECT APPROVAL] Context preparation completed in {context_time:.3f}s")
759
-
760
- try:
761
- # AI generation timing
762
- ai_start = time.time()
763
- logger.info(f"[PROJECT APPROVAL] Starting AI generation with model: {GENERATION_MODEL}")
764
-
765
- chat = client.chats.create(
766
- model=GENERATION_MODEL,
767
- config=types.GenerateContentConfig(response_modalities=["Text", "Image"])
768
- )
769
- full_resp = chat.send_message([detailed_prompt, pil_image])
770
- ai_time = time.time() - ai_start
771
- logger.info(f"[PROJECT APPROVAL] AI generation completed in {ai_time:.3f}s")
772
-
773
- # Response parsing timing
774
- parsing_start = time.time()
775
- gen_parts = full_resp.candidates[0].content.parts
776
-
777
- combined_text = ""
778
- inline_images = []
779
- for part in gen_parts:
780
- if part.text is not None:
781
- combined_text += part.text + "\n"
782
- if part.inline_data is not None:
783
- img = Image.open(io.BytesIO(part.inline_data.data)).convert('RGB')
784
- inline_images.append(img)
785
-
786
- combined_text = combined_text.strip()
787
- parsing_time = time.time() - parsing_start
788
- logger.info(f"[PROJECT APPROVAL] Response parsing completed in {parsing_time:.3f}s, found {len(inline_images)} images")
789
-
790
- # Text extraction timing
791
- extraction_start = time.time()
792
- tools_section = re.search(r"TOOLS AND MATERIALS:\s*(.*?)\s*STEPS:", combined_text, re.DOTALL).group(1).strip()
793
- steps_section = re.search(r"STEPS:\s*(.*)", combined_text, re.DOTALL).group(1).strip()
794
-
795
- tools_list = [line.strip("- ").strip() for line in tools_section.split('\n') if line.strip()]
796
- parsed_steps = parse_numbered_steps(steps_section)
797
- extraction_time = time.time() - extraction_start
798
- logger.info(f"[PROJECT APPROVAL] Text extraction completed in {extraction_time:.3f}s, tools: {len(tools_list)}, steps: {len(parsed_steps)}")
799
-
800
- if len(parsed_steps) != len(inline_images):
801
- logger.error(f"[PROJECT APPROVAL] ERROR: AI response mismatch - Steps: {len(parsed_steps)}, Images: {len(inline_images)}")
802
- return jsonify({'error': 'AI response mismatch: Steps and images do not match.'}), 500
803
-
804
- # Step processing timing
805
- step_processing_start = time.time()
806
- final_steps = []
807
- total_upload_time = 0
808
- total_tts_time = 0
809
-
810
- for i, step_info in enumerate(parsed_steps):
811
- logger.info(f"[PROJECT APPROVAL] Processing step {i+1}/{len(parsed_steps)}")
812
-
813
- # Image upload timing
814
- image_upload_start = time.time()
815
- img_byte_arr = io.BytesIO()
816
- inline_images[i].save(img_byte_arr, format='JPEG', optimize=True, quality=70)
817
- img_path = f"users/{uid}/projects/{project_id}/steps/step_{i+1}_image.jpg"
818
- img_url = upload_to_storage(img_byte_arr.getvalue(), img_path, 'image/jpeg')
819
- image_upload_time = time.time() - image_upload_start
820
- total_upload_time += image_upload_time
821
- logger.info(f"[PROJECT APPROVAL] Step {i+1} image upload completed in {image_upload_time:.3f}s")
822
-
823
- # TTS generation timing
824
- tts_start = time.time()
825
- narration_url = generate_tts_audio_and_upload(step_info['text'], uid, project_id, i + 1)
826
- tts_time = time.time() - tts_start
827
- total_tts_time += tts_time
828
- logger.info(f"[PROJECT APPROVAL] Step {i+1} TTS generation completed in {tts_time:.3f}s")
829
-
830
- step_info.update({
831
- "imageUrl": img_url,
832
- "narrationUrl": narration_url,
833
- "isDone": False,
834
- "notes": ""
835
- })
836
- final_steps.append(step_info)
837
-
838
- step_processing_time = time.time() - step_processing_start
839
- logger.info(f"[PROJECT APPROVAL] All steps processing completed in {step_processing_time:.3f}s")
840
- logger.info(f"[PROJECT APPROVAL] Total upload time: {total_upload_time:.3f}s, Total TTS time: {total_tts_time:.3f}s")
841
-
842
- # Database update timing
843
- db_update_start = time.time()
844
- update_data = {
845
- "status": "ready",
846
- "toolsList": tools_list,
847
- "steps": final_steps,
848
- "selectedOption": selected_option or ""
849
- }
850
- project_ref.update(update_data)
851
- db_update_time = time.time() - db_update_start
852
- logger.info(f"[PROJECT APPROVAL] Database update completed in {db_update_time:.3f}s")
853
-
854
- # Final project fetch timing
855
- final_fetch_start = time.time()
856
- updated_project = project_ref.get()
857
- updated_project["projectId"] = project_id
858
- final_fetch_time = time.time() - final_fetch_start
859
- logger.info(f"[PROJECT APPROVAL] Final project fetch completed in {final_fetch_time:.3f}s")
860
-
861
- # Credits deduction timing
862
- credits_update_start = time.time()
863
- user_ref.update({'credits': user_data.get('credits', 0) - 5})
864
- credits_update_time = time.time() - credits_update_start
865
- logger.info(f"[PROJECT APPROVAL] Credits update completed in {credits_update_time:.3f}s")
866
-
867
- # Total time calculation
868
- total_time = time.time() - start_time
869
- logger.info(f"[PROJECT APPROVAL] SUCCESS: Project approval completed in {total_time:.3f}s")
870
- logger.info(f"[PROJECT APPROVAL] TIMING BREAKDOWN:")
871
- logger.info(f"[PROJECT APPROVAL] - Authorization: {auth_time:.3f}s")
872
- logger.info(f"[PROJECT APPROVAL] - User fetch: {user_fetch_time:.3f}s")
873
- logger.info(f"[PROJECT APPROVAL] - Project fetch: {project_fetch_time:.3f}s")
874
- logger.info(f"[PROJECT APPROVAL] - Image download: {image_download_time:.3f}s")
875
- logger.info(f"[PROJECT APPROVAL] - Image processing: {image_processing_time:.3f}s")
876
- logger.info(f"[PROJECT APPROVAL] - Context prep: {context_time:.3f}s")
877
- logger.info(f"[PROJECT APPROVAL] - AI generation: {ai_time:.3f}s")
878
- logger.info(f"[PROJECT APPROVAL] - Response parsing: {parsing_time:.3f}s")
879
- logger.info(f"[PROJECT APPROVAL] - Text extraction: {extraction_time:.3f}s")
880
- logger.info(f"[PROJECT APPROVAL] - Step processing: {step_processing_time:.3f}s")
881
- logger.info(f"[PROJECT APPROVAL] - Total uploads: {total_upload_time:.3f}s")
882
- logger.info(f"[PROJECT APPROVAL] - Total TTS: {total_tts_time:.3f}s")
883
- logger.info(f"[PROJECT APPROVAL] - DB update: {db_update_time:.3f}s")
884
- logger.info(f"[PROJECT APPROVAL] - Final fetch: {final_fetch_time:.3f}s")
885
- logger.info(f"[PROJECT APPROVAL] - Credits update: {credits_update_time:.3f}s")
886
-
887
- return jsonify(updated_project)
888
-
889
- except Exception as e:
890
- total_time = time.time() - start_time
891
- logger.error(f"[PROJECT APPROVAL] ERROR: Exception occurred after {total_time:.3f}s: {e}")
892
- logger.error(f"[PROJECT APPROVAL] Error type: {type(e).__name__}")
893
- logger.error(f"[PROJECT APPROVAL] Project ID: {project_id}, User ID: {uid}")
894
- import traceback
895
- logger.error(f"[PROJECT APPROVAL] Full traceback: {traceback.format_exc()}")
896
- return jsonify({'error': str(e)}), 500
897
-
898
- @app.route('/api/projects', methods=['GET'])
899
- def list_projects():
900
- uid = verify_token(request.headers.get('Authorization'))
901
- if not uid: return jsonify({'error': 'Unauthorized'}), 401
902
- projects = (db_ref.child('projects').order_by_child('uid').equal_to(uid).get() or {}).values()
903
- return jsonify(list(projects))
904
-
905
- @app.route('/api/projects/<string:project_id>', methods=['GET'])
906
- def get_project(project_id):
907
- uid = verify_token(request.headers.get('Authorization'))
908
- if not uid: return jsonify({'error': 'Unauthorized'}), 401
909
- project_data = db_ref.child(f'projects/{project_id}').get()
910
- if not project_data or project_data.get('uid') != uid:
911
- return jsonify({'error': 'Project not found or access denied'}), 404
912
- return jsonify(project_data)
913
-
914
- @app.route('/api/projects/<string:project_id>/step/<int:step_number>', methods=['PUT'])
915
- def update_step(project_id, step_number):
916
- uid = verify_token(request.headers.get('Authorization'))
917
- if not uid: return jsonify({'error': 'Unauthorized'}), 401
918
- data = request.get_json()
919
- if data is None: return jsonify({'error': 'JSON body is required'}), 400
920
-
921
- project_data = db_ref.child(f'projects/{project_id}').get()
922
- if not project_data or project_data.get('uid') != uid:
923
- return jsonify({'error': 'Project not found or access denied'}), 404
924
-
925
- steps = project_data.get('steps', [])
926
- step_index = next((i for i, s in enumerate(steps) if s.get('stepNumber') == step_number), -1)
927
- if step_index == -1: return jsonify({'error': f'Step number {step_number} not found'}), 404
928
-
929
- step_path = f'projects/{project_id}/steps/{step_index}'
930
- if 'isDone' in data: db_ref.child(f'{step_path}/isDone').set(bool(data['isDone']))
931
- if 'notes' in data: db_ref.child(f'{step_path}/notes').set(str(data['notes']))
932
-
933
- return jsonify({"success": True, "updatedStep": db_ref.child(step_path).get()})
934
-
935
- @app.route('/api/projects/<string:project_id>', methods=['DELETE'])
936
- def delete_project(project_id):
937
- uid = verify_token(request.headers.get('Authorization'))
938
- if not uid: return jsonify({'error': 'Unauthorized'}), 401
939
-
940
- project_ref = db_ref.child(f'projects/{project_id}')
941
- project_data = project_ref.get()
942
- if not project_data or project_data.get('uid') != uid:
943
- return jsonify({'error': 'Project not found or access denied'}), 404
944
-
945
- project_ref.delete()
946
- for blob in bucket.list_blobs(prefix=f"users/{uid}/projects/{project_id}/"):
947
- blob.delete()
948
- return jsonify({"success": True, "message": f"Project {project_id} deleted."})
949
-
950
-
951
- #------------------------
952
- # AI phone call ElevenLabs
953
- #-------------------------
954
- import math
955
-
956
-
957
-
958
- # Fixed server code
959
-
960
- AGENT_ID = os.getenv("AGENT_ID", "agent_01jy2d4krmfkn9r7v7wdyqtjct")
961
- ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
962
-
963
- @app.route('/api/projects/<project_id>/initiate-call', methods=['POST'])
964
- def initiate_call(project_id):
965
  """
966
- This is the definitive, correct version. It uses the official 'get-signed-url'
967
- endpoint, which is the only one guaranteed to work for authenticated agents.
968
  """
969
- logger.info(f"[INITIATE] Received request for project: {project_id}")
970
-
971
- uid = verify_token(request.headers.get('Authorization'))
972
- if not uid:
973
- return jsonify({'error': 'Unauthorized'}), 401
974
-
975
- if not ELEVENLABS_API_KEY:
976
- logger.error("[INITIATE] ELEVENLABS_API_KEY is not set on the server.")
977
- return jsonify({'error': 'Server configuration error.'}), 500
978
-
979
- # This is the correct URL as per the official ElevenLabs documentation and our debug results.
980
- url = f"https://api.elevenlabs.io/v1/convai/conversation/get-signed-url?agent_id={AGENT_ID}"
981
- headers = {"xi-api-key": ELEVENLABS_API_KEY}
982
-
983
  try:
984
- response = requests.get(url, headers=headers, timeout=15)
985
- response.raise_for_status() # This will raise an error for 4xx/5xx responses
986
-
987
- data = response.json()
988
- signed_url = data.get("signed_url")
989
-
990
- if not signed_url:
991
- logger.error("[INITIATE] ElevenLabs response missing 'signed_url'.")
992
- return jsonify({'error': 'Failed to retrieve session URL from provider.'}), 502
993
-
994
- logger.info("[INITIATE] Successfully retrieved signed URL.")
995
- # The React SDK expects a JSON object with the key "signed_url".
996
- return jsonify({"signed_url": signed_url}), 200
997
 
998
- except requests.exceptions.RequestException as e:
999
- logger.error(f"[INITIATE] Error calling ElevenLabs API: {e}")
1000
- return jsonify({'error': 'Could not connect to AI service provider.'}), 504
1001
 
1002
- @app.route('/api/debug/test-agent', methods=['GET'])
1003
- def test_agent():
1004
- """
1005
- Fixed debug endpoint that tests the CORRECT conversation endpoint.
1006
- """
1007
- if not ELEVENLABS_API_KEY:
1008
- return jsonify({'error': 'API key not set on server'}), 500
1009
-
1010
- headers = {"xi-api-key": ELEVENLABS_API_KEY}
1011
- results = {'agent_id': AGENT_ID, 'tests': {}}
1012
-
1013
- try:
1014
- # Test 1: Check if the agent can be found by its ID.
1015
- agent_url = f"https://api.elevenlabs.io/v1/convai/agents/{AGENT_ID}"
1016
- agent_resp = requests.get(agent_url, headers=headers, timeout=10)
1017
- results['tests']['agent_check'] = {
1018
- 'status': agent_resp.status_code,
1019
- 'exists': agent_resp.ok
1020
- }
1021
-
1022
- # Test 2: Check if we can get a signed URL for this agent. This is the most important test.
1023
- conv_url = f"https://api.elevenlabs.io/v1/convai/conversation/get-signed-url?agent_id={AGENT_ID}"
1024
- conv_resp = requests.get(conv_url, headers=headers, timeout=10)
1025
- results['tests']['get_signed_url_check'] = {
1026
- 'status': conv_resp.status_code,
1027
- 'url_received': 'signed_url' in conv_resp.json() if conv_resp.ok else False
1028
- }
1029
-
1030
- return jsonify(results)
1031
 
1032
- except Exception as e:
1033
- return jsonify({'error': str(e), 'agent_id': AGENT_ID})
1034
 
1035
- @app.route('/api/projects/<project_id>/log-call-usage', methods=['POST'])
1036
- def log_call_usage(project_id):
1037
- """
1038
- Calculates and deducts credits from a user's account in Firebase
1039
- after a call is completed.
1040
- """
1041
- logger.info(f"[LOGGING] Received usage log for project: {project_id}")
1042
-
1043
- uid = verify_token(request.headers.get('Authorization'))
1044
- if not uid:
1045
- return jsonify({'error': 'Unauthorized'}), 401
1046
-
1047
- data = request.get_json()
1048
- duration_seconds = data.get("durationSeconds")
1049
-
1050
- if duration_seconds is None or not isinstance(duration_seconds, (int, float)):
1051
- return jsonify({'error': 'Invalid duration provided.'}), 400
1052
-
1053
- # Calculate credit cost (3 credits per minute, always rounded up)
1054
- minutes = math.ceil(duration_seconds / 60)
1055
- cost = minutes * 3
1056
-
1057
- logger.info(f"[LOGGING] User '{uid}' call duration: {duration_seconds:.2f}s, rounded to {minutes} minute(s). Cost: {cost} credits.")
1058
-
1059
- try:
1060
- user_ref = db_ref.child(f'users/{uid}')
1061
- user_data = user_ref.get()
1062
-
1063
- if user_data is None:
1064
- logger.error(f"[LOGGING] User with UID '{uid}' not found in the database.")
1065
- return jsonify({'error': 'User not found.'}), 404
1066
-
1067
- current_credits = user_data.get('credits', 0)
1068
-
1069
- new_credits = max(0, current_credits - cost)
1070
 
1071
- user_ref.update({'credits': new_credits})
1072
-
1073
- logger.info(f"[LOGGING] Successfully updated credits for user '{uid}'. Old: {current_credits}, New: {new_credits}")
 
 
 
 
 
 
1074
 
1075
- return jsonify({
1076
- "status": "success",
1077
- "creditsDeducted": cost,
1078
- "remainingCredits": new_credits
1079
- }), 200
1080
 
1081
  except Exception as e:
1082
- logger.error(f"[LOGGING] A database error occurred for user '{uid}': {e}")
1083
- return jsonify({'error': 'A server error occurred while updating credits.'}), 500
 
1084
 
1085
 
1086
  # -----------------------------------------------------------------------------
1087
  # 7. MAIN EXECUTION
1088
  # -----------------------------------------------------------------------------
1089
  if __name__ == '__main__':
 
1090
  app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
 
17
  import requests
18
  from elevenlabs import ElevenLabs
19
  # Import and configure Google GenAI, matching the Streamlit app
20
+ # from google import genai # Assuming you might use this later
21
+ # from google.genai import types
22
 
23
  # -----------------------------------------------------------------------------
24
  # 1. CONFIGURATION & INITIALIZATION
 
30
 
31
  # --- Firebase Initialization ---
32
  try:
33
+ # Best practice: Load credentials from environment variables
34
  credentials_json_string = os.environ.get("FIREBASE")
35
  if not credentials_json_string:
36
  raise ValueError("The FIREBASE environment variable is not set.")
 
50
  print("Firebase Admin SDK initialized successfully.")
51
  except Exception as e:
52
  print(f"FATAL: Error initializing Firebase: {e}")
53
+ # In a real app, you might want to prevent the app from starting if Firebase fails
54
+ # exit(1)
55
 
56
  # Initialize Firebase services
57
  bucket = storage.bucket()
58
  db_ref = db.reference()
59
 
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  # -----------------------------------------------------------------------------
62
+ # 2. HELPER FUNCTIONS
63
  # -----------------------------------------------------------------------------
64
 
65
+ def is_valid_email(email):
66
+ """Simple regex for basic email validation."""
67
+ regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
68
+ return re.match(regex, email) is not None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  # -----------------------------------------------------------------------------
72
  # 3. AUTHENTICATION & USER MANAGEMENT
73
  # -----------------------------------------------------------------------------
74
+ # (Your other authentication endpoints would go here)
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  # -----------------------------------------------------------------------------
78
+ # 4. WAITLIST ENDPOINT
79
  # -----------------------------------------------------------------------------
80
 
81
+ @app.route('/join-waitlist', methods=['POST'])
82
+ def join_waitlist():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  """
84
+ Endpoint to add a user's email to the waitlist.
85
+ Expects a JSON payload: {"email": "user@example.com"}
86
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  try:
88
+ # 1. Get and Validate Input
89
+ data = request.get_json()
90
+ if not data:
91
+ return jsonify({"status": "error", "message": "Invalid request. JSON payload expected."}), 400
 
 
 
 
 
 
 
 
 
92
 
93
+ email = data.get('email')
94
+ if not email:
95
+ return jsonify({"status": "error", "message": "Email is required."}), 400
96
 
97
+ if not is_valid_email(email):
98
+ return jsonify({"status": "error", "message": "Invalid email format."}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ email = email.lower() # Standardize email to lowercase
 
101
 
102
+ # 2. Check for Duplicates
103
+ waitlist_ref = db_ref.child('sozo_waitlist')
104
+ # Query Firebase to see if an entry with this email already exists
105
+ existing_user = waitlist_ref.order_by_child('email').equal_to(email).get()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
+ if existing_user:
108
+ return jsonify({"status": "success", "message": "You are already on the waitlist!"}), 200
109
+
110
+ # 3. Add to Firebase Realtime Database
111
+ new_entry_ref = waitlist_ref.push() # push() creates a unique key
112
+ new_entry_ref.set({
113
+ 'email': email,
114
+ 'timestamp': datetime.utcnow().isoformat() + 'Z' # ISO 8601 format
115
+ })
116
 
117
+ return jsonify({"status": "success", "message": "Thank you for joining the waitlist!"}), 201
 
 
 
 
118
 
119
  except Exception as e:
120
+ print(f"ERROR in /join-waitlist: {e}")
121
+ traceback.print_exc()
122
+ return jsonify({"status": "error", "message": "An internal server error occurred."}), 500
123
 
124
 
125
  # -----------------------------------------------------------------------------
126
  # 7. MAIN EXECUTION
127
  # -----------------------------------------------------------------------------
128
  if __name__ == '__main__':
129
+ # Use Gunicorn or another production-ready server instead of app.run in production
130
  app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))