rairo commited on
Commit
f3b65dc
·
verified ·
1 Parent(s): f3f35a4

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +316 -288
main.py CHANGED
@@ -1,26 +1,25 @@
1
  # app.py
2
-
3
  import os
4
  import io
5
  import uuid
6
  import re
7
  import json
8
  import traceback
9
- from datetime import datetime, timedelta
10
 
11
  from flask import Flask, request, jsonify
12
  from flask_cors import CORS
13
  import firebase_admin
14
  from firebase_admin import credentials, db, storage, auth
15
- import pandas as pd
16
  from pathlib import Path
17
 
18
- # Import the Sozo business logic
19
  from sozo_gen import (
20
- generate_report_draft,
21
- generate_single_chart,
22
  generate_video_from_project,
23
- load_dataframe_safely
 
24
  )
25
 
26
  # -----------------------------------------------------------------------------
@@ -115,416 +114,445 @@ def google_signin():
115
  return jsonify({'success': True, 'user': {'uid': uid, **user_data}}), 200
116
  except Exception as e: return jsonify({'error': str(e)}), 400
117
 
118
- import logging
119
-
120
- # Configure console logging
121
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
122
- logger = logging.getLogger(__name__)
123
-
124
- # -----------------------------------------------------------------------------
125
- # 4. SOZO BUSINESS STUDIO API ENDPOINTS WITH LOGGING
126
- # -----------------------------------------------------------------------------
127
-
128
- import logging
129
-
130
- # Configure console logging
131
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
132
- logger = logging.getLogger(__name__)
133
 
134
  # -----------------------------------------------------------------------------
135
- # 4. SOZO BUSINESS STUDIO API ENDPOINTS WITH LOGGING
136
  # -----------------------------------------------------------------------------
137
 
138
  @app.route('/api/sozo/projects', methods=['POST'])
139
  def create_sozo_project():
140
- logger.info("POST /api/sozo/projects - Creating new project")
141
-
142
  try:
143
- token = request.headers.get('Authorization', '').split(' ')[1]
 
 
 
 
 
144
  uid = verify_token(token)
145
  if not uid:
146
- logger.warning("Unauthorized access attempt to create project")
147
  return jsonify({'error': 'Unauthorized'}), 401
148
-
149
- logger.info(f"User {uid} creating project")
150
-
151
  if 'file' not in request.files:
152
- logger.warning(f"User {uid} - No file provided")
153
  return jsonify({'error': 'No file part'}), 400
154
 
155
  file = request.files['file']
156
- if file.filename == '':
157
- logger.warning(f"User {uid} - Empty filename")
158
- return jsonify({'error': 'No selected file'}), 400
159
-
160
- logger.info(f"User {uid} - Processing file: {file.filename}")
161
-
162
  context = request.form.get('context', '')
163
  project_id = uuid.uuid4().hex
164
-
165
- logger.info(f"Generated project ID: {project_id}")
166
 
167
  file_bytes = file.read()
168
- file.seek(0)
169
  ext = Path(file.filename).suffix
170
 
171
- logger.info(f"File size: {len(file_bytes)} bytes, extension: {ext}")
172
-
173
  blob_name = f"sozo_projects/{uid}/{project_id}/data{ext}"
 
174
  blob = bucket.blob(blob_name)
175
  blob.upload_from_string(file_bytes, content_type=file.content_type)
176
-
177
- logger.info(f"File uploaded to storage: {blob_name}")
178
 
179
  project_ref = db.reference(f'sozo_projects/{project_id}')
180
  project_data = {
181
  'uid': uid,
 
182
  'status': 'uploaded',
183
  'createdAt': datetime.utcnow().isoformat(),
 
184
  'userContext': context,
185
  'originalDataUrl': blob.public_url,
186
  'originalFilename': file.filename
187
  }
 
188
  project_ref.set(project_data)
189
 
190
- logger.info(f"Project data saved to database: {project_id}")
191
-
192
  df = load_dataframe_safely(io.BytesIO(file_bytes), file.filename)
193
  preview_json = df.head().to_json(orient='records')
194
 
195
- logger.info(f"Project {project_id} created successfully for user {uid}")
196
-
197
  return jsonify({
198
  'success': True,
199
- 'project_id': project_id,
200
  'preview': json.loads(preview_json)
201
  }), 201
202
 
203
  except Exception as e:
204
- logger.error(f"Error creating project: {str(e)}")
205
- logger.error(f"Traceback: {traceback.format_exc()}")
206
  return jsonify({'error': str(e)}), 500
207
 
208
- @app.route('/api/sozo/projects/<string:project_id>/generate-report', methods=['POST'])
209
- def generate_sozo_report(project_id):
210
- logger.info(f"POST /api/sozo/projects/{project_id}/generate-report - Generating report")
211
-
212
  try:
213
- token = request.headers.get('Authorization', '').split(' ')[1]
 
 
 
 
 
214
  uid = verify_token(token)
215
  if not uid:
216
- logger.warning(f"Unauthorized access attempt to generate report for project {project_id}")
217
  return jsonify({'error': 'Unauthorized'}), 401
 
 
 
 
218
 
