Use firm email templates, logo, and header/footer in async UI
Mirror and run GitLab CI / build (push) Has been cancelled
Ruff / ruff (push) Has been cancelled

This commit is contained in:
admin
2026-06-14 14:54:39 +00:00
parent 2803c81b44
commit 85cdd9216a
3 changed files with 143 additions and 37 deletions
+58 -20
View File
@@ -3,6 +3,7 @@ Email sender module for ScrAIbe.
Sends transcription outputs (TXT, JSON, etc.) via SMTP. Sends transcription outputs (TXT, JSON, etc.) via SMTP.
All credentials are configured via environment variables. All credentials are configured via environment variables.
Supports both plain text and HTML email bodies.
""" """
import os 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( def send_email(
to: str, to: str,
subject: str, subject: str,
body: str, body: str,
html: Optional[str],
attachments: List[str], attachments: List[str],
cc: Optional[str] = None, cc: Optional[str] = None,
) -> bool: ) -> bool:
""" """
Send an email with optional file attachments. Send an email with optional HTML body and file attachments.
Args: Args:
to: Comma-separated list of recipient email addresses. to: Comma-separated list of recipient email addresses.
subject: Email subject. subject: Email subject.
body: Email body (plain text). body: Email body (plain text).
html: Email body (HTML), or None.
attachments: List of file paths to attach. attachments: List of file paths to attach.
cc: Comma-separated list of CC email addresses (optional). cc: Comma-separated list of CC email addresses (optional).
@@ -88,34 +116,44 @@ def send_email(
raise EmailError("No valid 'To' email addresses provided.") raise EmailError("No valid 'To' email addresses provided.")
# Build message # Build message
msg = MIMEMultipart() msg = MIMEMultipart("alternative")
msg["From"] = cfg["from_address"] msg["From"] = cfg["from_address"]
msg["To"] = ", ".join(to_list) msg["To"] = ", ".join(to_list)
if cc_list: if cc_list:
msg["Cc"] = ", ".join(cc_list) msg["Cc"] = ", ".join(cc_list)
msg["Subject"] = subject msg["Subject"] = subject
# Attach plain text
msg.attach(MIMEText(body, "plain")) msg.attach(MIMEText(body, "plain"))
# Attach files # Attach HTML if provided
for file_path in attachments: if html:
if not os.path.isfile(file_path): msg.attach(MIMEText(html, "html"))
logger.warning("Attachment file not found, skipping: %s", file_path)
continue
try: # Attach files in a separate multipart/mixed part
with open(file_path, "rb") as f: if attachments:
part = MIMEBase("application", "octet-stream") mixed = MIMEMultipart("mixed")
part.set_payload(f.read()) mixed.attach(msg)
encoders.encode_base64(part) msg = mixed
part.add_header(
"Content-Disposition", for file_path in attachments:
"attachment", if not os.path.isfile(file_path):
filename=os.path.basename(file_path), logger.warning("Attachment file not found, skipping: %s", file_path)
) continue
msg.attach(part)
except Exception as e: try:
logger.warning("Failed to attach file %s: %s", file_path, e) 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 # Connect and send
try: try:
+52 -6
View File
@@ -11,10 +11,13 @@ from datetime import datetime
from .celery_app import celery_app from .celery_app import celery_app
from .autotranscript import Scraibe from .autotranscript import Scraibe
from .misc import setup_logging 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") 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: 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): 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" subject = "ScrAIbe: Your transcription request has been received"
# Build plain-text fallback
body = ( body = (
"Hello,\n\n" "Hello,\n\n"
"We have received your audio file for transcription.\n" "We have received your audio file for transcription.\n"
@@ -55,11 +60,24 @@ def send_initial_email(to: str, queue_pos: int):
"\n" "\n"
"You will receive an email with your transcript (and summary, if requested) " "You will receive an email with your transcript (and summary, if requested) "
"once processing is complete.\n\n" "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" "This is an automated message from ScrAIbe.\n"
) )
# Build HTML using template
html = None
try: 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) logger.info("Initial confirmation email sent to %s", to)
except EmailError as e: except EmailError as e:
logger.error("Failed to send initial email to %s: %s", to, e) logger.error("Failed to send initial email to %s: %s", to, e)
@@ -73,10 +91,11 @@ def send_success_email(
task_id: str, 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" subject = "ScrAIbe: Your transcript is ready"
# Build plain-text fallback
body = ( body = (
"Hello,\n\n" "Hello,\n\n"
"Your transcription is ready.\n\n" "Your transcription is ready.\n\n"
@@ -94,14 +113,27 @@ def send_success_email(
body += ( body += (
"\n" "\n"
"Job ID: " + str(task_id) + "\n\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" "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: try:
send_email( send_email(
to=to, to=to,
subject=subject, subject=subject,
body=body, body=body,
html=html,
attachments=attachments, attachments=attachments,
) )
logger.info("Success email sent to %s for job %s", to, task_id) 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): 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" subject = "ScrAIbe: Error with your transcription request"
# Build plain-text fallback
body = ( body = (
"Hello,\n\n" "Hello,\n\n"
"We encountered an error while processing your transcription request.\n\n" "We encountered an error while processing your transcription request.\n\n"
f"Details: {error_message}\n\n" f"Details: {error_message}\n\n"
"Job ID: " + str(task_id) + "\n\n" "Job ID: " + str(task_id) + "\n\n"
"Please contact your administrator if the problem persists.\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" "This is an automated message from ScrAIbe.\n"
) )
# Build HTML using template
html = None
try: 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) logger.info("Error email sent to %s for job %s", to, task_id)
except EmailError as e: except EmailError as e:
logger.error("Failed to send error email to %s for job %s: %s", to, task_id, e) logger.error("Failed to send error email to %s for job %s: %s", to, task_id, e)
+33 -11
View File
@@ -45,6 +45,20 @@ def load_config():
return 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(): def create_app():
""" """
Create and launch the Gradio Web GUI (async mode). 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") upload_dir = os.getenv("SCRAIBE_UPLOAD_DIR", "/tmp/scraibe_uploads")
os.makedirs(upload_dir, exist_ok=True) 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") header_path = layout_cfg.get("header", "/app/src/misc/header.html")
footer_path = layout_cfg.get("footer", "/app/src/misc/footer.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 = "" 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 = "" footer_html = ""
if os.path.exists(footer_path):
if header_path and os.path.exists(header_path): version = os.getenv("SCRABIE_VERSION", "0.1.1.dev")
with open(header_path, "r", encoding="utf-8") as f: footer_html = load_html_template(
header_html = f.read() footer_path,
footer_scraibe_webui_version=version,
if footer_path and os.path.exists(footer_path): )
with open(footer_path, "r", encoding="utf-8") as f:
footer_html = f.read()
# Build Gradio interface # Build Gradio interface
with gr.Blocks( with gr.Blocks(
@@ -225,12 +247,12 @@ def create_app():
# Launch options # Launch options
server_name = launch_cfg.get("server_name", os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")) server_name = launch_cfg.get("server_name", os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"))
server_port = launch_cfg.get("server_port", 7860) 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( app.launch(
server_name=str(server_name), server_name=str(server_name),
server_port=int(server_port), 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; }", css="body { font-family: Arial, sans-serif; }",
) )