Add generated article title images
This commit is contained in:
parent
037d616764
commit
3aaa6df53c
2 changed files with 91 additions and 12 deletions
100
ha_observer.py
100
ha_observer.py
|
|
@ -439,6 +439,7 @@ BLOG_CSS = """
|
|||
article li { margin:.35rem 0; }
|
||||
article p, article li { font-size:1.04rem; color:#e6fbff; }
|
||||
article h1 { font-size:clamp(1.8rem,4vw,3.5rem); text-align:left; }
|
||||
.title-image { display:block; width:100%; height:auto; margin:0 0 1.4rem; border:1px solid #22d3ee66; box-shadow:0 0 28px #00d9ff22; }
|
||||
article h2 { margin-top:1.8rem; padding-top:1rem; border-top:1px solid #22d3ee33; }
|
||||
article h1 + p, article h2 + p, article h3 + p { margin-top:.3rem; }
|
||||
strong { color:#ffffff; font-weight:750; }
|
||||
|
|
@ -489,6 +490,63 @@ def article_links() -> str:
|
|||
return "\n".join(links) or "<li>No articles yet. The raccoon newsroom is warming up.</li>"
|
||||
|
||||
|
||||
def svg_text_lines(text: str, max_chars: int = 28, max_lines: int = 3) -> list[str]:
|
||||
words = text.split()
|
||||
lines: list[str] = []
|
||||
current = ""
|
||||
for word in words:
|
||||
candidate = f"{current} {word}".strip()
|
||||
if len(candidate) <= max_chars:
|
||||
current = candidate
|
||||
continue
|
||||
if current:
|
||||
lines.append(current)
|
||||
current = word
|
||||
if len(lines) == max_lines - 1:
|
||||
break
|
||||
if current and len(lines) < max_lines:
|
||||
lines.append(current)
|
||||
if len(lines) == max_lines and len(" ".join(words)) > len(" ".join(lines)):
|
||||
lines[-1] = lines[-1].rstrip(".,;: ") + "…"
|
||||
return lines or ["Smart Home Briefing"]
|
||||
|
||||
|
||||
def write_title_image(article_name: str, title: str, generated_at: str) -> Path:
|
||||
images_dir = WEB_DIR / "images"
|
||||
images_dir.mkdir(parents=True, exist_ok=True)
|
||||
image_name = article_name.replace(".html", ".svg")
|
||||
lines = svg_text_lines(remove_most_emoji(title))
|
||||
text_spans = "\n".join(
|
||||
f'<text x="80" y="{220 + i * 72}" class="title">{html.escape(line)}</text>'
|
||||
for i, line in enumerate(lines)
|
||||
)
|
||||
svg = f"""<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-label="{html.escape(title)}">
|
||||
<defs>
|
||||
<radialGradient id="g1" cx="20%" cy="15%" r="65%"><stop offset="0" stop-color="#1d4ed8"/><stop offset="0.45" stop-color="#07111f"/><stop offset="1" stop-color="#020617"/></radialGradient>
|
||||
<linearGradient id="line" x1="0" x2="1"><stop stop-color="#00f5ff"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient>
|
||||
<filter id="glow"><feGaussianBlur stdDeviation="4" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
|
||||
<style>
|
||||
.kicker {{ font: 700 28px ui-monospace, SFMono-Regular, Menlo, monospace; fill: #67e8f9; letter-spacing: 5px; }}
|
||||
.title {{ font: 800 58px system-ui, -apple-system, Segoe UI, sans-serif; fill: #f8feff; filter: url(#glow); }}
|
||||
.meta {{ font: 500 24px ui-monospace, SFMono-Regular, Menlo, monospace; fill: #bfdbfe; }}
|
||||
</style>
|
||||
</defs>
|
||||
<rect width="1200" height="630" fill="url(#g1)"/>
|
||||
<path d="M0 105h1200M0 210h1200M0 315h1200M0 420h1200M0 525h1200M120 0v630M360 0v630M600 0v630M840 0v630M1080 0v630" stroke="#22d3ee" stroke-opacity=".12"/>
|
||||
<path d="M40 40h360M40 40v120M1160 590H800M1160 590V470" stroke="url(#line)" stroke-width="4" fill="none"/>
|
||||
<circle cx="940" cy="175" r="96" fill="none" stroke="#00f5ff" stroke-opacity=".75" stroke-width="3"/>
|
||||
<circle cx="940" cy="175" r="54" fill="none" stroke="#fbbf24" stroke-opacity=".85" stroke-width="3"/>
|
||||
<path d="M812 175h256M940 47v256" stroke="#00f5ff" stroke-opacity=".45" stroke-width="2"/>
|
||||
<text x="80" y="105" class="kicker">HOME TELEMETRY DISPATCH</text>
|
||||
{text_spans}
|
||||
<text x="80" y="550" class="meta">Smart Home Gossip Gazette · {html.escape(generated_at)}</text>
|
||||
</svg>
|
||||
"""
|
||||
path = images_dir / image_name
|
||||
path.write_text(svg, encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def write_favicon() -> Path:
|
||||
favicon = f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
|
|
@ -549,13 +607,17 @@ def write_rss_feed() -> Path:
|
|||
description = article_text[:600]
|
||||
pub_dt = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc)
|
||||
url = site_url(f"articles/{path.name}")
|
||||
image_path = WEB_DIR / "images" / path.name.replace(".html", ".svg")
|
||||
enclosure = ""
|
||||
if image_path.exists():
|
||||
enclosure = f'\n <enclosure url="{html.escape(site_url(f"images/{image_path.name}"))}" type="image/svg+xml" length="{image_path.stat().st_size}" />'
|
||||
items.append(f"""
|
||||
<item>
|
||||
<title>{html.escape(title)}</title>
|
||||
<link>{html.escape(url)}</link>
|
||||
<guid isPermaLink="true">{html.escape(url)}</guid>
|
||||
<pubDate>{format_datetime(pub_dt, usegmt=True)}</pubDate>
|
||||
<description>{html.escape(description)}</description>
|
||||
<description>{html.escape(description)}</description>{enclosure}
|
||||
</item>""")
|
||||
now = format_datetime(datetime.now(timezone.utc), usegmt=True)
|
||||
feed_url = site_url("rss.xml")
|
||||
|
|
@ -577,7 +639,10 @@ def write_rss_feed() -> Path:
|
|||
return path
|
||||
|
||||
|
||||
def blog_shell(title: str, subtitle: str, main_content: str, archive_links: str) -> str:
|
||||
def blog_shell(title: str, subtitle: str, main_content: str, archive_links: str, image_href: str = "") -> str:
|
||||
image_meta = ""
|
||||
if image_href:
|
||||
image_meta = f'<meta property="og:image" content="{html.escape(site_url(image_href.lstrip("/")))}">\n'
|
||||
return f"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -585,7 +650,7 @@ def blog_shell(title: str, subtitle: str, main_content: str, archive_links: str)
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{html.escape(title)}</title>
|
||||
<link rel="canonical" href="{html.escape(site_url())}">
|
||||
<link rel="alternate" type="application/rss+xml" title="Smart Home Gossip Gazette RSS" href="{html.escape(site_url('rss.xml'))}">
|
||||
{image_meta}<link rel="alternate" type="application/rss+xml" title="Smart Home Gossip Gazette RSS" href="{html.escape(site_url('rss.xml'))}">
|
||||
<link rel="icon" href="{html.escape(site_href('favicon.svg'))}" type="image/svg+xml">
|
||||
<style>{BLOG_CSS}</style>
|
||||
</head>
|
||||
|
|
@ -620,14 +685,21 @@ def publish_webpage(conclusions: str, raw_summary: str) -> Path:
|
|||
article_name = f"{now_dt:%Y-%m-%d}.html"
|
||||
body = markdownish_to_html(conclusions)
|
||||
raw = html.escape(raw_summary[:60000])
|
||||
article_title, _ = clean_rss_text(f"<article>{body}</article>")
|
||||
title_image_path = write_title_image(article_name, article_title, now)
|
||||
title_image_href = site_href(f"images/{title_image_path.name}")
|
||||
title_image_html = f'<img class="title-image" src="{html.escape(title_image_href)}" alt="{html.escape(article_title)}">'
|
||||
article_content = f"""
|
||||
<article>
|
||||
{body}
|
||||
<details>
|
||||
<summary>Raw data bundle shown to the AI goblin</summary>
|
||||
<pre>{raw}</pre>
|
||||
</details>
|
||||
<article id="article" class="article post h-entry" itemscope itemtype="https://schema.org/Article">
|
||||
{title_image_html}
|
||||
<div class="entry-content post-content e-content" itemprop="articleBody">
|
||||
{body}
|
||||
</div>
|
||||
</article>
|
||||
<details>
|
||||
<summary>Raw data bundle shown to the AI goblin</summary>
|
||||
<pre>{raw}</pre>
|
||||
</details>
|
||||
"""
|
||||
article_path = articles_dir / article_name
|
||||
article_path.touch(exist_ok=True)
|
||||
|
|
@ -637,20 +709,24 @@ def publish_webpage(conclusions: str, raw_summary: str) -> Path:
|
|||
f"Daily home intelligence briefing · Generated {now}",
|
||||
article_content,
|
||||
article_links(),
|
||||
image_href=f"images/{title_image_path.name}",
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
featured = f"""
|
||||
<article>
|
||||
<article id="article" class="article post h-entry" itemscope itemtype="https://schema.org/Article">
|
||||
<p class="meta">Latest article · {html.escape(now)}</p>
|
||||
{body}
|
||||
{title_image_html}
|
||||
<div class="entry-content post-content e-content" itemprop="articleBody">
|
||||
{body}
|
||||
</div>
|
||||
<p><a href="{html.escape(site_href(f'articles/{article_name}'))}">Permanent link for this article →</a></p>
|
||||
</article>
|
||||
"""
|
||||
index_path = WEB_DIR / "index.html"
|
||||
index_path.write_text(
|
||||
blog_shell("Smart Home Gossip Gazette", "A daily blog of your Home Assistant household signals", featured, article_links()),
|
||||
blog_shell("Smart Home Gossip Gazette", "A daily blog of your Home Assistant household signals", featured, article_links(), image_href=f"images/{title_image_path.name}"),
|
||||
encoding="utf-8",
|
||||
)
|
||||
write_favicon()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue