|
|
from flask import Flask, render_template, request, jsonify, session, send_file |
|
|
from werkzeug.utils import secure_filename |
|
|
import os |
|
|
from google import genai |
|
|
from google.genai import types |
|
|
import io |
|
|
from reportlab.lib.pagesizes import letter |
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer |
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
|
|
from reportlab.lib.units import inch |
|
|
from reportlab.lib.colors import navy, black |
|
|
|
|
|
|
|
|
from datetime import datetime |
|
|
import secrets |
|
|
import re |
|
|
import sqlite3 |
|
|
import threading |
|
|
import time |
|
|
import uuid |
|
|
|
|
|
app = Flask(__name__) |
|
|
app.secret_key = secrets.token_hex(16) |
|
|
app.config['UPLOAD_FOLDER'] = 'uploads' |
|
|
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 |
|
|
app.config['DATABASE'] = 'tasks.db' |
|
|
|
|
|
|
|
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) |
|
|
|
|
|
|
|
|
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY', 'YOUR_API_KEY_HERE') |
|
|
client = genai.Client(api_key=GEMINI_API_KEY) |
|
|
|
|
|
MAX_VIDEO_TOKENS = 600000 |
|
|
|
|
|
ALLOWED_EXTENSIONS = { |
|
|
'pdf': 'application/pdf', |
|
|
'mp3': 'audio/mp3', |
|
|
'mp4': 'video/mp4', |
|
|
'wav': 'audio/wav', |
|
|
'txt': 'text/plain', |
|
|
'doc': 'application/msword', |
|
|
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' |
|
|
} |
|
|
|
|
|
SUMMARY_TYPES = { |
|
|
'court': 'Fais un résumé très court (2-3 paragraphes maximum) des points clés essentiels en français.', |
|
|
'moyen': 'Fais un résumé détaillé structuré avec les points principaux et sous-points importants en français.', |
|
|
'detaille': 'Fais un résumé exhaustif et détaillé avec tous les points importants, citations clés et analyse approfondie en français' |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def init_db(): |
|
|
"""Initialise la base de données""" |
|
|
conn = sqlite3.connect(app.config['DATABASE']) |
|
|
c = conn.cursor() |
|
|
c.execute(''' |
|
|
CREATE TABLE IF NOT EXISTS tasks ( |
|
|
task_id TEXT PRIMARY KEY, |
|
|
user_session TEXT, |
|
|
filename TEXT, |
|
|
summary_type TEXT, |
|
|
status TEXT, |
|
|
progress INTEGER, |
|
|
summary TEXT, |
|
|
error TEXT, |
|
|
created_at TEXT, |
|
|
completed_at TEXT, |
|
|
source_type TEXT, |
|
|
source_path TEXT |
|
|
) |
|
|
''') |
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
def get_db_connection(): |
|
|
"""Retourne une connexion à la base de données""" |
|
|
conn = sqlite3.connect(app.config['DATABASE']) |
|
|
conn.row_factory = sqlite3.Row |
|
|
return conn |
|
|
|
|
|
def create_task(user_session, filename, summary_type, source_type, source_path): |
|
|
"""Crée une nouvelle tâche dans la base de données""" |
|
|
task_id = str(uuid.uuid4()) |
|
|
conn = get_db_connection() |
|
|
conn.execute(''' |
|
|
INSERT INTO tasks (task_id, user_session, filename, summary_type, status, |
|
|
progress, created_at, source_type, source_path) |
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) |
|
|
''', (task_id, user_session, filename, summary_type, 'pending', 0, |
|
|
datetime.now().isoformat(), source_type, source_path)) |
|
|
conn.commit() |
|
|
conn.close() |
|
|
return task_id |
|
|
|
|
|
def update_task_status(task_id, status, progress=None, summary=None, error=None): |
|
|
"""Met à jour le statut d'une tâche""" |
|
|
conn = get_db_connection() |
|
|
|
|
|
if status == 'completed' or status == 'failed': |
|
|
if progress is None: |
|
|
progress = 100 if status == 'completed' else 0 |
|
|
conn.execute(''' |
|
|
UPDATE tasks |
|
|
SET status = ?, progress = ?, summary = ?, error = ?, completed_at = ? |
|
|
WHERE task_id = ? |
|
|
''', (status, progress, summary, error, datetime.now().isoformat(), task_id)) |
|
|
else: |
|
|
query = 'UPDATE tasks SET status = ?' |
|
|
params = [status] |
|
|
|
|
|
if progress is not None: |
|
|
query += ', progress = ?' |
|
|
params.append(progress) |
|
|
|
|
|
query += ' WHERE task_id = ?' |
|
|
params.append(task_id) |
|
|
|
|
|
conn.execute(query, params) |
|
|
|
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
def get_task(task_id): |
|
|
"""Récupère une tâche par son ID""" |
|
|
conn = get_db_connection() |
|
|
task = conn.execute('SELECT * FROM tasks WHERE task_id = ?', (task_id,)).fetchone() |
|
|
conn.close() |
|
|
return task |
|
|
|
|
|
def get_user_tasks(user_session): |
|
|
"""Récupère toutes les tâches d'un utilisateur""" |
|
|
conn = get_db_connection() |
|
|
tasks = conn.execute( |
|
|
'SELECT * FROM tasks WHERE user_session = ? ORDER BY created_at DESC', |
|
|
(user_session,) |
|
|
).fetchall() |
|
|
conn.close() |
|
|
return tasks |
|
|
|
|
|
def delete_old_tasks(): |
|
|
"""Supprime les tâches de plus de 7 jours""" |
|
|
conn = get_db_connection() |
|
|
seven_days_ago = datetime.now().timestamp() - (7 * 24 * 60 * 60) |
|
|
conn.execute(''' |
|
|
DELETE FROM tasks |
|
|
WHERE datetime(created_at) < datetime(?, 'unixepoch') |
|
|
''', (seven_days_ago,)) |
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def allowed_file(filename): |
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS |
|
|
|
|
|
def get_mime_type(filename): |
|
|
ext = filename.rsplit('.', 1)[1].lower() |
|
|
return ALLOWED_EXTENSIONS.get(ext, 'application/octet-stream') |
|
|
|
|
|
def is_youtube_url(url): |
|
|
"""Vérifie si l'URL est une URL YouTube valide""" |
|
|
youtube_regex = r'(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)[\w-]+' |
|
|
return bool(re.match(youtube_regex, url)) |
|
|
|
|
|
def get_session_id(): |
|
|
"""Récupère ou crée un ID de session""" |
|
|
if 'session_id' not in session: |
|
|
session['session_id'] = str(uuid.uuid4()) |
|
|
return session['session_id'] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def process_file_background(task_id, file_path, filename, summary_type): |
|
|
"""Traite un fichier en arrière-plan""" |
|
|
try: |
|
|
update_task_status(task_id, 'processing', progress=10) |
|
|
|
|
|
mime_type = get_mime_type(filename) |
|
|
prompt = SUMMARY_TYPES.get(summary_type, SUMMARY_TYPES['moyen']) |
|
|
|
|
|
update_task_status(task_id, 'processing', progress=30) |
|
|
|
|
|
|
|
|
uploaded_file = client.files.upload(file=file_path) |
|
|
|
|
|
update_task_status(task_id, 'processing', progress=60) |
|
|
|
|
|
|
|
|
response = client.models.generate_content( |
|
|
model="gemini-2.5-flash", |
|
|
contents=[uploaded_file, prompt] |
|
|
) |
|
|
|
|
|
summary = response.text |
|
|
|
|
|
update_task_status(task_id, 'completed', progress=100, summary=summary) |
|
|
|
|
|
except Exception as e: |
|
|
update_task_status(task_id, 'failed', error=str(e)) |
|
|
|
|
|
finally: |
|
|
|
|
|
if os.path.exists(file_path): |
|
|
try: |
|
|
os.remove(file_path) |
|
|
except: |
|
|
pass |
|
|
|
|
|
def process_youtube_background(task_id, youtube_url, summary_type): |
|
|
"""Traite une vidéo YouTube en arrière-plan""" |
|
|
try: |
|
|
update_task_status(task_id, 'processing', progress=10) |
|
|
|
|
|
prompt = SUMMARY_TYPES.get(summary_type, SUMMARY_TYPES['moyen']) |
|
|
|
|
|
update_task_status(task_id, 'processing', progress=30) |
|
|
|
|
|
|
|
|
model_info = client.models.get(model="gemini-2.5-flash") |
|
|
context_window = model_info.input_token_limit |
|
|
|
|
|
update_task_status(task_id, 'processing', progress=50) |
|
|
|
|
|
|
|
|
response = client.models.generate_content( |
|
|
model='gemini-2.5-flash', |
|
|
contents=types.Content( |
|
|
parts=[ |
|
|
types.Part( |
|
|
file_data=types.FileData(file_uri=youtube_url) |
|
|
), |
|
|
types.Part(text=prompt) |
|
|
] |
|
|
) |
|
|
) |
|
|
|
|
|
summary = response.text |
|
|
|
|
|
update_task_status(task_id, 'completed', progress=100, summary=summary) |
|
|
|
|
|
except Exception as e: |
|
|
error_msg = str(e) |
|
|
if 'token' in error_msg.lower() or 'too large' in error_msg.lower(): |
|
|
error_msg = f"La vidéo est trop longue (dépasse la limite de {MAX_VIDEO_TOKENS:,} tokens). Veuillez utiliser une vidéo plus courte." |
|
|
update_task_status(task_id, 'failed', error=error_msg) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_pdf(summary_text, original_filename, summary_type): |
|
|
""" |
|
|
Crée un PDF du résumé avec un formatage amélioré qui interprète |
|
|
la structure (titres, sous-titres, listes) du texte avec support markdown. |
|
|
""" |
|
|
buffer = io.BytesIO() |
|
|
|
|
|
|
|
|
def add_page_number(canvas, doc): |
|
|
canvas.saveState() |
|
|
page_num = canvas.getPageNumber() |
|
|
text = f"Page {page_num}" |
|
|
canvas.setFont('Helvetica', 9) |
|
|
canvas.setFillColor(black) |
|
|
canvas.drawRightString(letter[0] - inch, 0.75 * inch, text) |
|
|
|
|
|
|
|
|
canvas.drawString(inch, 0.75 * inch, clean_filename[:50]) |
|
|
canvas.restoreState() |
|
|
|
|
|
doc = SimpleDocTemplate(buffer, pagesize=letter, |
|
|
leftMargin=inch, rightMargin=inch, |
|
|
topMargin=inch, bottomMargin=inch) |
|
|
|
|
|
|
|
|
styles = getSampleStyleSheet() |
|
|
|
|
|
|
|
|
if 'MainTitle' not in styles: |
|
|
styles.add(ParagraphStyle( |
|
|
name='MainTitle', |
|
|
parent=styles['h1'], |
|
|
fontSize=20, |
|
|
spaceAfter=20, |
|
|
textColor=navy, |
|
|
fontName='Helvetica-Bold', |
|
|
alignment=1 |
|
|
)) |
|
|
|
|
|
if 'SectionTitle' not in styles: |
|
|
styles.add(ParagraphStyle( |
|
|
name='SectionTitle', |
|
|
parent=styles['h2'], |
|
|
fontSize=14, |
|
|
spaceAfter=12, |
|
|
spaceBefore=12, |
|
|
textColor=navy, |
|
|
fontName='Helvetica-Bold' |
|
|
)) |
|
|
|
|
|
if 'SubSectionTitle' not in styles: |
|
|
styles.add(ParagraphStyle( |
|
|
name='SubSectionTitle', |
|
|
parent=styles['Normal'], |
|
|
fontSize=12, |
|
|
spaceAfter=8, |
|
|
spaceBefore=8, |
|
|
fontName='Helvetica-Bold' |
|
|
)) |
|
|
|
|
|
if 'BodyText' not in styles: |
|
|
styles.add(ParagraphStyle( |
|
|
name='BodyText', |
|
|
parent=styles['Normal'], |
|
|
spaceAfter=10, |
|
|
leading=16, |
|
|
alignment=4, |
|
|
fontSize=11 |
|
|
)) |
|
|
|
|
|
if 'ListItem' not in styles: |
|
|
styles.add(ParagraphStyle( |
|
|
name='ListItem', |
|
|
parent=styles['Normal'], |
|
|
leftIndent=20, |
|
|
spaceAfter=6, |
|
|
leading=15, |
|
|
fontSize=11 |
|
|
)) |
|
|
|
|
|
if 'BulletItem' not in styles: |
|
|
styles.add(ParagraphStyle( |
|
|
name='BulletItem', |
|
|
parent=styles['Normal'], |
|
|
leftIndent=25, |
|
|
spaceAfter=6, |
|
|
leading=15, |
|
|
fontSize=11, |
|
|
bulletIndent=10 |
|
|
)) |
|
|
|
|
|
if 'MetaInfo' not in styles: |
|
|
styles.add(ParagraphStyle( |
|
|
name='MetaInfo', |
|
|
parent=styles['Normal'], |
|
|
fontSize=10, |
|
|
textColor=black, |
|
|
spaceAfter=5, |
|
|
alignment=1 |
|
|
)) |
|
|
|
|
|
story = [] |
|
|
|
|
|
|
|
|
if original_filename and original_filename.lower().endswith('.pdf'): |
|
|
clean_filename = original_filename[:-4] |
|
|
else: |
|
|
clean_filename = original_filename or "Document" |
|
|
|
|
|
|
|
|
title = Paragraph(f"Résumé : {clean_filename}", styles['MainTitle']) |
|
|
story.append(title) |
|
|
|
|
|
info_text = f"Type de résumé : {summary_type.capitalize()} | Date : {datetime.now().strftime('%d/%m/%Y %H:%M')}" |
|
|
info = Paragraph(info_text, styles['MetaInfo']) |
|
|
story.append(info) |
|
|
story.append(Spacer(1, 0.4 * inch)) |
|
|
|
|
|
|
|
|
def convert_markdown_to_html(text): |
|
|
"""Convertit le markdown simple en HTML pour ReportLab""" |
|
|
|
|
|
text = text.replace('&', '&').replace('<', '<').replace('>', '>') |
|
|
|
|
|
|
|
|
text = re.sub(r'\*\*\*(.+?)\*\*\*', r'<b><i>\1</i></b>', text) |
|
|
|
|
|
|
|
|
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text) |
|
|
|
|
|
|
|
|
text = re.sub(r'\*(.+?)\*', r'<i>\1</i>', text) |
|
|
|
|
|
return text |
|
|
|
|
|
|
|
|
lines = summary_text.split('\n') |
|
|
|
|
|
for i, line in enumerate(lines): |
|
|
stripped_line = line.strip() |
|
|
|
|
|
|
|
|
if not stripped_line: |
|
|
|
|
|
if i > 0 and i < len(lines) - 1: |
|
|
story.append(Spacer(1, 0.1 * inch)) |
|
|
continue |
|
|
|
|
|
|
|
|
if stripped_line in ['---', '___', '***', '---', '___']: |
|
|
story.append(Spacer(1, 0.15 * inch)) |
|
|
continue |
|
|
|
|
|
|
|
|
match_header = re.match(r'^(#{1,3})\s+(.+)$', stripped_line) |
|
|
if match_header: |
|
|
level = len(match_header.group(1)) |
|
|
content = convert_markdown_to_html(match_header.group(2)) |
|
|
if level == 1: |
|
|
p = Paragraph(content, styles['SectionTitle']) |
|
|
else: |
|
|
p = Paragraph(content, styles['SubSectionTitle']) |
|
|
story.append(p) |
|
|
continue |
|
|
|
|
|
|
|
|
match_bold_title = re.match(r'^\*\*([^*]+)\*\*$', stripped_line) |
|
|
if match_bold_title and len(match_bold_title.group(1)) < 100: |
|
|
content = match_bold_title.group(1).strip() |
|
|
|
|
|
if len(content.split()) < 15: |
|
|
p = Paragraph(content, styles['SectionTitle']) |
|
|
story.append(p) |
|
|
continue |
|
|
|
|
|
|
|
|
match_bullet = re.match(r'^[\*\-]\s+(.+)$', stripped_line) |
|
|
if match_bullet: |
|
|
content = convert_markdown_to_html(match_bullet.group(1)) |
|
|
p = Paragraph(f"• {content}", styles['BulletItem']) |
|
|
story.append(p) |
|
|
continue |
|
|
|
|
|
|
|
|
match_numbered = re.match(r'^(\d+)[\.\)]\s+(.+)$', stripped_line) |
|
|
if match_numbered: |
|
|
num = match_numbered.group(1) |
|
|
content = convert_markdown_to_html(match_numbered.group(2)) |
|
|
p = Paragraph(f"{num}. {content}", styles['ListItem']) |
|
|
story.append(p) |
|
|
continue |
|
|
|
|
|
|
|
|
content = convert_markdown_to_html(stripped_line) |
|
|
p = Paragraph(content, styles['BodyText']) |
|
|
story.append(p) |
|
|
|
|
|
|
|
|
doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number) |
|
|
|
|
|
|
|
|
buffer.seek(0) |
|
|
|
|
|
return buffer |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
return render_template('index.html') |
|
|
|
|
|
@app.route('/upload', methods=['POST']) |
|
|
def upload_file(): |
|
|
user_session = get_session_id() |
|
|
youtube_url = request.form.get('youtube_url', '').strip() |
|
|
summary_type = request.form.get('summary_type', 'moyen') |
|
|
|
|
|
|
|
|
if youtube_url: |
|
|
if not is_youtube_url(youtube_url): |
|
|
return jsonify({'error': 'URL YouTube invalide'}), 400 |
|
|
|
|
|
|
|
|
task_id = create_task( |
|
|
user_session=user_session, |
|
|
filename='Vidéo YouTube', |
|
|
summary_type=summary_type, |
|
|
source_type='youtube', |
|
|
source_path=youtube_url |
|
|
) |
|
|
|
|
|
|
|
|
thread = threading.Thread( |
|
|
target=process_youtube_background, |
|
|
args=(task_id, youtube_url, summary_type) |
|
|
) |
|
|
thread.daemon = True |
|
|
thread.start() |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'task_id': task_id, |
|
|
'message': 'Traitement en cours...' |
|
|
}) |
|
|
|
|
|
|
|
|
if 'file' not in request.files: |
|
|
return jsonify({'error': 'Aucun fichier ou URL YouTube fourni'}), 400 |
|
|
|
|
|
file = request.files['file'] |
|
|
|
|
|
if file.filename == '': |
|
|
return jsonify({'error': 'Nom de fichier vide'}), 400 |
|
|
|
|
|
if not allowed_file(file.filename): |
|
|
return jsonify({'error': 'Type de fichier non supporté'}), 400 |
|
|
|
|
|
filename = secure_filename(file.filename) |
|
|
filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{uuid.uuid4()}_{filename}") |
|
|
file.save(filepath) |
|
|
|
|
|
|
|
|
task_id = create_task( |
|
|
user_session=user_session, |
|
|
filename=filename, |
|
|
summary_type=summary_type, |
|
|
source_type='file', |
|
|
source_path=filepath |
|
|
) |
|
|
|
|
|
|
|
|
thread = threading.Thread( |
|
|
target=process_file_background, |
|
|
args=(task_id, filepath, filename, summary_type) |
|
|
) |
|
|
thread.daemon = True |
|
|
thread.start() |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'task_id': task_id, |
|
|
'message': 'Traitement en cours...' |
|
|
}) |
|
|
|
|
|
@app.route('/task/<task_id>') |
|
|
def get_task_status(task_id): |
|
|
"""Récupère le statut d'une tâche""" |
|
|
task = get_task(task_id) |
|
|
|
|
|
if not task: |
|
|
return jsonify({'error': 'Tâche non trouvée'}), 404 |
|
|
|
|
|
return jsonify({ |
|
|
'task_id': task['task_id'], |
|
|
'filename': task['filename'], |
|
|
'summary_type': task['summary_type'], |
|
|
'status': task['status'], |
|
|
'progress': task['progress'], |
|
|
'summary': task['summary'], |
|
|
'error': task['error'], |
|
|
'created_at': task['created_at'], |
|
|
'completed_at': task['completed_at'] |
|
|
}) |
|
|
|
|
|
@app.route('/tasks') |
|
|
def get_all_tasks(): |
|
|
"""Récupère toutes les tâches de l'utilisateur""" |
|
|
user_session = get_session_id() |
|
|
tasks = get_user_tasks(user_session) |
|
|
|
|
|
task_list = [] |
|
|
for task in tasks: |
|
|
task_list.append({ |
|
|
'task_id': task['task_id'], |
|
|
'filename': task['filename'], |
|
|
'summary_type': task['summary_type'], |
|
|
'status': task['status'], |
|
|
'progress': task['progress'], |
|
|
'summary': task['summary'], |
|
|
'error': task['error'], |
|
|
'created_at': task['created_at'], |
|
|
'completed_at': task['completed_at'] |
|
|
}) |
|
|
|
|
|
return jsonify(task_list) |
|
|
|
|
|
@app.route('/download/<task_id>') |
|
|
def download_pdf(task_id): |
|
|
"""Télécharge le PDF d'une tâche""" |
|
|
task = get_task(task_id) |
|
|
|
|
|
if not task: |
|
|
return jsonify({'error': 'Tâche non trouvée'}), 404 |
|
|
|
|
|
if task['status'] != 'completed': |
|
|
return jsonify({'error': 'Résumé non encore disponible'}), 400 |
|
|
|
|
|
pdf_buffer = create_pdf(task['summary'], task['filename'], task['summary_type']) |
|
|
|
|
|
return send_file( |
|
|
pdf_buffer, |
|
|
mimetype='application/pdf', |
|
|
as_attachment=True, |
|
|
download_name=f"resume_{task['filename']}.pdf" |
|
|
) |
|
|
|
|
|
@app.route('/delete-task/<task_id>', methods=['DELETE']) |
|
|
def delete_task(task_id): |
|
|
"""Supprime une tâche""" |
|
|
user_session = get_session_id() |
|
|
task = get_task(task_id) |
|
|
|
|
|
if not task: |
|
|
return jsonify({'error': 'Tâche non trouvée'}), 404 |
|
|
|
|
|
if task['user_session'] != user_session: |
|
|
return jsonify({'error': 'Non autorisé'}), 403 |
|
|
|
|
|
conn = get_db_connection() |
|
|
conn.execute('DELETE FROM tasks WHERE task_id = ?', (task_id,)) |
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
return jsonify({'success': True}) |
|
|
|
|
|
@app.route('/clear-tasks', methods=['POST']) |
|
|
def clear_all_tasks(): |
|
|
"""Supprime toutes les tâches de l'utilisateur""" |
|
|
user_session = get_session_id() |
|
|
|
|
|
conn = get_db_connection() |
|
|
conn.execute('DELETE FROM tasks WHERE user_session = ?', (user_session,)) |
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
return jsonify({'success': True}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/stats') |
|
|
def system_stats(): |
|
|
"""Affiche les statistiques du système""" |
|
|
conn = get_db_connection() |
|
|
|
|
|
|
|
|
total = conn.execute('SELECT COUNT(*) as count FROM tasks').fetchone()['count'] |
|
|
|
|
|
|
|
|
completed = conn.execute( |
|
|
'SELECT COUNT(*) as count FROM tasks WHERE status = ?', |
|
|
('completed',) |
|
|
).fetchone()['count'] |
|
|
|
|
|
|
|
|
processing = conn.execute( |
|
|
'SELECT COUNT(*) as count FROM tasks WHERE status IN (?, ?)', |
|
|
('pending', 'processing') |
|
|
).fetchone()['count'] |
|
|
|
|
|
|
|
|
failed = conn.execute( |
|
|
'SELECT COUNT(*) as count FROM tasks WHERE status = ?', |
|
|
('failed',) |
|
|
).fetchone()['count'] |
|
|
|
|
|
|
|
|
by_type = conn.execute(''' |
|
|
SELECT summary_type, COUNT(*) as count |
|
|
FROM tasks |
|
|
WHERE status = 'completed' |
|
|
GROUP BY summary_type |
|
|
''').fetchall() |
|
|
|
|
|
|
|
|
by_source = conn.execute(''' |
|
|
SELECT source_type, COUNT(*) as count |
|
|
FROM tasks |
|
|
WHERE status = 'completed' |
|
|
GROUP BY source_type |
|
|
''').fetchall() |
|
|
|
|
|
|
|
|
all_summaries = conn.execute(''' |
|
|
SELECT task_id, filename, summary_type, summary, created_at, completed_at, source_type |
|
|
FROM tasks |
|
|
WHERE status = 'completed' |
|
|
ORDER BY completed_at DESC |
|
|
LIMIT 50 |
|
|
''').fetchall() |
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
html = f'''<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="utf-8"> |
|
|
<title>Statistiques Système</title> |
|
|
</head> |
|
|
<body> |
|
|
<h1>Statistiques du Système</h1> |
|
|
|
|
|
<h2>Vue d'ensemble</h2> |
|
|
<ul> |
|
|
<li>Total de résumés : {total}</li> |
|
|
<li>Résumés réussis : {completed}</li> |
|
|
<li>En cours : {processing}</li> |
|
|
<li>Échoués : {failed}</li> |
|
|
</ul> |
|
|
|
|
|
<h2>Par type de résumé</h2> |
|
|
<ul> |
|
|
''' |
|
|
|
|
|
for row in by_type: |
|
|
html += f' <li>{row["summary_type"].capitalize()} : {row["count"]}</li>\n' |
|
|
|
|
|
if not by_type: |
|
|
html += ' <li>Aucune donnée</li>\n' |
|
|
|
|
|
html += ''' </ul> |
|
|
|
|
|
<h2>Par source</h2> |
|
|
<ul> |
|
|
''' |
|
|
|
|
|
for row in by_source: |
|
|
source_name = 'Fichier' if row['source_type'] == 'file' else 'YouTube' |
|
|
html += f' <li>{source_name} : {row["count"]}</li>\n' |
|
|
|
|
|
if not by_source: |
|
|
html += ' <li>Aucune donnée</li>\n' |
|
|
|
|
|
html += ''' </ul> |
|
|
|
|
|
<hr> |
|
|
|
|
|
<h2>Résumés des utilisateurs (50 derniers)</h2> |
|
|
''' |
|
|
|
|
|
if not all_summaries: |
|
|
html += ' <p>Aucun résumé disponible</p>\n' |
|
|
else: |
|
|
for i, summary in enumerate(all_summaries, 1): |
|
|
source_name = 'Fichier' if summary['source_type'] == 'file' else 'YouTube' |
|
|
created = summary['created_at'][:19] if summary['created_at'] else 'N/A' |
|
|
completed = summary['completed_at'][:19] if summary['completed_at'] else 'N/A' |
|
|
|
|
|
html += f''' |
|
|
<h3>Résumé #{i}</h3> |
|
|
<ul> |
|
|
<li>Fichier : {summary["filename"]}</li> |
|
|
<li>Type : {summary["summary_type"].capitalize()}</li> |
|
|
<li>Source : {source_name}</li> |
|
|
<li>Créé le : {created}</li> |
|
|
<li>Complété le : {completed}</li> |
|
|
</ul> |
|
|
<p><strong>Résumé :</strong></p> |
|
|
<pre>{summary["summary"]}</pre> |
|
|
<hr> |
|
|
''' |
|
|
|
|
|
html += ''' |
|
|
<p><a href="/">Retour à l'accueil</a></p> |
|
|
</body> |
|
|
</html>''' |
|
|
|
|
|
return html |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with app.app_context(): |
|
|
init_db() |
|
|
|
|
|
|
|
|
def cleanup_old_tasks(): |
|
|
while True: |
|
|
time.sleep(86400) |
|
|
delete_old_tasks() |
|
|
|
|
|
cleanup_thread = threading.Thread(target=cleanup_old_tasks) |
|
|
cleanup_thread.daemon = True |
|
|
cleanup_thread.start() |
|
|
|
|
|
if __name__ == '__main__': |
|
|
app.run(debug=True) |