EnglishStudyHelper / flask_app_hf.py
amauricunha's picture
Upload flask_app_hf.py
236e06e verified
Raw
History Blame Contribute Delete
27.2 kB
# 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/<user_id>/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/<user_id>/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/<user_id>/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/<path:user_id>', 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)