# flask_app_hf.py - English Helper HF Spaces (Sem Autenticação) import os import io import json import base64 from datetime import datetime from flask import Flask, request, jsonify, send_file, render_template, render_template_string, redirect, url_for, Response try: from gtts import gTTS except Exception: gTTS = None try: from groq import Groq except Exception: Groq = None try: import google.generativeai as genai from google.generativeai.types import GenerationConfig except Exception: genai = None # --- CONFIGURAÇÃO INICIAL --- app = Flask(__name__) # Configuração simplificada para HF Spaces app.config['SECRET_KEY'] = 'hf-simple-key-no-auth' app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 print(f"✅ Flask HF app inicializado") print(f"✅ Working directory: {os.getcwd()}") # In-memory storage (suitable for HF Spaces testing only) IN_MEMORY = { 'users': {}, # user_id -> record dict 'flashcards': {}, # user_id -> [items] 'conversations': {}, # user_id -> [items] 'analytics': {}, # user_id -> [items] 'study_plans': {}, # user_id or 'global' -> plan dict/list } # Keep legacy DATA_ROOT variable for compatibility checks (unused in-memory) _module_dir = os.path.dirname(os.path.abspath(__file__)) DATA_ROOT = os.environ.get('EH_DATA_ROOT', os.path.join(_module_dir, 'hf_data')) # --- UTILITÁRIOS DE ARMAZENAMENTO --- def save_user_data(user_id, data_type, data): """Salvar dados do usuário em memória.""" try: store = IN_MEMORY.setdefault(data_type, {}) lst = store.setdefault(user_id, []) # attach timestamp if isinstance(data, dict): data = dict(data) entry = data if isinstance(entry, dict): entry.setdefault('timestamp', datetime.now().isoformat()) lst.append(entry) # keep last 100 if len(lst) > 100: store[user_id] = lst[-100:] return True except Exception as e: print(f"Erro ao salvar dados (in-memory): {e}") return False def save_user_record(user_id, metadata=None): """Create or update a simple user record in memory.""" try: record = IN_MEMORY['users'].get(user_id, {}) record['id'] = user_id record.setdefault('created_at', datetime.now().isoformat()) if metadata and isinstance(metadata, dict): record.update(metadata) IN_MEMORY['users'][user_id] = record return True except Exception as e: print(f"save_user_record error (in-memory): {e}") return False def load_user_data(user_id, data_type): """Load user data from memory.""" try: store = IN_MEMORY.get(data_type, {}) return list(store.get(user_id, [])) except Exception as e: print(f"Erro ao carregar dados (in-memory): {e}") return [] def get_all_users(): """Return sorted list of all user ids known in memory.""" users = set() users.update(IN_MEMORY.get('users', {}).keys()) for data_type in ['flashcards', 'conversations', 'analytics', 'study_plans']: users.update(IN_MEMORY.get(data_type, {}).keys()) return sorted([u for u in users if u]) # --- CONFIGURAÇÃO DE APIs --- # Configurar APIs groq_client = None genai_client = None try: groq_api_key = os.environ.get('GROQ_API_KEY') if groq_api_key: groq_client = Groq(api_key=groq_api_key) print("✅ Groq API configurada") except Exception as e: print(f"⚠️ Groq API não configurada: {e}") try: gemini_api_key = os.environ.get('GEMINI_API_KEY') if gemini_api_key: genai.configure(api_key=gemini_api_key) genai_client = genai print("✅ Gemini API configurada") except Exception as e: print(f"⚠️ Gemini API não configurada: {e}") # --- ROTAS PRINCIPAIS --- @app.route('/') def index(): return render_template('index.html') @app.route('/admin') def admin(): return render_template('admin.html') @app.route('/dashboard') def dashboard_redirect(): """Legacy route: redirect /dashboard to /admin.""" return redirect(url_for('admin')) @app.route('/status') def status(): return render_template('status.html') # --- API ENDPOINTS --- @app.route('/list-models', methods=['GET']) def list_models(): """Listar modelos disponíveis""" available_models = [] if groq_client: available_models.extend([ {"name": "Llama 3.2 90B (Ultra Fast)", "value": "groq:llama-3.2-90b-text-preview"}, {"name": "Llama 3.2 11B Vision (Fast)", "value": "groq:llama-3.2-11b-vision-preview"}, {"name": "Llama 3.1 70B (Fast)", "value": "groq:llama-3.1-70b-versatile"}, {"name": "Mixtral 8x7B (Fast)", "value": "groq:mixtral-8x7b-32768"} ]) if genai_client: available_models.extend([ {"name": "Gemini 2.5 Flash (Recommended)", "value": "gemini:gemini-2.5-flash-latest"}, {"name": "Gemini 2.5 Pro Experimental", "value": "gemini:gemini-2.5-pro-exp"}, {"name": "Gemini 1.5 Flash", "value": "gemini:gemini-1.5-flash-latest"}, {"name": "Gemini 1.5 Pro", "value": "gemini:gemini-1.5-pro-latest"} ]) return jsonify(available_models) # Cache de áudio TTS em memória tts_cache = {} @app.route('/tts-proxy', methods=['POST']) def tts_proxy(): data = request.get_json() text = data.get('text', '') tld = data.get('tld', 'co.uk') if not text: return jsonify({"error": "No text provided"}), 400 if len(text) > 10000: return jsonify({"error": "Text is too long. Maximum 10,000 characters allowed."}), 400 import hashlib cache_key = hashlib.md5(f"{text}_{tld}".encode()).hexdigest() try: if cache_key in tts_cache: print(f"🎵 TTS Cache HIT: {len(text)} chars") cached_audio = tts_cache[cache_key] audio_fp = io.BytesIO(cached_audio) return send_file(audio_fp, mimetype='audio/mpeg', as_attachment=False) print(f"🎵 TTS Cache MISS: Gerando áudio para {len(text)} chars, tld: {tld}") tts = gTTS(text=text, lang='en', tld=tld) mp3_fp = io.BytesIO() tts.write_to_fp(mp3_fp) mp3_fp.seek(0) audio_data = mp3_fp.read() tts_cache[cache_key] = audio_data if len(tts_cache) > 50: oldest_key = next(iter(tts_cache)) del tts_cache[oldest_key] audio_fp = io.BytesIO(audio_data) return send_file(audio_fp, mimetype='audio/mpeg', as_attachment=False) except Exception as e: print(f"❌ TTS Error: {e}") return jsonify({"error": f"Failed to generate audio via gTTS: {e}"}), 500 @app.route('/explain-proxy', methods=['POST']) def explain_proxy(): """Gerar explicação/flashcard""" data = request.get_json() selected_text = data.get('selectedText', '') model_info = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1) if len(model_info) != 2: return jsonify({"error": "Invalid model format"}), 400 model_provider, model_name = model_info if not selected_text: return jsonify({"error": "No text selected"}), 400 try: prompt = f""" Create a comprehensive flashcard for the English term/phrase: "{selected_text}" Provide: 1. Clear definition in English 2. Translation to Portuguese 3. Example sentence using the term 4. Same sentence with the term replaced by "____" for practice Return as JSON with keys: definition, translation, context_sentence, gapped_sentence """ if model_provider == 'gemini' and genai_client: model = genai_client.GenerativeModel(model_name) response = model.generate_content(prompt) # Extrair JSON da resposta response_text = response.text if '```json' in response_text: json_start = response_text.find('```json') + 7 json_end = response_text.find('```', json_start) response_text = response_text[json_start:json_end].strip() result = json.loads(response_text) result['term'] = selected_text return jsonify(result) elif model_provider == 'groq' and groq_client: response = groq_client.chat.completions.create( messages=[{"role": "user", "content": prompt}], model=model_name, temperature=0.3 ) response_text = response.choices[0].message.content if '```json' in response_text: json_start = response_text.find('```json') + 7 json_end = response_text.find('```', json_start) response_text = response_text[json_start:json_end].strip() result = json.loads(response_text) result['term'] = selected_text return jsonify(result) else: return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured"}), 503 except Exception as e: print(f"AI ANALYSIS ERROR: {e}") return jsonify({"error": str(e)}), 500 # --- ROUTES DE DADOS (SEM AUTENTICAÇÃO) --- @app.route('/users', methods=['GET']) def get_users(): """Obter lista de usuários""" users = get_all_users() return jsonify({'users': users, 'total': len(users)}) @app.route('/user/create', methods=['POST']) def create_user(): """Create a lightweight user record for HF Spaces (no auth).""" try: data = request.get_json() or {} user_id = data.get('user_id') or data.get('email') or data.get('name') if not user_id: return jsonify({'success': False, 'error': 'user_id (or email/name) required'}), 400 # sanitize user_id to a filename-friendly string safe_id = ''.join(c for c in user_id if c.isalnum() or c in ('-', '_')).lower() if not safe_id: return jsonify({'success': False, 'error': 'invalid user_id'}), 400 ok = save_user_record(safe_id, {'raw': user_id}) if not ok: return jsonify({'success': False, 'error': 'failed to save user record'}), 500 users = get_all_users() return jsonify({'success': True, 'user_id': safe_id, 'users': users}) except Exception as e: print(f"create_user error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/user//flashcards', methods=['GET', 'POST']) def user_flashcards(user_id): """Gerenciar flashcards do usuário""" if request.method == 'POST': data = request.get_json() if save_user_data(user_id, 'flashcards', data): return jsonify({'success': True, 'message': 'Flashcard saved'}) else: return jsonify({'success': False, 'message': 'Failed to save flashcard'}), 500 else: flashcards = load_user_data(user_id, 'flashcards') return jsonify({'flashcards': flashcards}) @app.route('/user//conversations', methods=['GET', 'POST']) def user_conversations(user_id): """Gerenciar conversas do usuário""" if request.method == 'POST': data = request.get_json() if save_user_data(user_id, 'conversations', data): return jsonify({'success': True, 'message': 'Conversation saved'}) else: return jsonify({'success': False, 'message': 'Failed to save conversation'}), 500 else: conversations = load_user_data(user_id, 'conversations') return jsonify({'conversations': conversations}) @app.route('/user//analytics', methods=['GET']) def user_analytics(user_id): """Obter analytics do usuário""" analytics = load_user_data(user_id, 'analytics') flashcards = load_user_data(user_id, 'flashcards') conversations = load_user_data(user_id, 'conversations') return jsonify({ 'total_flashcards': len(flashcards), 'total_conversations': len(conversations), 'total_sessions': len(analytics), 'recent_activity': analytics[-10:] if analytics else [] }) # --- STATUS E ADMIN --- @app.route('/admin/stats', methods=['GET']) def admin_stats(): """Estatísticas do sistema""" users = get_all_users() stats = { 'total_users': len(users), 'users': [] } for user_id in users: flashcards = len(load_user_data(user_id, 'flashcards')) conversations = len(load_user_data(user_id, 'conversations')) stats['users'].append({ 'user_id': user_id, 'flashcards': flashcards, 'conversations': conversations }) return jsonify(stats) @app.route('/system/status', methods=['GET']) def system_status(): """Status do sistema""" return jsonify({ 'status': 'running', 'version': 'HF-Simplified-1.0', 'apis': { 'groq': groq_client is not None, 'gemini': genai_client is not None }, 'storage': 'in_memory', 'demo_mode': True, 'auth': 'disabled' }) # --- ADMIN / HEALTH / EXPORT (file-based implementations for HF Spaces) --- @app.route('/admin/system/health', methods=['GET']) def admin_system_health(): """Return simple system health info using file-based storage (no external deps).""" try: import shutil # compute simple stats from IN_MEMORY schema = {} total_rows = 0 db_size = 0 for table, table_data in IN_MEMORY.items(): if isinstance(table_data, dict): row_count = sum(1 for _ in table_data.keys()) # approximate size by serializing entries size = 0 for k, v in table_data.items(): try: size += len(json.dumps(v, ensure_ascii=False).encode('utf-8')) except Exception: pass schema[table] = {'row_count': row_count, 'size_bytes': size} total_rows += row_count db_size += size # disk usage for current filesystem (informational) try: du = shutil.disk_usage('.') disk = { 'total': du.total, 'used': du.used, 'free': du.free, 'percent': round(du.used / du.total * 100, 2) if du.total else 0 } except Exception: disk = {} health = { 'memory': True, 'disk': disk, 'database': { 'storage': 'in_memory', 'total_rows': total_rows, 'estimated_size_bytes': db_size, 'tables': schema }, 'uptime': datetime.now().isoformat() } return jsonify({'success': True, 'health': health}) except Exception as e: print(f"Health endpoint error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/admin/database/schema', methods=['GET']) def admin_database_schema(): """Return a simple schema overview derived from hf_data folders.""" try: # derive schema from IN_MEMORY schema = {} for table, table_data in IN_MEMORY.items(): cols = [] row_count = 0 if isinstance(table_data, dict): row_count = sum(1 for _ in table_data.keys()) # infer columns/types from first value try: first_val = None for v in table_data.values(): first_val = v break sample = None if isinstance(first_val, list) and first_val: sample = first_val[0] elif isinstance(first_val, dict): sample = first_val if isinstance(sample, dict): cols = [{'name': k, 'type': type(v).__name__} for k, v in sample.items()] except Exception: cols = [] schema[table] = { 'row_count': row_count, 'columns': cols } return jsonify({'success': True, 'schema': schema}) except Exception as e: print(f"Schema error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/admin/users', methods=['GET']) def admin_list_users(): """Return paginated list of users (derived from hf_data files).""" try: page = int(request.args.get('page', 1)) per_page = int(request.args.get('per_page', 20)) users = get_all_users() total = len(users) total_pages = max(1, (total + per_page - 1) // per_page) start = (page - 1) * per_page end = start + per_page users_page = [] for uid in users[start:end]: # gather basic stats flashcards = len(load_user_data(uid, 'flashcards')) conversations = len(load_user_data(uid, 'conversations')) analytics = len(load_user_data(uid, 'analytics')) # created_at from user record if available created_at = None try: urec = IN_MEMORY.get('users', {}).get(uid) if urec and isinstance(urec, dict): created_at = urec.get('created_at') except Exception: created_at = None users_page.append({ 'id': uid, 'email': uid, 'created_at': created_at, 'flashcard_count': flashcards, 'conversation_count': conversations, 'session_count': analytics }) return jsonify({'success': True, 'data': {'users': users_page, 'page': page, 'per_page': per_page, 'total': total, 'total_pages': total_pages}}) except Exception as e: print(f"admin_list_users error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/admin/users/', methods=['GET', 'DELETE']) def admin_user_detail(user_id): """Get detailed info for a user or delete their data (file-based).""" try: if request.method == 'DELETE': # remove in-memory records across tables removed = [] for folder in ['flashcards', 'conversations', 'analytics', 'study_plans']: try: tbl = IN_MEMORY.get(folder, {}) if user_id in tbl: del tbl[user_id] removed.append(f"{folder}/{user_id}") except Exception as ex: print(f"Failed deleting in-memory {folder}/{user_id}: {ex}") # remove user record try: if user_id in IN_MEMORY.get('users', {}): del IN_MEMORY['users'][user_id] except Exception: pass return jsonify({'success': True, 'deleted': removed}) # GET -> return analytics, flashcards, conversations flashcards = load_user_data(user_id, 'flashcards') conversations = load_user_data(user_id, 'conversations') analytics = load_user_data(user_id, 'analytics') # Build token_usage overview if present in analytics entries token_usage = {} for entry in analytics: if isinstance(entry, dict) and 'token_usage' in entry: for prov, usage in entry['token_usage'].items(): s = token_usage.setdefault(prov, {'input': 0, 'output': 0, 'calls': 0}) s['input'] += usage.get('input_tokens', 0) s['output'] += usage.get('output_tokens', 0) s['calls'] += 1 user_info = { 'user': {'id': user_id, 'email': user_id, 'created_at': None}, 'flashcards': flashcards, 'conversations': conversations, 'recent_sessions': analytics[-10:] if analytics else [], 'settings': {}, 'token_usage': [{'provider': k, 'input_tokens': v['input'], 'output_tokens': v['output'], 'calls': v['calls']} for k, v in token_usage.items()] } return jsonify({'success': True, 'user': user_info}) except Exception as e: print(f"admin_user_detail error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/admin/system/alerts', methods=['GET']) def admin_system_alerts(): """Return current system alerts (file-based: empty by default).""" # In HF file-based mode we have no centralized alerting - return empty list return jsonify({'success': True, 'alerts': []}) @app.route('/admin/export/users', methods=['GET']) def admin_export_users(): """Export users list as CSV (file-based).""" try: import csv users = get_all_users() output = io.StringIO() writer = csv.writer(output) writer.writerow(['id', 'email', 'flashcards', 'conversations', 'analytics']) for uid in users: fc = len(load_user_data(uid, 'flashcards')) conv = len(load_user_data(uid, 'conversations')) an = len(load_user_data(uid, 'analytics')) writer.writerow([uid, uid, fc, conv, an]) mem = io.BytesIO(output.getvalue().encode('utf-8')) mem.seek(0) return send_file(mem, mimetype='text/csv', as_attachment=True, download_name='users_export.csv') except Exception as e: print(f"export users error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/admin/export/tokens', methods=['GET']) def admin_export_tokens(): """Export token usage summary as CSV (aggregated from analytics).""" try: import csv users = get_all_users() output = io.StringIO() writer = csv.writer(output) writer.writerow(['user_id', 'provider', 'input_tokens', 'output_tokens', 'calls']) for uid in users: analytics = load_user_data(uid, 'analytics') agg = {} for entry in analytics: if isinstance(entry, dict) and 'token_usage' in entry: for prov, usage in entry['token_usage'].items(): a = agg.setdefault(prov, {'input': 0, 'output': 0, 'calls': 0}) a['input'] += usage.get('input_tokens', 0) a['output'] += usage.get('output_tokens', 0) a['calls'] += 1 for prov, vals in agg.items(): writer.writerow([uid, prov, vals['input'], vals['output'], vals['calls']]) mem = io.BytesIO(output.getvalue().encode('utf-8')) mem.seek(0) return send_file(mem, mimetype='text/csv', as_attachment=True, download_name='tokens_export.csv') except Exception as e: print(f"export tokens error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/admin/export/all', methods=['GET']) def admin_export_all(): """Package the entire DATA_ROOT into a zip and send for download.""" try: import zipfile mem_zip = io.BytesIO() with zipfile.ZipFile(mem_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf: # dump each table as files for table, table_data in IN_MEMORY.items(): if isinstance(table_data, dict): for uid, val in table_data.items(): try: payload = json.dumps(val, ensure_ascii=False, indent=2) except Exception: payload = str(val) arcname = os.path.join(table, f"{uid}.json") zf.writestr(arcname, payload) else: # serialize whole object try: payload = json.dumps(table_data, ensure_ascii=False, indent=2) except Exception: payload = str(table_data) arcname = f"{table}.json" zf.writestr(arcname, payload) mem_zip.seek(0) return send_file(mem_zip, mimetype='application/zip', as_attachment=True, download_name='hf_data_export.zip') except Exception as e: print(f"export all error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/admin/dashboard', methods=['GET']) def admin_dashboard_compat(): """Compatibility endpoint for older admin UI that expects /admin/dashboard.""" try: stats = admin_stats() # reuse existing return jsonify({'success': True, 'stats': stats.get_json() if isinstance(stats, Response) else stats}) except Exception as e: print(f"admin_dashboard error: {e}") return jsonify({'success': False, 'error': str(e)}), 500 from study_plan import save_study_plan, load_study_plan, generate_study_plan @app.route('/study-plan', methods=['GET', 'POST']) def study_plan(): """Salvar ou carregar plano de estudos global (sem autenticação)""" if request.method == 'POST': try: data = request.get_json() user_id = data.get('user_id') plan = generate_study_plan(data) # save globally save_study_plan(plan) # also save per-user if provided if user_id: save_user_data(user_id, 'study_plans', plan) return jsonify({'success': True, 'message': 'Study plan generated and saved', 'plan': plan}) except Exception as e: print(f"Erro ao salvar study plan: {e}") return jsonify({'success': False, 'message': 'Failed to save study plan'}), 500 else: try: plan = load_study_plan() return jsonify({'success': True, 'plan': plan}) except Exception as e: print(f"Erro ao carregar study plan: {e}") return jsonify({'success': False, 'message': 'Failed to load study plan'}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=7860, debug=True)