219
- logger.info(f"User {uid} generating report for project {project_id}")
220
-
221
- project_ref = db.reference(f'sozo_projects/{project_id}')
222
- project_data = project_ref.get()
223
-
224
- if not project_data or project_data.get('uid') != uid:
225
- logger.warning(f"Project {project_id} not found or unauthorized for user {uid}")
226
- return jsonify({'error': 'Project not found or unauthorized'}), 404
227
-
228
- logger.info(f"Project {project_id} validated for user {uid}")
229
-
230
- blob_path = "/".join(project_data['originalDataUrl'].split('/')[4:])
231
- blob = bucket.blob(blob_path)
232
- file_bytes = blob.download_as_bytes()
233
-
234
- logger.info(f"Downloaded file data for project {project_id}, size: {len(file_bytes)} bytes")
235
-
236
- draft_data = generate_report_draft(
237
- io.BytesIO(file_bytes),
238
- project_data['originalFilename'],
239
- project_data['userContext'],
240
- uid,
241
- project_id,
242
- bucket
243
- )
244
-
245
- logger.info(f"Report draft generated for project {project_id}")
246
-
247
- # Clean and validate data before updating Firebase
248
- try:
249
- # Ensure rawMarkdown is a string and handle any encoding issues
250
- raw_markdown = draft_data.get('raw_md', '')
251
- if raw_markdown:
252
- # Clean any problematic characters that might cause JSON parsing issues
253
- raw_markdown = str(raw_markdown).replace('\x00', '').replace('\ufeff', '')
254
- logger.info(f"Raw markdown length: {len(raw_markdown)}")
255
-
256
- # Ensure chartUrls is a proper list/dict
257
- chart_urls = draft_data.get('chartUrls', [])
258
- logger.info(f"Chart URLs type: {type(chart_urls)}, count: {len(chart_urls)}")
259
- logger.info(f"Chart URLs content: {chart_urls}")
260
-
261
- # Convert chart_urls to a simple serializable format
262
- if isinstance(chart_urls, dict):
263
- # Convert dict to list of values or keep as dict but ensure all values are serializable
264
- serializable_chart_urls = {}
265
- for key, value in chart_urls.items():
266
- # Ensure key is string and value is serializable
267
- clean_key = str(key).replace('\x00', '').replace('\ufeff', '')
268
- clean_value = str(value).replace('\x00', '').replace('\ufeff', '') if value else ''
269
- serializable_chart_urls[clean_key] = clean_value
270
- chart_urls = serializable_chart_urls
271
- elif isinstance(chart_urls, list):
272
- # Clean list items
273
- chart_urls = [str(item).replace('\x00', '').replace('\ufeff', '') for item in chart_urls if item]
274
- else:
275
- logger.warning(f"Chart URLs is not a list/dict: {type(chart_urls)}")
276
- chart_urls = []
277
-
278
- # Try to serialize the data to JSON first to catch any issues
279
- import json
280
- test_data = {
281
- 'status': 'draft',
282
- 'rawMarkdown': raw_markdown,
283
- 'chartUrls': chart_urls
284
- }
285
-
286
- # Test JSON serialization
287
- json_str = json.dumps(test_data, ensure_ascii=False)
288
- logger.info(f"JSON serialization test passed, length: {len(json_str)}")
289
-
290
- logger.info(f"Updating project {project_id} with cleaned data")
291
- project_ref.update(test_data)
292
-
293
- except json.JSONEncodeError as json_error:
294
- logger.error(f"JSON serialization error for project {project_id}: {str(json_error)}")
295
- logger.error(f"Problematic data - Raw markdown preview: {raw_markdown[:100]}...")
296
- logger.error(f"Problematic data - Chart URLs: {chart_urls}")
297
- raise json_error
298
- except Exception as update_error:
299
- logger.error(f"Error preparing update data for project {project_id}: {str(update_error)}")
300
- logger.error(f"Draft data keys: {list(draft_data.keys()) if draft_data else 'None'}")
301
- logger.error(f"Raw markdown type: {type(draft_data.get('raw_md'))}")
302
- logger.error(f"Chart URLs type: {type(draft_data.get('chartUrls'))}")
303
- raise update_error
304
-
305
- logger.info(f"Project {project_id} updated with draft data")
306
-
307
- return jsonify({
308
- 'success': True,
309
- 'project': {**project_data, **update_data}
310
- }), 200
311
-
312
  except Exception as e:
313
- logger.error(f"Error generating report for project {project_id}: {str(e)}")
314
- logger.error(f"Traceback: {traceback.format_exc()}")
315
- db.reference(f'sozo_projects/{project_id}').update({
316
- 'status': 'failed',
317
- 'error': str(e)
318
- })
319
  return jsonify({'error': str(e)}), 500
320
 
321
- @app.route('/api/sozo/projects', methods=['GET'])
322
- def get_sozo_projects():
323
- logger.info("GET /api/sozo/projects - Fetching user projects")
324
-
325
  try:
326
- token = request.headers.get('Authorization', '').split(' ')[1]
 
 
 
 
 
327
  uid = verify_token(token)
328
  if not uid:
329
- logger.warning("Unauthorized access attempt to fetch projects")
330
  return jsonify({'error': 'Unauthorized'}), 401
331
-
332
- logger.info(f"User {uid} fetching projects")
333
-
334
- all_projects = db.reference('sozo_projects').order_by_child('uid').equal_to(uid).get()
335
-
336
- project_count = len(all_projects) if all_projects else 0
337
- logger.info(f"Found {project_count} projects for user {uid}")
338
-
339
- return jsonify(all_projects or {}), 200
340
-
 
 
 
 
 
 
341
  except Exception as e:
342
- logger.error(f"Error fetching projects: {str(e)}")
343
  return jsonify({'error': str(e)}), 500
344
 
345
- @app.route('/api/sozo/projects/<string:project_id>', methods=['GET'])
346
- def get_sozo_project(project_id):
347
- logger.info(f"GET /api/sozo/projects/{project_id} - Fetching project")
348
-
349
  try:
350
- token = request.headers.get('Authorization', '').split(' ')[1]
 
 
 
 
 
351
  uid = verify_token(token)
352
  if not uid:
