Add generated article title images

This commit is contained in:
hbrain 2026-05-17 08:10:32 +00:00
parent 037d616764
commit 3aaa6df53c
2 changed files with 91 additions and 12 deletions

View file

@ -439,6 +439,7 @@ BLOG_CSS = """
article li { margin:.35rem 0; } article li { margin:.35rem 0; }
article p, article li { font-size:1.04rem; color:#e6fbff; } article p, article li { font-size:1.04rem; color:#e6fbff; }
article h1 { font-size:clamp(1.8rem,4vw,3.5rem); text-align:left; } 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 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; } article h1 + p, article h2 + p, article h3 + p { margin-top:.3rem; }
strong { color:#ffffff; font-weight:750; } 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>" 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: def write_favicon() -> Path:
favicon = f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> favicon = f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs> <defs>
@ -549,13 +607,17 @@ def write_rss_feed() -> Path:
description = article_text[:600] description = article_text[:600]
pub_dt = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc) pub_dt = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc)
url = site_url(f"articles/{path.name}") 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""" items.append(f"""
<item> <item>
<title>{html.escape(title)}</title> <title>{html.escape(title)}</title>
<link>{html.escape(url)}</link> <link>{html.escape(url)}</link>
<guid isPermaLink="true">{html.escape(url)}</guid> <guid isPermaLink="true">{html.escape(url)}</guid>
<pubDate>{format_datetime(pub_dt, usegmt=True)}</pubDate> <pubDate>{format_datetime(pub_dt, usegmt=True)}</pubDate>
<description>{html.escape(description)}</description> <description>{html.escape(description)}</description>{enclosure}
</item>""") </item>""")
now = format_datetime(datetime.now(timezone.utc), usegmt=True) now = format_datetime(datetime.now(timezone.utc), usegmt=True)
feed_url = site_url("rss.xml") feed_url = site_url("rss.xml")
@ -577,7 +639,10 @@ def write_rss_feed() -> Path:
return 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> return f"""<!doctype html>
<html lang="en"> <html lang="en">
<head> <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"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(title)}</title> <title>{html.escape(title)}</title>
<link rel="canonical" href="{html.escape(site_url())}"> <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"> <link rel="icon" href="{html.escape(site_href('favicon.svg'))}" type="image/svg+xml">
<style>{BLOG_CSS}</style> <style>{BLOG_CSS}</style>
</head> </head>
@ -620,14 +685,21 @@ def publish_webpage(conclusions: str, raw_summary: str) -> Path:
article_name = f"{now_dt:%Y-%m-%d}.html" article_name = f"{now_dt:%Y-%m-%d}.html"
body = markdownish_to_html(conclusions) body = markdownish_to_html(conclusions)
raw = html.escape(raw_summary[:60000]) 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_content = f"""
<article> <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} {body}
</div>
</article>
<details> <details>
<summary>Raw data bundle shown to the AI goblin</summary> <summary>Raw data bundle shown to the AI goblin</summary>
<pre>{raw}</pre> <pre>{raw}</pre>
</details> </details>
</article>
""" """
article_path = articles_dir / article_name article_path = articles_dir / article_name
article_path.touch(exist_ok=True) 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}", f"Daily home intelligence briefing · Generated {now}",
article_content, article_content,
article_links(), article_links(),
image_href=f"images/{title_image_path.name}",
), ),
encoding="utf-8", encoding="utf-8",
) )
featured = f""" 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> <p class="meta">Latest article · {html.escape(now)}</p>
{title_image_html}
<div class="entry-content post-content e-content" itemprop="articleBody">
{body} {body}
</div>
<p><a href="{html.escape(site_href(f'articles/{article_name}'))}">Permanent link for this article </a></p> <p><a href="{html.escape(site_href(f'articles/{article_name}'))}">Permanent link for this article </a></p>
</article> </article>
""" """
index_path = WEB_DIR / "index.html" index_path = WEB_DIR / "index.html"
index_path.write_text( 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", encoding="utf-8",
) )
write_favicon() write_favicon()

View file

@ -20,3 +20,6 @@ Optional custom questions to answer:
2. Are any batteries, devices, or sensors acting suspicious? 2. Are any batteries, devices, or sensors acting suspicious?
3. Could the home infer when I am asleep, away, or busy? 3. Could the home infer when I am asleep, away, or busy?
4. What would make this setup more private or secure? 4. What would make this setup more private or secure?
Try to sound like Marvin from Hitchikers guide to the Galaxy...