From 85cdd9216afec25c565e77a6076c6b7c2c3f89f2 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 14 Jun 2026 14:54:39 +0000 Subject: [PATCH] Use firm email templates, logo, and header/footer in async UI --- scraibe/email_sender.py | 78 ++++++++++++++++++++++++++++++----------- scraibe/tasks.py | 58 ++++++++++++++++++++++++++---- scraibe/webui.py | 44 +++++++++++++++++------ 3 files changed, 143 insertions(+), 37 deletions(-) diff --git a/scraibe/email_sender.py b/scraibe/email_sender.py index ce5ed56..c58fe18 100644 --- a/scraibe/email_sender.py +++ b/scraibe/email_sender.py @@ -3,6 +3,7 @@ Email sender module for ScrAIbe. Sends transcription outputs (TXT, JSON, etc.) via SMTP. All credentials are configured via environment variables. +Supports both plain text and HTML email bodies. """ import os @@ -51,20 +52,47 @@ def get_email_config(): } +def load_template(template_name: str, **kwargs) -> str: + """ + Load an HTML email template from misc/ and render placeholders. + + Expects files like: + /app/src/misc/upload_notification_template.html + /app/src/misc/success_template.html + /app/src/misc/error_notification_template.html + """ + base = os.getenv("SCRAIBE_TEMPLATES_DIR", "/app/src/misc") + path = os.path.join(base, template_name) + + if not os.path.exists(path): + raise EmailError(f"Email template not found: {path}") + + with open(path, "r", encoding="utf-8") as f: + template = f.read() + + # Replace {placeholder} style variables safely + try: + return template.format(**kwargs) + except KeyError as e: + raise EmailError(f"Missing template variable: {e}") + + def send_email( to: str, subject: str, body: str, + html: Optional[str], attachments: List[str], cc: Optional[str] = None, ) -> bool: """ - Send an email with optional file attachments. + Send an email with optional HTML body and file attachments. Args: to: Comma-separated list of recipient email addresses. subject: Email subject. body: Email body (plain text). + html: Email body (HTML), or None. attachments: List of file paths to attach. cc: Comma-separated list of CC email addresses (optional). @@ -88,34 +116,44 @@ def send_email( raise EmailError("No valid 'To' email addresses provided.") # Build message - msg = MIMEMultipart() + msg = MIMEMultipart("alternative") msg["From"] = cfg["from_address"] msg["To"] = ", ".join(to_list) if cc_list: msg["Cc"] = ", ".join(cc_list) msg["Subject"] = subject + # Attach plain text msg.attach(MIMEText(body, "plain")) - # Attach files - for file_path in attachments: - if not os.path.isfile(file_path): - logger.warning("Attachment file not found, skipping: %s", file_path) - continue + # Attach HTML if provided + if html: + msg.attach(MIMEText(html, "html")) - try: - with open(file_path, "rb") as f: - part = MIMEBase("application", "octet-stream") - part.set_payload(f.read()) - encoders.encode_base64(part) - part.add_header( - "Content-Disposition", - "attachment", - filename=os.path.basename(file_path), - ) - msg.attach(part) - except Exception as e: - logger.warning("Failed to attach file %s: %s", file_path, e) + # Attach files in a separate multipart/mixed part + if attachments: + mixed = MIMEMultipart("mixed") + mixed.attach(msg) + msg = mixed + + for file_path in attachments: + if not os.path.isfile(file_path): + logger.warning("Attachment file not found, skipping: %s", file_path) + continue + + try: + with open(file_path, "rb") as f: + part = MIMEBase("application", "octet-stream") + part.set_payload(f.read()) + encoders.encode_base64(part) + part.add_header( + "Content-Disposition", + "attachment", + filename=os.path.basename(file_path), + ) + msg.attach(part) + except Exception as e: + logger.warning("Failed to attach file %s: %s", file_path, e) # Connect and send try: diff --git a/scraibe/tasks.py b/scraibe/tasks.py index 14f2f74..a441ecc 100644 --- a/scraibe/tasks.py +++ b/scraibe/tasks.py @@ -11,10 +11,13 @@ from datetime import datetime from .celery_app import celery_app from .autotranscript import Scraibe from .misc import setup_logging -from .email_sender import send_email, EmailError +from .email_sender import send_email, EmailError, load_template logger = logging.getLogger("scraibe.tasks") +# Contact email used in templates; can be overridden via env +CONTACT_EMAIL = os.getenv("EMAIL_CONTACT_ADDRESS", "support@example.com") + def get_queue_position(task_id: str) -> int: """ @@ -38,9 +41,11 @@ def get_queue_position(task_id: str) -> int: def send_initial_email(to: str, queue_pos: int): """ - Send initial confirmation email with queue position. + Send initial confirmation email with queue position using HTML template. """ subject = "ScrAIbe: Your transcription request has been received" + + # Build plain-text fallback body = ( "Hello,\n\n" "We have received your audio file for transcription.\n" @@ -55,11 +60,24 @@ def send_initial_email(to: str, queue_pos: int): "\n" "You will receive an email with your transcript (and summary, if requested) " "once processing is complete.\n\n" + "If you have any questions, contact us at " + f"{CONTACT_EMAIL}.\n\n" "This is an automated message from ScrAIbe.\n" ) + # Build HTML using template + html = None try: - send_email(to=to, subject=subject, body=body, attachments=[]) + html = load_template( + "upload_notification_template.html", + queue_position=str(queue_pos) if queue_pos > 0 else "the queue", + contact_email=CONTACT_EMAIL, + ) + except EmailError as e: + logger.warning("Failed to render upload notification template: %s", e) + + try: + send_email(to=to, subject=subject, body=body, html=html, attachments=[]) logger.info("Initial confirmation email sent to %s", to) except EmailError as e: logger.error("Failed to send initial email to %s: %s", to, e) @@ -73,10 +91,11 @@ def send_success_email( task_id: str, ): """ - Send final email with transcript and attachments. + Send final email with transcript and attachments using HTML template. """ subject = "ScrAIbe: Your transcript is ready" + # Build plain-text fallback body = ( "Hello,\n\n" "Your transcription is ready.\n\n" @@ -94,14 +113,27 @@ def send_success_email( body += ( "\n" "Job ID: " + str(task_id) + "\n\n" + "If you have any questions, contact us at " + f"{CONTACT_EMAIL}.\n\n" "This is an automated message from ScrAIbe.\n" ) + # Build HTML using template + html = None + try: + html = load_template( + "success_template.html", + contact_email=CONTACT_EMAIL, + ) + except EmailError as e: + logger.warning("Failed to render success template: %s", e) + try: send_email( to=to, subject=subject, body=body, + html=html, attachments=attachments, ) logger.info("Success email sent to %s for job %s", to, task_id) @@ -111,21 +143,35 @@ def send_success_email( def send_error_email(to: str, error_message: str, task_id: str): """ - Send error notification email. + Send error notification email using HTML template. """ subject = "ScrAIbe: Error with your transcription request" + # Build plain-text fallback body = ( "Hello,\n\n" "We encountered an error while processing your transcription request.\n\n" f"Details: {error_message}\n\n" "Job ID: " + str(task_id) + "\n\n" "Please contact your administrator if the problem persists.\n\n" + "If you have any questions, contact us at " + f"{CONTACT_EMAIL}.\n\n" "This is an automated message from ScrAIbe.\n" ) + # Build HTML using template + html = None try: - send_email(to=to, subject=subject, body=body, attachments=[]) + html = load_template( + "error_notification_template.html", + exception=str(error_message), + contact_email=CONTACT_EMAIL, + ) + except EmailError as e: + logger.warning("Failed to render error template: %s", e) + + try: + send_email(to=to, subject=subject, body=body, html=html, attachments=[]) logger.info("Error email sent to %s for job %s", to, task_id) except EmailError as e: logger.error("Failed to send error email to %s for job %s: %s", to, task_id, e) diff --git a/scraibe/webui.py b/scraibe/webui.py index 3b1cb28..684f6bb 100644 --- a/scraibe/webui.py +++ b/scraibe/webui.py @@ -45,6 +45,20 @@ def load_config(): return config +def load_html_template(path: str, **kwargs) -> str: + """ + Load an HTML template and fill placeholders. + """ + if not os.path.exists(path): + return "" + with open(path, "r", encoding="utf-8") as f: + template = f.read() + try: + return template.format(**kwargs) + except KeyError: + return template + + def create_app(): """ Create and launch the Gradio Web GUI (async mode). @@ -65,20 +79,28 @@ def create_app(): upload_dir = os.getenv("SCRAIBE_UPLOAD_DIR", "/tmp/scraibe_uploads") os.makedirs(upload_dir, exist_ok=True) - # Load header/footer HTML if present + # Paths for assets header_path = layout_cfg.get("header", "/app/src/misc/header.html") footer_path = layout_cfg.get("footer", "/app/src/misc/footer.html") + logo_path = layout_cfg.get("logo", "/app/src/misc/logo.png") + # Prepare header HTML with logo header_html = "" + if os.path.exists(header_path): + header_html = load_html_template( + header_path, + header_logo_url="https://www.apstrom.de/", + header_logo_src=logo_path if os.path.exists(logo_path) else "", + ) + + # Prepare footer HTML footer_html = "" - - if header_path and os.path.exists(header_path): - with open(header_path, "r", encoding="utf-8") as f: - header_html = f.read() - - if footer_path and os.path.exists(footer_path): - with open(footer_path, "r", encoding="utf-8") as f: - footer_html = f.read() + if os.path.exists(footer_path): + version = os.getenv("SCRABIE_VERSION", "0.1.1.dev") + footer_html = load_html_template( + footer_path, + footer_scraibe_webui_version=version, + ) # Build Gradio interface with gr.Blocks( @@ -225,12 +247,12 @@ def create_app(): # Launch options server_name = launch_cfg.get("server_name", os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")) server_port = launch_cfg.get("server_port", 7860) - favicon_path = launch_cfg.get("favicon_path", "/app/src/misc/logo.png") + favicon_path = logo_path if os.path.exists(logo_path) else None app.launch( server_name=str(server_name), server_port=int(server_port), - favicon_path=favicon_path if os.path.exists(favicon_path) else None, + favicon_path=favicon_path, css="body { font-family: Arial, sans-serif; }", )