pierreguillou's picture
Update app.py
4661970 verified
raw
history blame
24.4 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 pandas as pd
import io
import tempfile
from urllib.parse import urlparse
import re
# Import DocLing and necessary configuration classes
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.datamodel.base_models import InputFormat
# Import and rebuild ChatGoogleGenerativeAI deferred
try:
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.caches import BaseCache
ChatGoogleGenerativeAI.model_rebuild()
except Exception as e:
print(f"Warning during rebuild: {e}")
from langchain_google_genai import ChatGoogleGenerativeAI
# --- START OF OCR CONFIGURATION ---
# Create a single, pre-configured DocumentConverter instance to be reused.
# This is more efficient than creating it on every function call.
# 1. Define the pipeline options to enable OCR for PDFs.
# Configure a single global DocLing converter with Tesseract OCR enabled and all languages
# Note: With tesseract-ocr-all installed, all language data files are available.
pdf_options = PdfPipelineOptions(
do_ocr=True,
ocr_model="tesseract",
# Provide a broad default set. With tesseract-ocr-all, many language packs exist.
# You can keep this small for speed or expand it. Here we include a practical wide set.
ocr_languages=[
"eng","fra","deu","spa","ita","por","nld","pol","tur","ces","rus","ukr","ell","ron","hun",
"bul","hrv","srp","slk","slv","lit","lav","est","cat","eus","glg","isl","dan","nor","swe",
"fin","alb","mlt","afr","zul","swa","amh","uzb","aze","kaz","kir","mon","tgl","ind","msa",
"tha","vie","khm","lao","mya","ben","hin","mar","guj","pan","mal","tam","tel","kan","nep",
"sin","urd","fas","pus","kur","aze_cyrl","tat","uig","heb","ara","yid","grc","chr","epo",
"hye","kat","kat_old","aze_latn","mkd","bel","srp_latn","srp_cyrillic",
# CJK — these are heavier and slower; include only if needed:
"chi_sim","chi_tra","jpn","kor"
]
)
# 2. Create the format-specific configuration.
format_options = {
InputFormat.PDF: PdfFormatOption(pipeline_options=pdf_options)
}
# 3. Initialize the converter with the OCR configuration.
# This converter will now automatically perform OCR on any PDF file.
docling_converter = DocumentConverter(format_options=format_options)
# --- END OF OCR CONFIGURATION ---
# Model configuration
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
}
}
# Default API for Gemini 2.5 Flash via HF Spaces Secrets
DEFAULT_GEMINI_API = os.getenv("FLASH_GOOGLE_API_KEY")
def extract_text_from_file(file):
"""
Extract text from an uploaded file or path (str).
- Accepts an object with .name attribute (e.g. Gradio upload) OR a file path (str).
- DocLing for: .pdf (Tesseract OCR enabled if configured), .docx, .xlsx, .pptx
- Converts .csv /.xls -> temporary .xlsx then DocLing
- .txt read directly
"""
if file is None:
return ""
# Normalize to a filesystem path string
path = file.name if hasattr(file, "name") else str(file)
ext = os.path.splitext(path)[1].lower()
docling_direct = {".pdf", ".docx", ".xlsx", ".pptx"}
to_xlsx_first = {".csv", ".xls"}
try:
if ext in docling_direct:
result = docling_converter.convert(path)
return result.document.export_to_markdown()
elif ext in to_xlsx_first:
# Convert CSV/XLS -> XLSX
if ext == ".csv":
df = pd.read_csv(path)
else: # .xls
df = pd.read_excel(path)
with tempfile.NamedTemporaryFile(delete=True, suffix=".xlsx") as tmp:
df.to_excel(tmp.name, index=False)
result = docling_converter.convert(tmp.name)
return result.document.export_to_markdown()
elif ext == ".txt":
with open(path, "r", encoding="utf-8") as f:
return f.read()
else:
return "Unsupported file format"
except Exception as e:
return f"Error reading file: {str(e)}"
def extract_text_from_url(url):
"""Extract text from a 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] # Limit to 10k characters
except Exception as e:
return f"Error retrieving URL: {str(e)}"
def get_document_content(text_input, url_input, file_input):
"""Retrieve document content based on 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):
"""Create an LLM model instance"""
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):
"""Generate educational HTML file"""
start_time = time.time()
if model_name != "Gemini 2.5 Flash (Google AI)" and not api_key.strip():
return None, "❌ Error: Please provide an API key for this model.", 0
document_content = get_document_content(text_input, url_input, file_input)
if not document_content:
return None, "❌ Error: Please provide a document (text, URL or file).", 0
try:
# Create LLM instance
llm = create_llm_instance(model_name, api_key)
# Read prompt template
with open("creation_educational_html_from_any_document_18082025.txt", "r", encoding="utf-8") as f:
prompt_template = f.read()
# Replace variables
model_config = MODELS[model_name]
prompt = prompt_template.format(
model_name=model_config["model_name"],
provider_name=model_config["provider"],
document=document_content
)
# Generate content
message = HumanMessage(content=prompt)
response = llm.invoke([message])
html_content = response.content
# Clean any code tags from models
html_content = html_content.replace("```html", "")
html_content = html_content.replace("```", "")
# Calculate generation time
generation_time = time.time() - start_time
# Save HTML file
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"educational_document_{timestamp}.html"
with open(filename, "w", encoding="utf-8") as f:
f.write(html_content)
success_message = f"✅ HTML file generated successfully in {generation_time:.2f} seconds!"
return filename, success_message, generation_time
except Exception as e:
error_message = f"❌ Error during generation: {str(e)}"
return None, error_message, 0
def reset_form():
"""Reset the form to zero"""
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):
"""Update API information based on selected model"""
if model_name == "Gemini 2.5 Flash (Google AI)":
return gr.update(
label="API Key (optional)",
placeholder="Free API available until exhausted, or use your own key",
info="💡 A free API is already configured for this model. You can use your own key if you wish."
)
else:
return gr.update(
label="API Key (required)",
placeholder="Enter your API key",
info="🔑 API key required for this model"
)
# Gradio Interface (Apple-like)
with gr.Blocks(
title="EduHTML Creator - Educational HTML Content Generator",
theme=gr.themes.Soft(),
css="""
/* ==== Apple-inspired Global Reset & Typography ==== */
:root {
--apple-black: #0000;
--apple-white: #ffff;
--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: #ffff !important; /* Force white text */
box-shadow: var(--shadow-soft);
}
.header, .header * {
color: #ffff !important; /* All content in white */
}
.header a, .header a:visited, .header a:active {
color: #ffff !important; /* White links */
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; }
/* ==== Tabs Styling (Apple-like) ==== */
.tab-nav {
background: var(--apple-grey-100);
border-radius: var(--radius-md);
padding: 4px;
display: flex;
gap: 2px;
margin-bottom: 16px;
}
.tab-nav button {
flex: 1;
padding: 8px 16px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
color: var(--apple-grey-600);
font-weight: 500;
font-size: 14px;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.tab-nav button.selected {
background: var(--apple-white);
color: var(--text-color);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
font-weight: 600;
}
.tab-nav button:hover:not(.selected) {
color: var(--text-color);
background: rgba(255,255,255,0.5);
}
/* Tab content */
.tab-content {
min-height: 120px;
}
/* ==== 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: #ffff !important; /* Force white text */
box-shadow: var(--shadow-soft);
}
.footer, .footer * {
color: #ffff !important; /* All content in white */
}
.footer a, .footer a:visited, .footer a:active {
color: #ffff !important; /* White links */
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:
# Header hero (black, full-width look within container)
gr.HTML("""
<div class="header" role="banner">
<div class="header-inner">
<h1>🎓 EduHTML Creator</h1>
<p>
Transform any document into interactive educational HTML content, with a premium Apple-inspired design.
Document fidelity, clear structure, interactivity, and highlighting of key information.
</p>
</div>
</div>
""")
with gr.Column(elem_classes=["main-container"]):
# Model Configuration Section
gr.HTML("<div class='section'>")
model_dropdown = gr.Dropdown(
choices=list(MODELS.keys()),
value="Gemini 2.5 Flash (Google AI)",
label="LLM Model",
info="Select the model to use for generation"
)
api_input = gr.Textbox(
label="API Key (optional)",
placeholder="Free API (Gemini Flash) available. You can enter your own key.",
info="For OpenAI/Anthropic, a key is required.",
type="password"
)
gr.HTML("</div>")
# Document Source Section with tabs
gr.HTML("<div class='section alt'>")
gr.HTML("<h3>Document Source</h3>")
with gr.Tabs():
with gr.TabItem("📝 Text"):
text_input = gr.Textbox(
label="Copied/pasted text",
placeholder="Paste your text here...",
lines=4
)
with gr.TabItem("🌐 URL"):
url_input = gr.Textbox(
label="Web Link",
placeholder="https://example.com/article"
)
with gr.TabItem("📁 File"):
file_input = gr.File(
label="File",
file_types=[".pdf", ".txt", ".docx", ".xlsx", ".xls", ".pptx"]
)
gr.HTML("</div>")
# Action buttons
with gr.Row():
submit_btn = gr.Button("Generate HTML", variant="primary", elem_classes=["apple-button"])
reset_btn = gr.Button("Reset", elem_classes=["reset-button"])
# Results Section
status_output = gr.HTML(label="Status")
gr.HTML("<div class='section preview-card'>")
gr.HTML("<div class='preview-header'><div class='preview-dot' aria-hidden='true'></div><div>Preview</div></div>")
html_preview = gr.HTML(label="Preview", visible=False, elem_id="html-preview", elem_classes=["preview-body"])
html_file_output = gr.File(label="Downloadable HTML file", visible=False)
gr.HTML("</div>")
# Footer (black)
gr.HTML("""
<div class="footer" role="contentinfo">
<div class="footer-inner">
<span>Apple-inspired design • High contrasts • Smooth interactions</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>
""")
# Events
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
)