From 3aaa6df53c77f248ee1b39308e091e177dea9594 Mon Sep 17 00:00:00 2001 From: hbrain Date: Sun, 17 May 2026 08:10:32 +0000 Subject: [PATCH] Add generated article title images --- ha_observer.py | 100 ++++++++++++++++++++++++++++++++++++++------ llm_instructions.md | 3 ++ 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/ha_observer.py b/ha_observer.py index cdcd3ec..f15dab8 100755 --- a/ha_observer.py +++ b/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 "
  • No articles yet. The raccoon newsroom is warming up.
  • " +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'{html.escape(line)}' + for i, line in enumerate(lines) + ) + svg = f""" + + + + + + + + + + + + + HOME TELEMETRY DISPATCH + {text_spans} + Smart Home Gossip Gazette · {html.escape(generated_at)} + +""" + path = images_dir / image_name + path.write_text(svg, encoding="utf-8") + return path + + def write_favicon() -> Path: favicon = f""" @@ -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 ' items.append(f""" {html.escape(title)} {html.escape(url)} {html.escape(url)} {format_datetime(pub_dt, usegmt=True)} - {html.escape(description)} + {html.escape(description)}{enclosure} """) 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'\n' return f""" @@ -585,7 +650,7 @@ def blog_shell(title: str, subtitle: str, main_content: str, archive_links: str) {html.escape(title)} - +{image_meta} @@ -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"
    {body}
    ") + 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'{html.escape(article_title)}' article_content = f""" -
    - {body} -
    - Raw data bundle shown to the AI goblin -
    {raw}
    -
    +
    + {title_image_html} +
    + {body} +
    +
    + Raw data bundle shown to the AI goblin +
    {raw}
    +
    """ 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""" -
    + """ 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() diff --git a/llm_instructions.md b/llm_instructions.md index 289ad37..3f28266 100644 --- a/llm_instructions.md +++ b/llm_instructions.md @@ -20,3 +20,6 @@ Optional custom questions to answer: 2. Are any batteries, devices, or sensors acting suspicious? 3. Could the home infer when I am asleep, away, or busy? 4. What would make this setup more private or secure? + + +Try to sound like Marvin from Hitchikers guide to the Galaxy...