pierreguillou's picture
Update app.py
aef2057 verified
raw
history blame
22.5 kB
import gradio as gr
import os
import time
import requests
from datetime import datetime
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage
from langchain_core.caches import BaseCache
from langchain_core.callbacks import Callbacks
ChatGoogleGenerativeAI.model_rebuild()
import PyPDF2
import docx
import pandas as pd
from pptx import Presentation
import io
import tempfile
from urllib.parse import urlparse
import re
# Import et rebuild de ChatGoogleGenerativeAI en différé
try:
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.caches import BaseCache
ChatGoogleGenerativeAI.model_rebuild()
except Exception as e:
print(f"Avertissement lors du rebuild: {e}")
from langchain_google_genai import ChatGoogleGenerativeAI
# Configuration des modèles
MODELS = {
"Gemini 2.5 Flash (Google AI)": {
"provider": "Google AI",
"class": ChatGoogleGenerativeAI,
"model_name": "gemini-2.0-flash-exp",
"default_api": True
},
"ChatGPT 5 (OpenAI)": {
"provider": "OpenAI",
"class": ChatOpenAI,
"model_name": "gpt-4o",
"default_api": False
},
"Claude Sonnet 4 (Anthropic)": {
"provider": "Anthropic",
"class": ChatAnthropic,
"model_name": "claude-3-5-sonnet-20241022",
"default_api": False
},
"Gemini 2.5 Pro (Google AI)": {
"provider": "Google AI",
"class": ChatGoogleGenerativeAI,
"model_name": "gemini-2.0-flash-exp",
"default_api": False
}
}
# API par défaut pour Gemini 2.5 Flash via HF Spaces Secrets
DEFAULT_GEMINI_API = os.getenv("FLASH_GOOGLE_API_KEY")
def extract_text_from_file(file):
"""Extrait le texte d'un fichier uploadé"""
if file is None:
return ""
file_extension = os.path.splitext(file.name)[1].lower()
try:
if file_extension == '.pdf':
with open(file.name, 'rb') as f:
reader = PyPDF2.PdfReader(f)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n"
return text
elif file_extension == '.docx':
doc = docx.Document(file.name)
text = ""
for paragraph in doc.paragraphs:
text += paragraph.text + "\n"
return text
elif file_extension == '.txt':
with open(file.name, 'r', encoding='utf-8') as f:
return f.read()
elif file_extension in ['.xlsx', '.xls']:
df = pd.read_excel(file.name)
return df.to_string()
elif file_extension == '.pptx':
prs = Presentation(file.name)
text = ""
for slide in prs.slides:
for shape in slide.shapes:
if hasattr(shape, "text"):
text += shape.text + "\n"
return text
else:
return "Format de fichier non supporté"
except Exception as e:
return f"Erreur lors de la lecture du fichier: {str(e)}"
def extract_text_from_url(url):
"""Extrait le texte d'une URL"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
content = response.text
content = re.sub(r'<[^>]+>', '', content)
content = re.sub(r'\s+', ' ', content).strip()
return content[:10000] # Limite à 10k caractères
except Exception as e:
return f"Erreur lors de la récupération de l'URL: {str(e)}"
def get_document_content(text_input, url_input, file_input):
"""Récupère le contenu du document selon la source"""
if text_input.strip():
return text_input.strip()
elif url_input.strip():
return extract_text_from_url(url_input.strip())
elif file_input is not None:
return extract_text_from_file(file_input)
else:
return ""
def create_llm_instance(model_name, api_key):
"""Crée une instance du modèle LLM"""
model_config = MODELS[model_name]
if model_config["provider"] == "OpenAI":
return model_config["class"](
model=model_config["model_name"],
api_key=api_key,
temperature=0.7
)
elif model_config["provider"] == "Anthropic":
return model_config["class"](
model=model_config["model_name"],
api_key=api_key,
temperature=0.7
)
elif model_config["provider"] == "Google AI":
api_to_use = api_key if api_key else DEFAULT_GEMINI_API
return model_config["class"](
model=model_config["model_name"],
google_api_key=api_to_use,
temperature=0.7
)
def generate_html(model_name, api_key, text_input, url_input, file_input):
"""Génère le fichier HTML éducatif"""
start_time = time.time()
if model_name != "Gemini 2.5 Flash (Google AI)" and not api_key.strip():
return None, "❌ Erreur: Veuillez fournir une clé API pour ce modèle.", 0
document_content = get_document_content(text_input, url_input, file_input)
if not document_content:
return None, "❌ Erreur: Veuillez fournir un document (texte, URL ou fichier).", 0
try:
# Création de l'instance LLM
llm = create_llm_instance(model_name, api_key)
# Lecture du prompt template
with open("creation_educational_html_from_any_document_18082025.txt", "r", encoding="utf-8") as f:
prompt_template = f.read()
# Remplacement des variables
model_config = MODELS[model_name]
prompt = prompt_template.format(
model_name=model_config["model_name"],
provider_name=model_config["provider"],
document=document_content
)
# Génération du contenu
message = HumanMessage(content=prompt)
response = llm.invoke([message])
html_content = response.content
# Nettoyage des éventuelles balises de code des modèles
html_content = html_content.replace("```html", "")
html_content = html_content.replace("```", "")
# Calcul du temps de génération
generation_time = time.time() - start_time
# Sauvegarde du fichier HTML
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"document_educatif_{timestamp}.html"
with open(filename, "w", encoding="utf-8") as f:
f.write(html_content)
success_message = f"✅ Fichier HTML généré avec succès en {generation_time:.2f} secondes!"
return filename, success_message, generation_time
except Exception as e:
error_message = f"❌ Erreur lors de la génération: {str(e)}"
return None, error_message, 0
def reset_form():
"""Remet à zéro le formulaire"""
return (
"Gemini 2.5 Flash (Google AI)", # model_name
"", # api_key
"", # text_input
"", # url_input
None, # file_input
"", # status_message
None, # html_file
"" # html_preview
)
def update_api_info(model_name):
"""Met à jour les informations sur l'API selon le modèle sélectionné"""
if model_name == "Gemini 2.5 Flash (Google AI)":
return gr.update(
label="Clé API (optionnelle)",
placeholder="API gratuite disponible jusqu'à épuisement, ou utilisez votre propre clé",
info="💡 Une API gratuite est déjà configurée pour ce modèle. Vous pouvez utiliser votre propre clé si vous le souhaitez."
)
else:
return gr.update(
label="Clé API (obligatoire)",
placeholder="Entrez votre clé API",
info="🔑 Clé API requise pour ce modèle"
)
# Interface Gradio (Apple-like)
with gr.Blocks(
title="EduHTML Creator - Générateur de contenu éducatif HTML",
theme=gr.themes.Soft(),
css="""
/* =============== Apple-inspired Global Reset & Typography =============== */
:root {
--apple-black: #000000;
--apple-white: #ffffff;
--apple-grey-50: #f5f5f7;
--apple-grey-100: #efeff4; /* iOS group background */
--apple-grey-200: #e5e5ea;
--apple-grey-300: #d1d1d6;
--apple-grey-600: #8e8e93;
--apple-blue: #007aff;
--apple-blue-hover: #0056cc;
--apple-green: #34c759;
--apple-red: #ff3b30;
--shadow-soft: 0 8px 30px rgba(0,0,0,0.08);
--radius-lg: 14px;
--radius-md: 12px;
--radius-sm: 10px;
--transition: all 0.3s ease;
--text-color: #1d1d1f;
--link-underline-offset: 2px;
}
* { box-sizing: border-box; }
html, body {
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: var(--text-color);
background: var(--apple-grey-50);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* =============== Layout Containers =============== */
.main-container {
max-width: 1100px;
margin: 0 auto;
padding: 24px;
}
/* Fixed top nav (subtle, Apple-like) */
.top-nav {
position: sticky;
top: 0;
z-index: 1000;
backdrop-filter: saturate(180%) blur(12px);
background: rgba(250,250,252,0.72);
border-bottom: 1px solid rgba(0,0,0,0.06);
}
.top-nav-inner {
max-width: 1100px;
margin: 0 auto;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
letter-spacing: 0.2px;
}
.brand .dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--apple-blue);
box-shadow: 0 0 0 4px rgba(0,122,255,0.15);
}
.nav-actions .burger {
width: 36px; height: 36px;
border-radius: 9px;
border: 1px solid rgba(0,0,0,0.08);
display: grid; place-items: center;
background: var(--apple-white);
box-shadow: var(--shadow-soft);
cursor: pointer;
transition: var(--transition);
}
.nav-actions .burger:hover { transform: translateY(-1px); }
/* Inline burger menu panel (local help/actions) */
.inline-menu {
position: fixed;
top: 64px;
right: 16px;
width: 260px;
background: var(--apple-white);
border: 1px solid rgba(0,0,0,0.06);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
padding: 12px;
opacity: 0;
pointer-events: none;
transform: translateY(-8px);
transition: var(--transition);
}
.inline-menu.open {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.inline-menu h4 {
margin: 6px 0 10px 0;
font-size: 14px;
font-weight: 700;
}
.inline-menu a {
display: block;
padding: 8px 10px;
border-radius: 10px;
text-decoration: underline;
text-underline-offset: var(--link-underline-offset);
color: var(--text-color);
}
.inline-menu a:hover { background: var(--apple-grey-100); }
/* =============== Header (full-width black) =============== */
.header {
margin: 18px 0 28px 0;
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--apple-black);
color: #ffffff !important; /* Force texte blanc */
box-shadow: var(--shadow-soft);
}
.header, .header * {
color: #ffffff !important; /* Tout le contenu en blanc */
}
.header a, .header a:visited, .header a:active {
color: #ffffff !important; /* Liens blancs */
text-decoration: underline;
text-underline-offset: var(--link-underline-offset);
}
.header-inner {
padding: 48px 32px;
text-align: center;
}
.header h1 {
margin: 0;
font-weight: 700;
letter-spacing: -0.02em;
font-size: clamp(28px, 3.5vw, 40px);
}
.header p {
opacity: 0.92;
font-size: 17px;
line-height: 1.5;
margin: 12px auto 0;
max-width: 760px;
}
/* =============== Section Cards (alternating backgrounds) =============== */
.section {
background: var(--apple-white);
border: 1px solid rgba(0,0,0,0.06);
border-radius: var(--radius-lg);
padding: 20px;
box-shadow: var(--shadow-soft);
}
.section + .section { margin-top: 16px; }
/* Alternate section – subtle grey Apple background */
.section.alt {
background: var(--apple-grey-100);
}
/* Labels/titles inside sections */
.section h3, .section h2, .section h4 {
margin-top: 0;
letter-spacing: -0.01em;
}
.section h3 {
font-size: 18px;
font-weight: 700;
margin-bottom: 12px;
}
/* =============== Inputs & Controls =============== */
input[type="text"], input[type="password"], textarea {
border-radius: var(--radius-md) !important;
border: 1px solid rgba(0,0,0,0.08) !important;
background: var(--apple-white) !important;
transition: var(--transition) !important;
box-shadow: 0 1px 0 rgba(0,0,0,0.02) inset;
}
input:focus, textarea:focus {
outline: none !important;
border-color: var(--apple-blue) !important;
box-shadow: 0 0 0 3px rgba(0,122,255,0.15) !important;
}
/* Buttons */
.apple-button, .reset-button, .ghost-button {
display: inline-flex;
align-items: center;
gap: 8px;
height: 44px;
padding: 0 18px;
border-radius: 12px;
border: 1px solid transparent;
cursor: pointer;
font-weight: 600;
letter-spacing: 0.2px;
transition: var(--transition);
user-select: none;
}
.apple-button {
background: var(--apple-blue);
color: #fff;
box-shadow: 0 6px 18px rgba(0,122,255,0.25);
}
.apple-button:hover { background: var(--apple-blue-hover); transform: translateY(-1px); }
.apple-button:active { transform: translateY(0); }
.reset-button {
background: var(--apple-red);
color: #fff;
box-shadow: 0 6px 18px rgba(255,59,48,0.22);
}
.reset-button:hover { filter: brightness(0.96); transform: translateY(-1px); }
.reset-button:active { transform: translateY(0); }
/* Secondary ghost button style if needed */
.ghost-button {
background: var(--apple-white);
color: var(--text-color);
border: 1px solid rgba(0,0,0,0.08);
}
.ghost-button:hover { background: #fafafa; }
/* Status messages */
.status-success { color: var(--apple-green); font-weight: 600; }
.status-error { color: var(--apple-red); font-weight: 600; }
/* =============== Download & Preview =============== */
.preview-card {
background: var(--apple-white);
border: 1px solid rgba(0,0,0,0.06);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.preview-header {
background: linear-gradient(180deg, #fafafa, #f2f2f7);
border-bottom: 1px solid rgba(0,0,0,0.06);
padding: 12px 16px;
display: flex; align-items: center; gap: 8px;
}
.preview-dot {
width: 10px; height: 10px; border-radius: 50%;
background: #ff5f57; box-shadow: 16px 0 0 #ffbd2e, 32px 0 0 #28c840;
}
.preview-body {
padding: 16px;
max-height: 520px;
overflow: auto;
background: var(--apple-grey-50);
}
.preview-body pre, .preview-body code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
}
/* =============== Footer (full-width black) =============== */
.footer {
margin-top: 22px;
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--apple-black);
color: #ffffff !important; /* Force texte blanc */
box-shadow: var(--shadow-soft);
}
.footer, .footer * {
color: #ffffff !important; /* Tout le contenu en blanc */
}
.footer a, .footer a:visited, .footer a:active {
color: #ffffff !important; /* Liens blancs */
text-decoration: underline;
text-underline-offset: var(--link-underline-offset);
}
.footer-inner {
padding: 18px 20px;
font-size: 14px;
line-height: 1.5;
}
/* Links globally */
a { text-decoration: underline; text-underline-offset: var(--link-underline-offset); }
/* Hover animations */
.section, .preview-card, .header, .footer {
transition: var(--transition);
}
.section:hover, .preview-card:hover {
transform: translateY(-2px);
}
/* Responsive */
@media (max-width: 768px) {
.top-nav-inner { padding: 0 12px; }
.header-inner { padding: 36px 20px; }
.preview-body { max-height: 420px; }
}
"""
) as app:
# Top navigation (sticky) + burger menu
gr.HTML("""
<div class='top-nav' role='navigation' aria-label='Navigation principale'>
<div class='top-nav-inner'>
<div class='brand'>
<span class='dot' aria-hidden='true'></span>
<span>EduHTML Creator</span>
</div>
<div class='nav-actions'>
<button class='burger' aria-label='Menu' id='inlineMenuBtn'>
<svg width='18' height='18' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg' aria-hidden='true'>
<path d='M3 6h18M3 12h18M3 18h18' stroke='#1d1d1f' stroke-width='2' stroke-linecap='round'/>
</svg>
</button>
</div>
</div>
<div class='inline-menu' id='inlineMenu' aria-hidden='true'>
<h4>Actions rapides</h4>
<a href='#' id='scrollTopLink'>Revenir en haut</a>
<a href='https://www.apple.com' target='_blank' rel='noopener'>Inspiration Apple</a>
</div>
</div>
""")
# Header hero (black, full-width look within container)
gr.HTML("""
<div class="header" role="banner">
<div class="header-inner">
<h1>🎓 EduHTML Creator</h1>
<p>
Transformez n'importe quel document en contenu éducatif HTML interactif, avec un design premium inspiré d'Apple.
Fidélité au document, structuration claire, interactivité, et mise en valeur des informations clés.
</p>
</div>
</div>
""")
with gr.Row(elem_classes=["main-container"]):
with gr.Column(scale=1):
gr.HTML("<div class='section'>")
model_dropdown = gr.Dropdown(
choices=list(MODELS.keys()),
value="Gemini 2.5 Flash (Google AI)",
label="Modèle LLM",
info="Sélectionnez le modèle à utiliser pour la génération"
)
api_input = gr.Textbox(
label="Clé API (optionnelle)",
placeholder="API gratuite (Gemini Flash) disponible. Vous pouvez entrer votre propre clé.",
info="Pour OpenAI/Anthropic, une clé est obligatoire.",
type="password"
)
gr.HTML("</div>")
gr.HTML("<div class='section alt'>")
gr.HTML("<h3>Source du document</h3>")
text_input = gr.Textbox(
label="Texte copié/collé",
placeholder="Collez votre texte ici...",
lines=6
)
url_input = gr.Textbox(
label="Lien Web",
placeholder="https://exemple.com/article"
)
file_input = gr.File(
label="Fichier",
file_types=[".pdf", ".txt", ".docx", ".xlsx", ".xls", ".pptx"]
)
gr.HTML("</div>")
with gr.Row():
submit_btn = gr.Button("Générer le HTML", variant="primary", elem_classes=["apple-button"])
reset_btn = gr.Button("Reset", elem_classes=["reset-button"])
with gr.Column(scale=1):
status_output = gr.HTML(label="Statut")
gr.HTML("<div class='section preview-card'>")
gr.HTML("<div class='preview-header'><div class='preview-dot' aria-hidden='true'></div><div>Prévisualisation</div></div>")
html_preview = gr.HTML(label="Prévisualisation", visible=False, elem_id="html-preview", elem_classes=["preview-body"])
html_file_output = gr.File(label="Fichier HTML téléchargeable", visible=False)
gr.HTML("</div>")
# Footer (black)
gr.HTML("""
<div class="footer" role="contentinfo">
<div class="footer-inner">
<span>Design inspiré d'Apple • Contrastes élevés • Interactions fluides</span>
</div>
</div>
""")
# Light JS: smooth scroll to top, inline burger, focus handling
gr.HTML("""
<script>
(function() {
const menuBtn = document.getElementById('inlineMenuBtn');
const menu = document.getElementById('inlineMenu');
const topLink = document.getElementById('scrollTopLink');
function closeMenu() {
if (!menu) return;
menu.classList.remove('open');
menu.setAttribute('aria-hidden', 'true');
}
if (menuBtn && menu) {
menuBtn.addEventListener('click', function(e) {
e.preventDefault();
const isOpen = menu.classList.contains('open');
if (isOpen) {
closeMenu();
} else {
menu.classList.add('open');
menu.setAttribute('aria-hidden', 'false');
}
});
}
if (topLink) {
topLink.addEventListener('click', function(e) {
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
closeMenu();
});
}
// Close when clicking outside
document.addEventListener('click', function(e) {
if (!menu || !menuBtn) return;
if (!menu.contains(e.target) && !menuBtn.contains(e.target)) {
closeMenu();
}
});
// Accessibility: close on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeMenu();
});
})();
</script>
""")
# Événements
model_dropdown.change(
fn=update_api_info,
inputs=[model_dropdown],
outputs=[api_input]
)
submit_btn.click(
fn=generate_html,
inputs=[model_dropdown, api_input, text_input, url_input, file_input],
outputs=[html_file_output, status_output, gr.State()]
).then(
fn=lambda file, status, _: (
gr.update(visible=file is not None),
status,
gr.update(visible=file is not None, value=(open(file, 'r', encoding='utf-8').read() if file else ""))
),
inputs=[html_file_output, status_output, gr.State()],
outputs=[html_file_output, status_output, html_preview]
)
reset_btn.click(
fn=reset_form,
outputs=[model_dropdown, api_input, text_input, url_input, file_input, status_output, html_file_output, html_preview]
)
if __name__ == "__main__":
app.launch(
server_name="0.0.0.0",
server_port=7860,
share=True
)