#!/usr/bin/env python3 """ Simple Report Visualizer View content evaluation reports with images side-by-side """ import os import json import glob import zipfile import uuid import shutil import sys from datetime import datetime from flask import Flask, render_template, jsonify, send_file, request, flash, redirect, url_for from werkzeug.utils import secure_filename from pathlib import Path # Load environment variables try: from dotenv import load_dotenv load_dotenv() print("✅ Environment variables loaded from .env file") except ImportError: print("⚠️ python-dotenv not available, using system environment variables only") # Add src to path for imports sys.path.insert(0, 'src') app = Flask(__name__) app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key-change-in-production') app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB max file size app.config['UPLOAD_FOLDER'] = 'uploads' app.config['PROCESSED_FOLDER'] = 'processed' # Ensure directories exist os.makedirs('uploads', exist_ok=True) os.makedirs('processed', exist_ok=True) os.makedirs('reviews', exist_ok=True) os.makedirs('templates', exist_ok=True) os.makedirs('batch_images', exist_ok=True) # Persistent storage for batch images # Import content evaluation modules try: from src.content_evaluator import ContentEvaluator from src.report_generator import ReportGenerator EVALUATOR_AVAILABLE = True except ImportError as e: print(f"⚠️ Content evaluator not available: {e}") EVALUATOR_AVAILABLE = False # Import Facebook Ad Library client try: from src.facebook_ad_client import create_facebook_client FACEBOOK_CLIENT_AVAILABLE = True print("✅ Facebook Ad Library client available") except ImportError as e: print(f"⚠️ Facebook Ad Library client not available: {e}") FACEBOOK_CLIENT_AVAILABLE = False ALLOWED_EXTENSIONS = {'zip'} IMAGE_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'} VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov', 'mkv', 'webm', 'm4v'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def is_image_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in IMAGE_EXTENSIONS def is_video_file_ext(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in VIDEO_EXTENSIONS def is_media_file(filename): return is_image_file(filename) or is_video_file_ext(filename) def get_available_reports(): """Get all JSON reports from reviews directory""" reports = [] reviews_dir = Path("reviews") if reviews_dir.exists(): for json_file in reviews_dir.glob("*.json"): try: with open(json_file, 'r') as f: data = json.load(f) # Extract basic info if 'evaluation_report' in data: report_data = data['evaluation_report'] elif 'detailed_evaluation_report' in data: report_data = data['detailed_evaluation_report'] else: continue reports.append({ 'filename': json_file.name, 'path': str(json_file), 'timestamp': report_data.get('timestamp', 'Unknown'), 'total_images': len(report_data.get('results', [])), 'approved': report_data.get('summary', {}).get('approved', 0), 'rejected': report_data.get('summary', {}).get('rejected', 0) }) except Exception as e: print(f"Error reading {json_file}: {e}") return sorted(reports, key=lambda x: x['timestamp'], reverse=True) @app.route('/') def index(): """Main page with report selection""" reports = get_available_reports() return render_template('ez_viewer.html', reports=reports) @app.route('/api/report/') def get_report(filename): """Get report data""" filepath = Path("reviews") / filename if not filepath.exists(): return jsonify({'error': 'Report not found'}), 404 try: with open(filepath, 'r') as f: data = json.load(f) # Normalize the data structure if 'evaluation_report' in data: report_data = data['evaluation_report'] elif 'detailed_evaluation_report' in data: report_data = data['detailed_evaluation_report'] else: return jsonify({'error': 'Invalid report format'}), 400 return jsonify(report_data) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/image/') def serve_image(image_path): """Serve image files""" import urllib.parse # Decode URL encoding decoded_path = urllib.parse.unquote(image_path) safe_path = Path(decoded_path) # If it's already an absolute path that exists, use it if safe_path.is_absolute() and safe_path.exists() and safe_path.is_file(): return send_file(safe_path) # If it's a relative path, make it absolute from current working directory if not safe_path.is_absolute(): absolute_path = Path.cwd() / safe_path if absolute_path.exists() and absolute_path.is_file(): return send_file(absolute_path) # Try common image directories with absolute paths base_dir = Path.cwd() possible_paths = [ base_dir / "examples/images" / safe_path.name, base_dir / "images" / safe_path.name, base_dir / "batch_images" / safe_path.parent.name / safe_path.name if len(safe_path.parts) > 1 else None, base_dir / safe_path.name if not safe_path.is_absolute() else safe_path ] # Remove None values possible_paths = [p for p in possible_paths if p is not None] for path in possible_paths: if path.exists() and path.is_file(): return send_file(path) return f"Image not found: {decoded_path}", 404 @app.route('/api/batch_image//') def serve_batch_image(session_id, filename): """Serve images from batch processing""" batch_dir = Path('batch_images') / session_id image_path = batch_dir / filename if image_path.exists() and image_path.is_file(): return send_file(image_path) return f"Batch image not found: {session_id}/{filename}", 404 @app.route('/upload', methods=['GET', 'POST']) def upload_zip(): """Handle zip file upload and processing""" if request.method == 'GET': return render_template('upload.html') if 'zipfile' not in request.files: flash('No file selected') return redirect(request.url) file = request.files['zipfile'] if file.filename == '': flash('No file selected') return redirect(request.url) if file and allowed_file(file.filename): # Create unique session ID session_id = str(uuid.uuid4()) session_dir = Path(app.config['PROCESSED_FOLDER']) / session_id session_dir.mkdir(parents=True, exist_ok=True) # Save zip file filename = secure_filename(file.filename) zip_path = Path(app.config['UPLOAD_FOLDER']) / f"{session_id}_{filename}" file.save(zip_path) try: # Create batch directory for permanent image storage batch_dir = Path('batch_images') / session_id batch_dir.mkdir(parents=True, exist_ok=True) # Extract zip file to temporary location first temp_extract_dir = session_dir / 'temp' with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(temp_extract_dir) # Find and copy image files to permanent location image_files = [] processed_files = set() # Track processed files to avoid duplicates image_counter = 1 print(f"🔍 Scanning extracted files in {temp_extract_dir}") for root, dirs, files in os.walk(temp_extract_dir): print(f" Checking directory: {root}") for file in files: source_path = Path(root) / file # Skip if already processed (avoid duplicates) if str(source_path) in processed_files: continue if is_media_file(file): media_type = "video" if is_video_file_ext(file) else "image" print(f" Found {media_type}: {file}") # Create unique filename to avoid conflicts file_ext = Path(file).suffix.lower() prefix = "video" if media_type == "video" else "image" safe_filename = f"{prefix}_{image_counter:03d}{file_ext}" dest_path = batch_dir / safe_filename try: # Verify source file exists and is readable if not source_path.exists(): print(f" ⚠️ Source file doesn't exist: {source_path}") continue # Check file size (skip empty files) if source_path.stat().st_size == 0: print(f" ⚠️ Empty file skipped: {file}") continue # Validate file based on type if media_type == "image": # Try to open with PIL to verify it's a valid image from PIL import Image with Image.open(source_path) as test_img: test_img.verify() elif media_type == "video": # Basic video validation - try to open with OpenCV import cv2 cap = cv2.VideoCapture(str(source_path)) if not cap.isOpened(): raise Exception("Cannot open video file") cap.release() # Copy file to permanent location shutil.copy2(source_path, dest_path) print(f" ✅ Copied to: {safe_filename}") image_files.append(dest_path) processed_files.add(str(source_path)) image_counter += 1 except Exception as e: print(f" ❌ Failed to process {file}: {e}") continue # Clean up temporary extraction shutil.rmtree(temp_extract_dir, ignore_errors=True) print(f"📊 Extraction complete: {len(image_files)} valid images found") if not image_files: flash('No valid image files found in the zip archive') return redirect(url_for('upload_zip')) # Process images if evaluator is available if EVALUATOR_AVAILABLE: try: # Initialize evaluator evaluator = ContentEvaluator('config/suno_guidelines.txt') report_generator = ReportGenerator() # Process images with better error handling results = [] print(f"🔄 Processing {len(image_files)} images...") for i, img_path in enumerate(image_files, 1): print(f" Processing {i}/{len(image_files)}: {img_path.name}") try: # Double-check file exists before processing if not img_path.exists(): print(f" ⚠️ File missing, skipping: {img_path}") continue # Determine if it's a video or image if is_video_file_ext(img_path.name): print(f" 🎬 Processing video: {img_path.name}") result = evaluator.evaluate_video(str(img_path)) else: print(f" 🖼️ Processing image: {img_path.name}") result = evaluator.evaluate_image(str(img_path)) if result and not result.get('error'): results.append(result) print(f" ✅ Processed successfully") else: print(f" ⚠️ Evaluation failed: {result.get('error', 'Unknown error')}") except Exception as e: print(f" ❌ Error processing {img_path.name}: {e}") continue if results: # Generate summary and report summary = evaluator.generate_summary(results) report_json = report_generator.generate_json_report(results, summary) # Save report with session ID report_filename = f"batch_evaluation_{session_id[:8]}.json" report_path = report_generator.save_report(report_json, f'reviews/{report_filename}') flash(f'Successfully processed {len(results)} out of {len(image_files)} images. Report saved as {report_filename}') return redirect(url_for('view_session', session_id=session_id, report_file=report_filename)) else: flash('No images could be processed') return redirect(url_for('upload_zip')) except Exception as e: flash(f'Error processing images: {str(e)}') return redirect(url_for('upload_zip')) else: # If evaluator not available, just show uploaded images flash(f'Uploaded {len(image_files)} images (evaluation not available)') return redirect(url_for('view_session', session_id=session_id)) except Exception as e: flash(f'Error processing zip file: {str(e)}') return redirect(url_for('upload_zip')) finally: # Clean up zip file if zip_path.exists(): zip_path.unlink() else: flash('Please upload a valid ZIP file') return redirect(url_for('upload_zip')) @app.route('/session/') def view_session(session_id): """View processed session images""" session_dir = Path(app.config['PROCESSED_FOLDER']) / session_id if not session_dir.exists(): flash('Session not found') return redirect(url_for('index')) # Get report file if specified report_file = request.args.get('report_file') # Find media files in batch storage media_files = [] batch_dir = Path('batch_images') / session_id if batch_dir.exists(): for file_path in batch_dir.glob('*'): if file_path.is_file() and is_media_file(file_path.name): media_type = "video" if is_video_file_ext(file_path.name) else "image" media_files.append({ 'name': file_path.name, 'path': f"{session_id}/{file_path.name}", 'full_path': str(file_path), 'batch_path': file_path.name, 'type': media_type }) return render_template('session_view.html', session_id=session_id, images=media_files, # Now includes both images and videos report_file=report_file) @app.route('/api/session_image//') def serve_session_image(session_id, image_path): """Serve images from a processing session""" import urllib.parse # Decode URL encoding decoded_path = urllib.parse.unquote(image_path) session_dir = Path(app.config['PROCESSED_FOLDER']) / session_id full_path = session_dir / decoded_path # Make sure path is absolute and exists absolute_path = full_path.resolve() if absolute_path.exists() and absolute_path.is_file(): return send_file(absolute_path) return f"Session image not found: {decoded_path}", 404 @app.route('/facebook-ads') def facebook_ads(): """Facebook Ads search interface""" if not FACEBOOK_CLIENT_AVAILABLE: flash('Facebook Ad Library integration not available. Please check configuration.') return redirect(url_for('index')) return render_template('facebook_ads.html') @app.route('/facebook-ads/search', methods=['POST']) def search_facebook_ads(): """Handle Facebook ads search request""" if not FACEBOOK_CLIENT_AVAILABLE: return jsonify({'error': 'Facebook Ad Library integration not available'}), 400 try: # Get search parameters from form search_terms = request.form.get('search_terms', '').strip() country = request.form.get('country', 'US') ad_type = request.form.get('ad_type', 'ALL') media_type = request.form.get('media_type', 'ALL') start_date = request.form.get('start_date') end_date = request.form.get('end_date') limit = min(int(request.form.get('limit', 50)), 100) # Cap at 100 for UI print(f"🔍 Facebook ad search request:") print(f" Terms: {search_terms or 'Any'}") print(f" Country: {country}") print(f" Limit: {limit}") # Create Facebook client fb_client = create_facebook_client() # Test connection first connection_test = fb_client.test_connection() if not connection_test.get('success'): return jsonify({ 'error': 'Facebook API connection failed', 'details': connection_test.get('error', 'Unknown error') }), 400 # Search for ads search_results = fb_client.search_ads( search_terms=search_terms if search_terms else None, ad_reached_countries=country, ad_type=ad_type, media_type=media_type, start_date=start_date if start_date else None, end_date=end_date if end_date else None, limit=limit ) if 'error' in search_results: return jsonify({ 'error': 'Ad search failed', 'details': search_results.get('error', 'Unknown error') }), 400 ads = search_results.get('data', []) # Create session for this search session_id = str(uuid.uuid4()) session_dir = Path('processed') / f"facebook_ads_{session_id}" session_dir.mkdir(parents=True, exist_ok=True) # Save search metadata search_metadata = { 'session_id': session_id, 'search_params': { 'search_terms': search_terms, 'country': country, 'ad_type': ad_type, 'media_type': media_type, 'start_date': start_date, 'end_date': end_date, 'limit': limit }, 'results_count': len(ads), 'search_time': datetime.now().isoformat(), 'ads': ads[:10] # Store first 10 for preview } metadata_file = session_dir / 'search_metadata.json' with open(metadata_file, 'w') as f: json.dump(search_metadata, f, indent=2) flash(f'Found {len(ads)} Facebook ads matching your criteria') return jsonify({ 'success': True, 'session_id': session_id, 'ads_found': len(ads), 'ads_preview': ads[:10], # Send first 10 for display 'redirect_url': url_for('facebook_ads_results', session_id=session_id) }) except Exception as e: print(f"❌ Facebook ads search error: {e}") return jsonify({'error': f'Search failed: {str(e)}'}), 500 @app.route('/facebook-ads/results/') def facebook_ads_results(session_id): """Display Facebook ads search results""" session_dir = Path('processed') / f"facebook_ads_{session_id}" metadata_file = session_dir / 'search_metadata.json' if not metadata_file.exists(): flash('Search results not found') return redirect(url_for('facebook_ads')) try: with open(metadata_file, 'r') as f: search_data = json.load(f) return render_template('facebook_ads_results.html', session_id=session_id, search_data=search_data) except Exception as e: flash(f'Error loading search results: {e}') return redirect(url_for('facebook_ads')) @app.route('/facebook-ads/process/', methods=['POST']) def process_facebook_ads(session_id): """Process selected Facebook ads for evaluation""" if not FACEBOOK_CLIENT_AVAILABLE or not EVALUATOR_AVAILABLE: return jsonify({'error': 'Required services not available'}), 400 try: session_dir = Path('processed') / f"facebook_ads_{session_id}" metadata_file = session_dir / 'search_metadata.json' if not metadata_file.exists(): return jsonify({'error': 'Session not found'}), 404 # Get selected ad IDs from request selected_ads = request.json.get('selected_ads', []) if not selected_ads: return jsonify({'error': 'No ads selected'}), 400 # Load search metadata with open(metadata_file, 'r') as f: search_data = json.load(f) # Note: For now, we'll create placeholder evaluations since # Facebook API doesn't provide direct creative downloads results = [] for ad_id in selected_ads: # Find the ad in our search results ad_data = None for ad in search_data.get('ads', []): if ad.get('id') == ad_id: ad_data = ad break if not ad_data: continue # Create a result entry with available metadata result = { 'approved': True, # Placeholder - would need actual creative analysis 'reason': 'Facebook ad metadata processed - creative analysis requires manual review', 'confidence': 0.5, 'link': ad_data.get('ad_snapshot_url', ''), 'facebook_ad_data': { 'ad_id': ad_id, 'page_name': ad_data.get('page_name', 'Unknown'), 'creation_time': ad_data.get('ad_creation_time'), 'delivery_start': ad_data.get('ad_delivery_start_time'), 'delivery_stop': ad_data.get('ad_delivery_stop_time'), 'spend': ad_data.get('spend', {}), 'impressions': ad_data.get('impressions', {}), 'creative_bodies': ad_data.get('ad_creative_bodies', []), 'snapshot_url': ad_data.get('ad_snapshot_url') }, 'media_type': 'facebook_ad', 'text_extracted': ' '.join(ad_data.get('ad_creative_bodies', [])), 'word_count': len(' '.join(ad_data.get('ad_creative_bodies', [])).split()) } results.append(result) if results: # Generate report report_generator = ReportGenerator() # Create summary summary = { 'total_images': len(results), 'approved': sum(1 for r in results if r.get('approved', False)), 'rejected': len(results) - sum(1 for r in results if r.get('approved', False)), 'approval_rate': sum(1 for r in results if r.get('approved', False)) / len(results) if results else 0 } report_json = report_generator.generate_json_report(results, summary) report_filename = f"facebook_ads_evaluation_{session_id[:8]}.json" report_path = report_generator.save_report(report_json, f'reviews/{report_filename}') return jsonify({ 'success': True, 'processed_count': len(results), 'report_filename': report_filename, 'redirect_url': url_for('index') # Redirect to main dashboard }) return jsonify({'error': 'No ads could be processed'}), 400 except Exception as e: print(f"❌ Facebook ads processing error: {e}") return jsonify({'error': f'Processing failed: {str(e)}'}), 500 @app.route('/api/facebook-ads/test') def test_facebook_connection(): """Test Facebook API connection""" if not FACEBOOK_CLIENT_AVAILABLE: return jsonify({'error': 'Facebook client not available'}), 400 try: fb_client = create_facebook_client() result = fb_client.test_connection() return jsonify(result) except Exception as e: return jsonify({'error': f'Connection test failed: {str(e)}'}), 500 if __name__ == '__main__': print("🚀 Starting EZ Content Evaluation Platform") print("📊 Dashboard: http://localhost:3000") print("📤 Upload: http://localhost:3000/upload") if EVALUATOR_AVAILABLE: print("✅ Content evaluation ready") else: print("⚠️ Content evaluation disabled (missing dependencies)") port = int(os.environ.get('PORT', 3000)) app.run(debug=True, host='0.0.0.0', port=port)