353
- logger.warning(f"Unauthorized access attempt to fetch project {project_id}")
354
  return jsonify({'error': 'Unauthorized'}), 401
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
- logger.info(f"User {uid} fetching project {project_id}")
357
-
358
- project_data = db.reference(f'sozo_projects/{project_id}').get()
359
 
360
- if not project_data or project_data.get('uid') != uid:
361
- logger.warning(f"Project {project_id} not found or unauthorized for user {uid}")
362
- return jsonify({'error': 'Project not found or unauthorized'}), 404
 
 
 
 
363
 
364
- logger.info(f"Project {project_id} fetched successfully for user {uid}")
 
 
 
 
365
 
366
- return jsonify(project_data), 200
 
367
 
 
 
 
 
368
  except Exception as e:
369
- logger.error(f"Error fetching project {project_id}: {str(e)}")
370
  return jsonify({'error': str(e)}), 500
371
 
372
  @app.route('/api/sozo/projects/<string:project_id>', methods=['DELETE'])
373
  def delete_sozo_project(project_id):
374
- logger.info(f"DELETE /api/sozo/projects/{project_id} - Deleting project")
375
-
376
  try:
377
- token = request.headers.get('Authorization', '').split(' ')[1]
 
 
 
 
 
378
  uid = verify_token(token)
379
  if not uid:
380
- logger.warning(f"Unauthorized access attempt to delete project {project_id}")
381
  return jsonify({'error': 'Unauthorized'}), 401
382
-
383
- logger.info(f"User {uid} attempting to delete project {project_id}")
384
-
385
- # Get project data first to verify ownership and get file paths
386
  project_ref = db.reference(f'sozo_projects/{project_id}')
387
  project_data = project_ref.get()
388
-
389
  if not project_data:
390
- logger.warning(f"Project {project_id} not found for deletion")
391
  return jsonify({'error': 'Project not found'}), 404
392
-
393
  if project_data.get('uid') != uid:
394
- logger.warning(f"User {uid} unauthorized to delete project {project_id}")
395
  return jsonify({'error': 'Unauthorized to delete this project'}), 403
396
 
397
- logger.info(f"Project {project_id} verified for deletion by user {uid}")
398
-
399
- # Delete files from storage
400
- deleted_files = []
401
- try:
402
- # Delete original data file
403
- if 'originalDataUrl' in project_data:
404
- blob_path = "/".join(project_data['originalDataUrl'].split('/')[4:])
405
- blob = bucket.blob(blob_path)
406
- if blob.exists():
407
- blob.delete()
408
- deleted_files.append(blob_path)
409
- logger.info(f"Deleted original data file: {blob_path}")
410
-
411
- # Delete chart files if they exist
412
- chart_urls = project_data.get('chartUrls', {})
413
- if isinstance(chart_urls, dict):
414
- for chart_name, chart_url in chart_urls.items():
415
- try:
416
- chart_blob_path = "/".join(chart_url.split('/')[4:])
417
- chart_blob = bucket.blob(chart_blob_path)
418
- if chart_blob.exists():
419
- chart_blob.delete()
420
- deleted_files.append(chart_blob_path)
421
- logger.info(f"Deleted chart file: {chart_blob_path}")
422
- except Exception as chart_error:
423
- logger.warning(f"Error deleting chart {chart_name}: {str(chart_error)}")
424
-
425
- # Delete entire project folder if it exists
426
- project_folder = f"sozo_projects/{uid}/{project_id}/"
427
- blobs = bucket.list_blobs(prefix=project_folder)
428
- for blob in blobs:
429
- if blob.name not in deleted_files: # Avoid duplicate deletions
430
- blob.delete()
431
- deleted_files.append(blob.name)
432
- logger.info(f"Deleted additional file: {blob.name}")
433
-
434
- logger.info(f"Deleted {len(deleted_files)} files from storage for project {project_id}")
435
-
436
- except Exception as storage_error:
437
- logger.error(f"Error deleting storage files for project {project_id}: {str(storage_error)}")
438
- # Continue with database deletion even if storage cleanup fails
439
-
440
- # Delete project from database
441
  project_ref.delete()
442
- logger.info(f"Project {project_id} deleted from database")
443
-
444
- logger.info(f"Project {project_id} successfully deleted by user {uid}")
445
-
446
- return jsonify({
447
- 'success': True,
448
- 'message': f'Project {project_id} deleted successfully',
449
- 'deleted_files': len(deleted_files)
450
- }), 200
451
 
 
 
 
452
  except Exception as e:
453
- logger.error(f"Error deleting project {project_id}: {str(e)}")
454
- logger.error(f"Traceback: {traceback.format_exc()}")
455
  return jsonify({'error': str(e)}), 500
456
 
457
- @app.route('/api/sozo/projects/<string:project_id>/markdown', methods=['PUT'])
458
- def update_sozo_markdown(project_id):
 
459
  try:
460
  token = request.headers.get('Authorization', '').split(' ')[1]
461
  uid = verify_token(token)
462
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
 
 
463
  project_ref = db.reference(f'sozo_projects/{project_id}')
