Use firm email templates, logo, and header/footer in async UI
This commit is contained in:
+58
-20
@@ -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
@@ -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
@@ -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; }",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user