Files
signage/generate_welcome_page.py

330 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Willkommensseite-Generierung für CANCOM Simple Signage.
───────────────────────────────────────────────────────
Diese Hilfsdatei sucht anhand von Kundennamen Logos (via OpenAI + Brandfetch),
generiert daraus eine HTML-Willkommensseite und fügt sie in die Lobby-Playlist ein.
"""
import os
import json
from openai import OpenAI
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MEDIA_DIR = os.path.join(BASE_DIR, "media")
WELCOME_FILENAME = "welcome.html"
# -------------------------------------------------
# OpenAI-Client
# -------------------------------------------------
def get_openai_client():
"""Initialisiert den OpenAI-Client mit dem API-Key.
Der Key ist aktuell direkt im Code hinterlegt sollte in env var ausgelagert werden."""
api_key = "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"
if not api_key:
raise ValueError("OPENAI_API_KEY environment variable not set")
return OpenAI(api_key=api_key)
# -------------------------------------------------
# Brandfetch-Logo-Suche
# -------------------------------------------------
def get_brandfetch_logo(domain):
"""Versucht, ein Firmenlogo über Brandfetch CDN abzurufen.
Baut eine direkte CDN-URL: https://cdn.brandfetch.io/{domain}/logo"""
print(f"🔍 Generate - Versuche Brandfetch für Domain: {domain}")
logo_url = f"https://cdn.brandfetch.io/{domain}/logo?c=1idyd4Tpb2nKaXIIc8T"
print(f"✅ Generate - Brandfetch Logo gefunden: {logo_url}")
return logo_url
# -------------------------------------------------
# Logo-Ermittlung via OpenAI
# -------------------------------------------------
def search_customer_logo(customer_name):
"""
Sucht das Logo eines Kunden über einen zweistufigen Prozess:
1. OpenAI GPT-4 ermittelt die Domain des Unternehmens
2. Brandfetch CDN liefert das Logo zur Domain
Gibt die Logo-URL zurück oder None, wenn nichts gefunden wurde.
"""
print(f"✅ Generate - Suche nach Kundenlogo für: {customer_name}")
try:
client = get_openai_client()
response = client.chat.completions.create(
model="gpt-4",
messages=[
{
"role": "system",
"content": """Du bist ein Experte für die Suche nach Domain Namen. Du findest anhand des Firmennamens die richtige Domain dazu
Gib nur den Domain Namen zurück, keine weiteren Informationen.
FALLBACK: Wenn du kein direkt Aufrufbare Domain findest, antworte mit "FALLBACK"."""
},
{
"role": "user",
"content": f"Finde die Domain für dasUnternehmen: {customer_name}"
}
],
temperature=0.1,
max_tokens=100
)
logo_url = response.choices[0].message.content.strip()
print(f"🔍 OpenAI Logo-URL: {logo_url}")
if logo_url.upper() == "FALLBACK":
logo_url = None
if logo_url:
brandfetch_url = get_brandfetch_logo(logo_url)
if brandfetch_url:
return brandfetch_url
return None
except Exception as e:
print(f"❌ Logo search error: {e}")
return None
# -------------------------------------------------
# HTML-Generierung
# -------------------------------------------------
def md_to_html(text):
"""Wandelt einfaches Markdown in HTML um (fett, kursiv, code, zeilenumbrüche)."""
import re
text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text)
text = re.sub(r'\*(.+?)\*', r'<em>\1</em>', text)
text = re.sub(r'`(.+?)`', r'<code>\1</code>', text)
lines = text.split('\n')
return '<br>'.join(line.strip() for line in lines)
def generate_welcome_html(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None):
"""
Generiert den HTML-Code für die Willkommensseite.
- Links oben: CANCOM-Logo + "meets" + Kundenlogos
- Unter jedem Logo: Ansprechpartner-Kontaktdaten (Landscape)
- Portrait: Kontaktdaten links neben den Logos
customer_names: Liste der Kundennamen (max. 3)
logo_urls: Liste der Logo-URLs (gleiche Reihenfolge)
contacts: Liste der Ansprechpartner-Texte (Markdown, optional)
site: Standort-Name für die Anzeige
background_url: URLs zum Hintergrundbild (None → Standard-Wallpaper)
"""
site_display = site.capitalize()
names_str = ", ".join(customer_names) if customer_names else "Unbekannt"
print(f"✅ Generate - Willkommensseite wird generiert für: {names_str} (site: {site})")
site_display = site.capitalize()
if contacts is None:
contacts = [""] * len(customer_names)
bg_image = background_url if background_url else "/static/wallpaper.png"
cancom_svg = '''<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xcs="https://www.xtool.com/pages/software" version="1.1" preserveAspectRatio="xMinYMin meet" width="61.682394106284505mm" height="9.909010347638684mm" viewBox="136.55880294685778 209.2954948261807 61.682394106284505 9.909010347638684" xcs:version="2.7.22"><style></style><path transform="matrix(0.158038,0,0,0.158038,133.2558,163.322122)" stroke="none" fill="currentColor" data-view-type="laser" d="M125.9,352.1h18.9L119,291.7h-16.2l-25,60.4h18.2l14.6-38.9L125.9,352.1z M323.6,322 c0,8.8-6.1,16.4-15.4,16.4c-9.1,0-15.4-7.6-15.4-16.4s6.1-16.4,15.2-16.4C317,305.6,323.6,313.4,323.6,322 M340.5,322.3 c0-17.4-13.4-31.3-32.3-31.3c-18.7,0-32.3,14.1-32.3,31.3c0,17.4,13.4,31.3,32.3,31.3C326.9,353.6,340.5,339.4,340.5,322.3 M230.4,322.5c0-8.8,6.1-16.4,14.9-16.4c3,0,5.8,0.8,8.1,2l6.6-13.4c-3-2-8.1-3.8-14.9-3.8c-18.2,0-31.6,14.1-31.6,31.3 c0,18.2,12.6,31.1,31.6,31.1c17.2,0,26-10.6,28.8-21l-13.6-5.6c-1.8,5.8-6.3,11.6-14.4,11.6C236.5,338.4,230.4,331.4,230.4,322.5 M38,322.5c0-8.8,6.1-16.4,14.9-16.4c3,0,5.8,0.8,8.1,2l6.6-13.4c-3-2-8.1-3.8-14.9-3.8c-18.4,0-31.8,14.1-31.8,31.3 c0,18.2,12.6,31.1,31.6,31.1c17.2,0,26-10.6,28.8-21l-13.6-5.6c-1.8,5.8-6.3,11.6-14.4,11.6C44,338.4,38,331.4,38,322.5 M191.8,352.1h14.4v-59.8H190v33.1l-25-33.1h-15.4v60.1h16.2v-34.1L191.8,352.1z M411.2,352.1v-59.8h-17.7l-14.4,23.2l-14.4-23.2 h-17.7v60.1h16.4V318l15.4,23.2h0.3l15.4-23.5v34.6L411.2,352.1L411.2,352.1z" fill-rule="nonzero"></path></svg>'''
logo_html = ""
for i, (name, url) in enumerate(zip(customer_names, logo_urls)):
if url:
contact_html = ""
if i < len(contacts) and contacts[i]:
contact_html = f'''<div class="contact-info">{md_to_html(contacts[i])}</div>'''
logo_html += f'''
<div class="customer-logo-item">
<img src="{url}" alt="{name}" class="customer-logo" crossorigin="anonymous">
<div class="customer-meta">
<span class="customer-name">{name}</span>
{contact_html}
</div>
</div>'''
html_content = f"""<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen bei CANCOM</title>
<link rel="stylesheet"
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
<script defer
src="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/js/tabler.min.js">
</script>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
width: 100vw; height: 100vh; overflow: hidden;
font-family: 'Segoe UI', 'Helvetica Neue', sans-serif;
display: flex; align-items: center; justify-content: center;
background-image: url("{bg_image}");
background-repeat: no-repeat; background-size: cover;
}}
.screen {{
width: 100%; height: 100%; position: relative;
display: flex; flex-direction: column;
align-items: flex-end; justify-content: space-between;
padding: 60px;
}}
.logo-section {{
display: flex; flex-direction: column;
align-items: flex-end; gap: 10px; z-index: 10;
}}
.cancom-logo {{
width: 235px; height: auto; color: #DA002D;
object-fit: contain;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.1));
}}
.meets {{
font-size: 2.5rem; font-weight: 400; color: #000;
margin-top: -8px; margin-bottom: 10px; letter-spacing: 1px;
}}
.customer-logos {{
display: flex; flex-direction: row;
align-items: flex-start; justify-content: flex-end;
gap: 48px; max-width: 100%;
}}
.customer-logo-item {{
flex: 0 0 280px;
display: flex; flex-direction: column;
align-items: center; justify-content: flex-start;
}}
.customer-logo {{
width: 100%; height: auto;
max-height: 180px;
object-fit: contain;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15));
}}
.customer-name {{
font-size: 1rem; font-weight: 600; color: #000;
margin-top: 8px; text-align: center;
}}
.contact-info {{
font-size: 0.85rem; color: #333; line-height: 1.5;
margin-top: 6px; text-align: center;
}}
.contact-info strong {{ color: #000; }}
.contact-info code {{
font-size: 0.8rem; padding: 1px 4px;
background: rgba(0,0,0,0.06); border-radius: 3px;
}}
@media (orientation: portrait) {{
.customer-logos {{
flex-direction: column; gap: 20px;
}}
.customer-logo-item {{
flex: 0 0 auto;
flex-direction: row; gap: 20px;
width: 100%; max-width: 600px;
}}
.customer-logo {{
max-height: 100px; width: 180px; flex-shrink: 0;
}}
.customer-name {{
margin-top: 0; text-align: left;
}}
.contact-info {{
text-align: left;
}}
.customer-meta {{
display: flex; flex-direction: column;
align-items: flex-start; flex: 1;
}}
}}
.content {{
text-align: right; z-index: 10;
}}
.welcome-block {{
font-size: 4rem; font-weight: 700;
line-height: 1.3; color: #000;
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.3);
}}
</style>
</head>
<body>
<div class="screen">
<div class="logo-section">
<div class="cancom-logo">{cancom_svg}</div>
<div class="meets">meets</div>
<div class="customer-logos">
{logo_html}
</div>
</div>
<div class="content">
<div class="welcome-block">
Herzlich <span style="color: #DA002D;">Willkommen</span><br>
<span style="color: #DA002D;">in</span> unserer<br>
Niederlassung <span style="color: #DA002D;">{site_display}!</span>
</div>
</div>
</div>
</body>
</html>"""
return html_content
# -------------------------------------------------
# Speichern + Playlist-Einfügung
# -------------------------------------------------
def save_welcome_page(customer_names, logo_urls, contacts=None, site="stuttgart", background_url=None):
"""Speichert die generierte Willkommensseite als welcome.html im Lobby-Verzeichnis.
contacts: Liste der Ansprechpartner-Texte (Markdown, optional)
background_url: Benutzerdefiniertes Hintergrundbild oder None (Standard).
Gibt den Dateinamen zurück oder None bei Fehler."""
try:
welcome_dir = os.path.join(MEDIA_DIR, site, "lobby")
os.makedirs(welcome_dir, exist_ok=True)
filepath = os.path.join(os.path.abspath(welcome_dir), WELCOME_FILENAME)
html_content = generate_welcome_html(customer_names, logo_urls, contacts=contacts, site=site, background_url=background_url)
with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content)
if os.path.getsize(filepath) == 0:
raise IOError("File not written")
names_str = ", ".join(customer_names)
print(f"✅ Generate - Willkommensseite generiert für: {names_str} (site: {site})")
print(f"✅ Generate - Willkommensseite gespeichert in: {filepath}")
return WELCOME_FILENAME
except Exception as e:
print(f"❌ Error saving welcome page: {e}")
return None
def add_customer_to_lobby_playlist(html_filename, site="stuttgart"):
"""Fügt die Willkommensseite (welcome.html) an Position 0 in die Lobby-Playlist ein.
Entfernt vorher alle vorhandenen welcome.html-Einträge (als String oder Dict).
Importiert load_config/save_config aus app.py (kein Zirkelimport)."""
try:
from app import load_config, save_config
config = load_config()
sites_cfg = config.setdefault("sites", {})
site_cfg = sites_cfg.setdefault(site, {"screens": {}})
screens = site_cfg.setdefault("screens", {})
lobby = screens.setdefault("lobby", {})
playlist = lobby.setdefault("playlist", [])
full_filename = f"{html_filename}"
# Alte welcome.html-Einträge entfernen (sowohl String- als auch Dict-Form)
playlist[:] = [
item for item in playlist
if not (
(isinstance(item, str) and item == full_filename) or
(isinstance(item, dict) and item.get("name") == full_filename)
)
]
playlist.insert(0, full_filename)
save_config(config)
print(f"✅ Generate - Willkommensseite in Lobby-Playliste platziert (site: {site})")
return True
except Exception as e:
print(f"❌ Generate - Error adding to playlist: {e}")
return False