464
- if not project_ref.get() or project_ref.get().get('uid') != uid: return jsonify({'error': 'Project not found or unauthorized'}), 404
465
- data = request.get_json()
466
- if 'raw_md' not in data: return jsonify({'error': 'raw_md is required'}), 400
467
- project_ref.update({'rawMarkdown': data['raw_md']})
468
- return jsonify({'success': True}), 200
469
- except Exception as e: return jsonify({'error': str(e)}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
 
471
- @app.route('/api/sozo/projects/<string:project_id>/charts', methods=['POST'])
472
- def regenerate_sozo_chart(project_id):
 
473
  try:
474
  token = request.headers.get('Authorization', '').split(' ')[1]
475
  uid = verify_token(token)
476
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
 
 
477
  project_ref = db.reference(f'sozo_projects/{project_id}')
478
  project_data = project_ref.get()
479
- if not project_data or project_data.get('uid') != uid: return jsonify({'error': 'Project not found or unauthorized'}), 404
 
 
 
480
  data = request.get_json()
481
- description = data.get('description')
482
- if not description: return jsonify({'error': 'Chart description is required'}), 400
483
 
484
- blob_path = "/".join(project_data['originalDataUrl'].split('/')[4:])
 
 
 
 
485
  blob = bucket.blob(blob_path)
486
  file_bytes = blob.download_as_bytes()
487
  df = load_dataframe_safely(io.BytesIO(file_bytes), project_data['originalFilename'])
488
 
489
- new_chart_url = generate_single_chart(df, description, uid, project_id, bucket)
490
- if not new_chart_url: return jsonify({'error': 'Chart generation failed'}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
491
 
492
- project_ref.child('chartUrls').update({description: new_chart_url})
493
- return jsonify({'success': True, 'description': description, 'new_url': new_chart_url}), 200
 
 
494
  except Exception as e:
495
- traceback.print_exc()
 
496
  return jsonify({'error': str(e)}), 500
497
 
498
- @app.route('/api/sozo/projects/<string:project_id>/generate-video', methods=['POST'])
499
- def generate_sozo_video(project_id):
 
500
  try:
501
  token = request.headers.get('Authorization', '').split(' ')[1]
502
  uid = verify_token(token)
503
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
 
 
504
  project_ref = db.reference(f'sozo_projects/{project_id}')
505
  project_data = project_ref.get()
506
- if not project_data or project_data.get('uid') != uid: return jsonify({'error': 'Project not found or unauthorized'}), 404
507
-
 
 
508
  data = request.get_json()
509
- voice_model = data.get('voice_model', 'aura-2-andromeda-en')
510
- if voice_model not in ['aura-2-andromeda-en', 'aura-2-orpheus-en']: return jsonify({'error': 'Invalid voice model specified'}), 400
 
 
511
 
512
- project_ref.update({'status': 'generating_video'})
513
- blob_path = "/".join(project_data['originalDataUrl'].split('/')[4:])
514
  blob = bucket.blob(blob_path)
515
  file_bytes = blob.download_as_bytes()
516
  df = load_dataframe_safely(io.BytesIO(file_bytes), project_data['originalFilename'])
517
 
518
- video_url = generate_video_from_project(df, project_data.get('rawMarkdown', ''), uid, project_id, voice_model, bucket)
519
- if not video_url: raise Exception("Video generation failed in core function.")
 
 
 
 
520
 
521
- project_ref.update({'status': 'video_complete', 'videoUrl': video_url})
522
- return jsonify({'success': True, 'video_url': video_url}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  except Exception as e:
524
- db.reference(f'sozo_projects/{project_id}').update({'status': 'failed', 'error': str(e)})
525
- traceback.print_exc()
526
  return jsonify({'error': str(e)}), 500
527
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  # -----------------------------------------------------------------------------
529
  # 5. UNIVERSAL ENDPOINTS (Waitlist, Feedback, Credits)
530
  # -----------------------------------------------------------------------------
 
1
  # app.py
 
2
  import os
3
  import io
4
  import uuid
5
  import re
6
  import json
7
  import traceback
8
+ from datetime import datetime
9
 
10
  from flask import Flask, request, jsonify
11
  from flask_cors import CORS
12
  import firebase_admin
13
  from firebase_admin import credentials, db, storage, auth
 
14
  from pathlib import Path
15
 
16
+ # Import the refactored Sozo business logic
17
  from sozo_gen import (
18
+ generate_report_draft,
19
+ generate_single_chart,
20
  generate_video_from_project,
21
+ load_dataframe_safely,
22
+ deepgram_tts
23
  )
24
 
25
  # -----------------------------------------------------------------------------
 
114
  return jsonify({'success': True, 'user': {'uid': uid, **user_data}}), 200
115
  except Exception as e: return jsonify({'error': str(e)}), 400
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
  # -----------------------------------------------------------------------------
119
+ # 3. SOZO BUSINESS STUDIO API ENDPOINTS
120
  # -----------------------------------------------------------------------------
121
 
122
  @app.route('/api/sozo/projects', methods=['POST'])
123
  def create_sozo_project():
124
+ logger.info("Endpoint /api/sozo/projects POST: Received request to create new project.")
 
125
  try:
126
+ auth_header = request.headers.get('Authorization', '')
127
+ if not auth_header.startswith('Bearer '):
128
+ logger.warning("Create project failed: Missing or invalid auth header.")
129
+ return jsonify({'error': 'Missing or invalid token'}), 401
130
+
131
+ token = auth_header.split(' ')[1]
132
  uid = verify_token(token)
133
  if not uid:
134
+ logger.warning("Create project failed: Invalid token.")
135
  return jsonify({'error': 'Unauthorized'}), 401
136
+ logger.info(f"Token verified for user UID: {uid}")
137
+
 
138
  if 'file' not in request.files:
139
+ logger.warning(f"User {uid}: Create project failed: No file part in request.")
140
  return jsonify({'error': 'No file part'}), 400
141
 
142
  file = request.files['file']
 
 
 
 
 
 
143
  context = request.form.get('context', '')
144
  project_id = uuid.uuid4().hex
145
+ logger.info(f"User {uid}: Generated new project ID: {project_id}")
 
146
 
147
  file_bytes = file.read()
 
148
  ext = Path(file.filename).suffix
149
 
 
 
150
  blob_name = f"sozo_projects/{uid}/{project_id}/data{ext}"
151
+ logger.info(f"User {uid}: Uploading raw data to storage at {blob_name}")
152
  blob = bucket.blob(blob_name)
153
  blob.upload_from_string(file_bytes, content_type=file.content_type)
154
+ logger.info(f"User {uid}: Successfully uploaded raw data for project {project_id}")
 
155
 
156
  project_ref = db.reference(f'sozo_projects/{project_id}')
157
  project_data = {
158
  'uid': uid,
159
+ 'id': project_id,
160
  'status': 'uploaded',
161
  'createdAt': datetime.utcnow().isoformat(),
162
+ 'updatedAt': datetime.utcnow().isoformat(),
163
  'userContext': context,
164
  'originalDataUrl': blob.public_url,
165
  'originalFilename': file.filename
166
  }
167
+ logger.info(f"User {uid}: Saving project metadata to database for project {project_id}")
168
  project_ref.set(project_data)
169
 
 
 
170
  df = load_dataframe_safely(io.BytesIO(file_bytes), file.filename)
171
  preview_json = df.head().to_json(orient='records')
172
 
173
+ logger.info(f"User {uid}: Project {project_id} created successfully.")
 
174
  return jsonify({
175
  'success': True,
176
+ 'project': project_data,
177
  'preview': json.loads(preview_json)
178
  }), 201
179
 
180
  except Exception as e:
181
+ logger.error(f"CRITICAL ERROR during project creation: {traceback.format_exc()}")
 
182
  return jsonify({'error': str(e)}), 500
183
 
184
+ @app.route('/api/sozo/projects', methods=['GET'])
185
+ def get_sozo_projects():
186
+ logger.info("Endpoint /api/sozo/projects GET: Received request to list projects.")
 
187
  try:
188
+ auth_header = request.headers.get('Authorization', '')
189
+ if not auth_header.startswith('Bearer '):
190
+ logger.warning("List projects failed: Missing or invalid auth header.")
191
+ return jsonify({'error': 'Missing or invalid token'}), 401
192
+
193
+ token = auth_header.split(' ')[1]
194
  uid = verify_token(token)
195
  if not uid:
196
+ logger.warning("List projects failed: Invalid token.")
197
  return jsonify({'error': 'Unauthorized'}), 401
198
+ logger.info(f"Token verified for user UID: {uid}. Fetching projects.")
199
+
200
+ projects_ref = db.reference('sozo_projects')
201
+ user_projects = projects_ref.order_by_child('uid').equal_to(uid).get()
202
 
203
+ if not user_projects:
204
+ logger.info(f"User {uid}: No projects found.")
205
+ return jsonify([]), 200
206
+
207
+ # Firebase returns a dictionary, convert it to a list for the client
208
+ projects_list = [project for project in user_projects.values()]
209
+ logger.info(f"User {uid}: Found and returning {len(projects_list)} projects.")
210
+ return jsonify(projects_list), 200
211
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  except Exception as e:
213
+ logger.error(f"CRITICAL ERROR during project list: {traceback.format_exc()}")
 
 
 
 
 
214
  return jsonify({'error': str(e)}), 500
215
 
216
+ @app.route('/api/sozo/projects/<string:project_id>', methods=['GET'])
217
+ def get_sozo_project(project_id):
218
+ logger.info(f"Endpoint /api/sozo/projects/{project_id} GET: Received request for single project.")
 
219
  try:
220
+ auth_header = request.headers.get('Authorization', '')
221
+ if not auth_header.startswith('Bearer '):
222
+ logger.warning(f"Get project {project_id} failed: Missing or invalid auth header.")
223
+ return jsonify({'error': 'Missing or invalid token'}), 401
224
+
225
+ token = auth_header.split(' ')[1]
226
  uid = verify_token(token)
227
  if not uid:
228
+ logger.warning(f"Get project {project_id} failed: Invalid token.")
229
  return jsonify({'error': 'Unauthorized'}), 401
230
+ logger.info(f"Token verified for user UID: {uid}. Fetching project {project_id}.")
231
+
232
+ project_ref = db.reference(f'sozo_projects/{project_id}')
233
+ project_data = project_ref.get()
234
+
235
+ if not project_data:
236
+ logger.warning(f"User {uid}: Attempted to access non-existent project {project_id}.")
237
+ return jsonify({'error': 'Project not found'}), 404
238
+
239
+ if project_data.get('uid') != uid:
240
+ logger.error(f"User {uid}: UNAUTHORIZED attempt to access project {project_id} owned by {project_data.get('uid')}.")
241
+ return jsonify({'error': 'Unauthorized to access this project'}), 403
242
+
243
+ logger.info(f"User {uid}: Successfully fetched project {project_id}.")
244
+ return jsonify(project_data), 200
245
+
246
  except Exception as e:
247
+ logger.error(f"CRITICAL ERROR during get project {project_id}: {traceback.format_exc()}")
248
  return jsonify({'error': str(e)}), 500
249
 
250
+ @app.route('/api/sozo/projects/<string:project_id>', methods=['PUT'])
251
+ def update_sozo_project(project_id):
252
+ logger.info(f"Endpoint /api/sozo/projects/{project_id} PUT: Received request to update project.")
 
253
  try:
254
+ auth_header = request.headers.get('Authorization', '')
255
+ if not auth_header.startswith('Bearer '):
256
+ logger.warning(f"Update project {project_id} failed: Missing or invalid auth header.")
257
+ return jsonify({'error': 'Missing or invalid token'}), 401
258
+
259
+ token = auth_header.split(' ')[1]
260
  uid = verify_token(token)
261
  if not uid:
262
+ logger.warning(f"Update project {project_id} failed: Invalid token.")
263
  return jsonify({'error': 'Unauthorized'}), 401
264
+ logger.info(f"Token verified for user UID: {uid}. Updating project {project_id}.")
265
+
266
+ project_ref = db.reference(f'sozo_projects/{project_id}')
267
+ project_data = project_ref.get()
268
+
269
+ if not project_data:
270
+ logger.warning(f"User {uid}: Attempted to update non-existent project {project_id}.")
271
+ return jsonify({'error': 'Project not found'}), 404
272
+
273
+ if project_data.get('uid') != uid:
274
+ logger.error(f"User {uid}: UNAUTHORIZED attempt to update project {project_id} owned by {project_data.get('uid')}.")
275
+ return jsonify({'error': 'Unauthorized to update this project'}), 403
276
 
277
+ logger.info(f"User {uid}: Ownership of project {project_id} verified.")
 
 
278
 
279
+ update_data = request.get_json()
280
+ if not update_data:
281
+ return jsonify({'error': 'No update data provided'}), 400
282
+
283
+ # Define fields the user is allowed to update
284
+ allowed_updates = ['userContext', 'originalFilename']
285
+ final_updates = {key: update_data[key] for key in update_data if key in allowed_updates}
286
 
287
+ if not final_updates:
288
+ logger.warning(f"User {uid}: Update for project {project_id} contained no valid fields.")
289
+ return jsonify({'error': 'No valid fields to update'}), 400
290
+
291
+ final_updates['updatedAt'] = datetime.utcnow().isoformat()
292
 
293
+ logger.info(f"User {uid}: Applying updates to project {project_id}: {final_updates}")
294
+ project_ref.update(final_updates)
295
 
296
+ updated_project = project_ref.get()
297
+ logger.info(f"User {uid}: Successfully updated project {project_id}.")
298
+ return jsonify(updated_project), 200
299
+
300
  except Exception as e:
301
+ logger.error(f"CRITICAL ERROR during update project {project_id}: {traceback.format_exc()}")
302
  return jsonify({'error': str(e)}), 500
303
 
304
  @app.route('/api/sozo/projects/<string:project_id>', methods=['DELETE'])
305
  def delete_sozo_project(project_id):
306
+ logger.info(f"Endpoint /api/sozo/projects/{project_id} DELETE: Received request to delete project.")
 
307
  try:
308
+ auth_header = request.headers.get('Authorization', '')
309
+ if not auth_header.startswith('Bearer '):
310
+ logger.warning(f"Delete project {project_id} failed: Missing or invalid auth header.")
311
+ return jsonify({'error': 'Missing or invalid token'}), 401
312
+
313
+ token = auth_header.split(' ')[1]
314
  uid = verify_token(token)
315
  if not uid:
316
+ logger.warning(f"Delete project {project_id} failed: Invalid token.")
317
  return jsonify({'error': 'Unauthorized'}), 401
318
+ logger.info(f"Token verified for user UID: {uid}. Deleting project {project_id}.")
319
+
 
 
320
  project_ref = db.reference(f'sozo_projects/{project_id}')
321
  project_data = project_ref.get()
322
+
323
  if not project_data:
324
+ logger.warning(f"User {uid}: Attempted to delete non-existent project {project_id}.")
325
  return jsonify({'error': 'Project not found'}), 404
326
+
327
  if project_data.get('uid') != uid:
328
+ logger.error(f"User {uid}: UNAUTHORIZED attempt to delete project {project_id} owned by {project_data.get('uid')}.")
329
  return jsonify({'error': 'Unauthorized to delete this project'}), 403
330
 
331
+ logger.info(f"User {uid}: Ownership of project {project_id} verified. Proceeding with deletion.")
332
+
333
+ # Delete all associated files from Firebase Storage
334
+ project_folder_prefix = f"sozo_projects/{uid}/{project_id}/"
335
+ logger.info(f"User {uid}: Deleting all files from storage folder: {project_folder_prefix}")
336
+ blobs_to_delete = bucket.list_blobs(prefix=project_folder_prefix)
337
+ deleted_files_count = 0
338
+ for blob in blobs_to_delete:
339
+ logger.info(f"User {uid}: Deleting file {blob.name} from storage.")
340
+ blob.delete()
341
+ deleted_files_count += 1
342
+ logger.info(f"User {uid}: Deleted {deleted_files_count} files from storage for project {project_id}.")
343
+
344
+ # Delete project from Realtime Database
345
+ logger.info(f"User {uid}: Deleting project {project_id} from database.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  project_ref.delete()
 
 
 
 
 
 
 
 
 
347
 
348
+ logger.info(f"User {uid}: Successfully deleted project {project_id}.")
349
+ return jsonify({'success': True, 'message': f'Project {project_id} and all associated files deleted.'}), 200
350
+
351
  except Exception as e:
352
+ logger.error(f"CRITICAL ERROR during delete project {project_id}: {traceback.format_exc()}")
 
353
  return jsonify({'error': str(e)}), 500
354
 
355
+ @app.route('/api/sozo/projects/<string:project_id>/generate-report', methods=['POST'])
356
+ def generate_sozo_report(project_id):
357
+ logger.info(f"Endpoint /generate-report POST for project {project_id}")
358
  try:
359
  token = request.headers.get('Authorization', '').split(' ')[1]
360
  uid = verify_token(token)
361
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
362
+ logger.info(f"Token verified for user {uid} for report generation.")
363
+
364
  project_ref = db.reference(f'sozo_projects/{project_id}')
365
+ project_data = project_ref.get()
366
+ if not project_data or project_data.get('uid') != uid:
367
+ logger.warning(f"User {uid} failed to generate report: Project {project_id} not found or not owned.")
368
+ return jsonify({'error': 'Project not found or unauthorized'}), 404
369
+
370
+ logger.info(f"User {uid}: Ownership verified for project {project_id}. Starting report generation.")
371
+ project_ref.update({'status': 'generating_report', 'updatedAt': datetime.utcnow().isoformat()})
372
+
373
+ blob_path = f"sozo_projects/{uid}/{project_id}/data{Path(project_data['originalFilename']).suffix}"
374
+ logger.info(f"User {uid}: Downloading data from {blob_path}")
375
+ blob = bucket.blob(blob_path)
376
+ file_bytes = blob.download_as_bytes()
377
+ df = load_dataframe_safely(io.BytesIO(file_bytes), project_data['originalFilename'])
378
+
379
+ logger.info(f"User {uid}: Calling core logic to generate report draft for project {project_id}.")
380
+ draft_data = generate_report_draft(df, project_data['userContext'])
381
+
382
+ logger.info(f"User {uid}: Saving report content specification to database for project {project_id}.")
383
+ project_ref.child('report_content').set(draft_data)
384
+
385
+ project_ref.update({'status': 'draft_complete', 'updatedAt': datetime.utcnow().isoformat()})
386
+
387
+ full_project_data = project_ref.get()
388
+ logger.info(f"User {uid}: Report generation for project {project_id} complete.")
389
+ return jsonify({'success': True, 'project': full_project_data}), 200
390
+
391
+ except Exception as e:
392
+ db.reference(f'sozo_projects/{project_id}').update({'status': 'failed', 'error': str(e), 'updatedAt': datetime.utcnow().isoformat()})
393
+ logger.error(f"CRITICAL ERROR generating report for {project_id}: {traceback.format_exc()}")
394
+ return jsonify({'error': str(e)}), 500
395
 
396
+ @app.route('/api/sozo/projects/<string:project_id>/generate-video', methods=['POST'])
397
+ def generate_sozo_video(project_id):
398
+ logger.info(f"Endpoint /generate-video POST for project {project_id}")
399
  try:
400
  token = request.headers.get('Authorization', '').split(' ')[1]
401
  uid = verify_token(token)
402
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
403
+ logger.info(f"Token verified for user {uid} for video script generation.")
404
+
405
  project_ref = db.reference(f'sozo_projects/{project_id}')
406
  project_data = project_ref.get()
407
+ if not project_data or project_data.get('uid') != uid:
408
+ logger.warning(f"User {uid} failed to generate video script: Project {project_id} not found or not owned.")
409
+ return jsonify({'error': 'Project not found or unauthorized'}), 404
410
+
411
  data = request.get_json()
412
+ voice_model = data.get('voice_model', 'aura-2-andromeda-en')
 
413
 
414
+ logger.info(f"User {uid}: Ownership verified for project {project_id}. Starting video script generation with voice {voice_model}.")
415
+ project_ref.update({'status': 'generating_video', 'updatedAt': datetime.utcnow().isoformat()})
416
+
417
+ blob_path = f"sozo_projects/{uid}/{project_id}/data{Path(project_data['originalFilename']).suffix}"
418
+ logger.info(f"User {uid}: Downloading data from {blob_path}")
419
  blob = bucket.blob(blob_path)
420
  file_bytes = blob.download_as_bytes()
421
  df = load_dataframe_safely(io.BytesIO(file_bytes), project_data['originalFilename'])
422
 
423
+ logger.info(f"User {uid}: Calling core logic to generate video script for project {project_id}.")
424
+ video_script = generate_video_from_project(df, project_data.get('report_content', {}).get('raw_md', ''), voice_model)
425
+
426
+ logger.info(f"User {uid}: Uploading generated audio files to storage for project {project_id}.")
427
+ for scene in video_script.get("scenes", []):
428
+ if scene.get("audio_content"):
429
+ audio_bytes = scene.pop("audio_content")
430
+ audio_blob_name = f"sozo_projects/{uid}/{project_id}/audio/{scene['scene_id']}.mp3"
431
+ audio_blob = bucket.blob(audio_blob_name)
432
+ audio_blob.upload_from_string(audio_bytes, content_type="audio/mpeg")
433
+ scene["audio_storage_path"] = audio_blob.public_url
434
+
435
+ logger.info(f"User {uid}: Saving video script specification to database for project {project_id}.")
436
+ project_ref.child('video_script').set(video_script)
437
+ project_ref.update({'status': 'video_script_complete', 'updatedAt': datetime.utcnow().isoformat()})
438
 
439
+ full_project_data = project_ref.get()
440
+ logger.info(f"User {uid}: Video script generation for project {project_id} complete.")
441
+ return jsonify({'success': True, 'project': full_project_data}), 200
442
+
443
  except Exception as e:
444
+ db.reference(f'sozo_projects/{project_id}').update({'status': 'failed', 'error': str(e), 'updatedAt': datetime.utcnow().isoformat()})
445
+ logger.error(f"CRITICAL ERROR generating video for {project_id}: {traceback.format_exc()}")
446
  return jsonify({'error': str(e)}), 500
447
 
448
+ @app.route('/api/sozo/projects/<string:project_id>/charts', methods=['POST'])
449
+ def regenerate_sozo_chart(project_id):
450
+ logger.info(f"Endpoint /charts POST for project {project_id}")
451
  try:
452
  token = request.headers.get('Authorization', '').split(' ')[1]
453
  uid = verify_token(token)
454
  if not uid: return jsonify({'error': 'Unauthorized'}), 401
455
+ logger.info(f"Token verified for user {uid} for chart regeneration.")
456
+
457
  project_ref = db.reference(f'sozo_projects/{project_id}')
458
  project_data = project_ref.get()
459
+ if not project_data or project_data.get('uid') != uid:
460
+ logger.warning(f"User {uid} failed to regenerate chart: Project {project_id} not found or not owned.")
461
+ return jsonify({'error': 'Project not found or unauthorized'}), 404
462
+
463
  data = request.get_json()
464
+ description = data.get('description')
465
+ chart_id_to_replace = data.get('chart_id')
466
+ if not description or not chart_id_to_replace:
467
+ return jsonify({'error': 'Chart description and chart_id are required'}), 400
468
 
469
+ logger.info(f"User {uid}: Regenerating chart '{chart_id_to_replace}' for project {project_id} with new description: '{description}'")
470
+ blob_path = f"sozo_projects/{uid}/{project_id}/data{Path(project_data['originalFilename']).suffix}"
471
  blob = bucket.blob(blob_path)
472
  file_bytes = blob.download_as_bytes()
473
  df = load_dataframe_safely(io.BytesIO(file_bytes), project_data['originalFilename'])
474
 
475
+ new_chart_spec = generate_single_chart(df, description)
476
+
477
+ logger.info(f"User {uid}: Updating chart spec in database for project {project_id}.")
478
+ report_content_ref = project_ref.child('report_content')
479
+ report_content = report_content_ref.get()
480
+ chart_specs = report_content.get('chart_specs', [])
481
 
482
+ chart_found = False
483
+ for i, spec in enumerate(chart_specs):
484
+ if spec.get('id') == chart_id_to_replace:
485
+ chart_specs[i] = new_chart_spec
486
+ chart_found = True
487
+ break
488
+
489
+ if not chart_found:
490
+ logger.warning(f"User {uid}: Chart with id {chart_id_to_replace} not found in project {project_id}.")
491
+ return jsonify({'error': f'Chart with id {chart_id_to_replace} not found'}), 404
492
+
493
+ report_content_ref.child('chart_specs').set(chart_specs)
494
+ project_ref.update({'updatedAt': datetime.utcnow().isoformat()})
495
+
496
+ logger.info(f"User {uid}: Successfully regenerated chart {chart_id_to_replace} for project {project_id}.")
497
+ return jsonify({'success': True, 'new_chart_spec': new_chart_spec}), 200
498
  except Exception as e:
499
+ logger.error(f"CRITICAL ERROR regenerating chart for {project_id}: {traceback.format_exc()}")
 
500
  return jsonify({'error': str(e)}), 500
501
 
502
+ @app.route('/api/sozo/projects/<string:project_id>/update-narration-audio', methods=['POST'])
503
+ def update_narration_audio(project_id):
504
+ logger.info(f"Endpoint /update-narration-audio POST for project {project_id}")
505
+ try:
506
+ token = request.headers.get('Authorization', '').split(' ')[1]
507
+ uid = verify_token(token)
508
+ if not uid: return jsonify({'error': 'Unauthorized'}), 401
509
+ logger.info(f"Token verified for user {uid} for narration update.")
510
+
511
+ data = request.get_json()
512
+ scene_id = data.get('scene_id')
513
+ narration_text = data.get('narration_text')
514
+ voice_model = data.get('voice_model', 'aura-2-andromeda-en')
515
+ if not scene_id or narration_text is None:
516
+ return jsonify({'error': 'scene_id and narration_text are required'}), 400
517
+
518
+ logger.info(f"User {uid}: Updating narration for scene {scene_id} in project {project_id}.")
519
+ audio_bytes = deepgram_tts(narration_text, voice_model)
520
+ if not audio_bytes:
521
+ logger.error(f"User {uid}: Deepgram TTS failed for project {project_id}, scene {scene_id}.")
522
+ return jsonify({'error': 'Failed to generate audio'}), 500
523
+
524
+ audio_blob_name = f"sozo_projects/{uid}/{project_id}/audio/{scene_id}.mp3"
525
+ logger.info(f"User {uid}: Uploading new audio to {audio_blob_name}.")
526
+ audio_blob = bucket.blob(audio_blob_name)
527
+ audio_blob.upload_from_string(audio_bytes, content_type="audio/mpeg")
528
+ new_audio_url = audio_blob.public_url
529
+
530
+ logger.info(f"User {uid}: Updating database with new narration and audio URL for project {project_id}.")
531
+ scene_ref = db.reference(f'sozo_projects/{project_id}/video_script/scenes')
532
+ scenes = scene_ref.get()
533
+ scene_found = False
534
+ if scenes:
535
+ for i, scene in enumerate(scenes):
536
+ if scene.get('scene_id') == scene_id:
537
+ scene_ref.child(str(i)).update({
538
+ 'narration': narration_text,
539
+ 'audio_storage_path': new_audio_url
540
+ })
541
+ scene_found = True
542
+ break
543
+
544
+ if not scene_found:
545
+ logger.warning(f"User {uid}: Scene {scene_id} not found in database for project {project_id} during narration update.")
546
+ return jsonify({'error': 'Scene not found in database'}), 404
547
+
548
+ project_ref = db.reference(f'sozo_projects/{project_id}')
549
+ project_ref.update({'updatedAt': datetime.utcnow().isoformat()})
550
+
551
+ logger.info(f"User {uid}: Successfully updated narration for scene {scene_id} in project {project_id}.")
552
+ return jsonify({'success': True, 'new_audio_url': new_audio_url}), 200
553
+ except Exception as e:
554
+ logger.error(f"CRITICAL ERROR updating narration for {project_id}: {traceback.format_exc()}")
555
+ return jsonify({'error': str(e)}), 500
556
  # -----------------------------------------------------------------------------
557
  # 5. UNIVERSAL ENDPOINTS (Waitlist, Feedback, Credits)
558
  # -----------------------------------------------------------------------------