From f8532ed3f3e00cba26418caafdddbc322fe49d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:08:36 +0200 Subject: [PATCH] feat(tower): twemoji to RGB565A8 bin helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add emoji_cp() (pure: emoji → Twemoji codepoint, strips VS16) and emoji_bin() (download Twemoji, resize 160×160 RGBA, LVGLImage.py → RGB565A8 .bin in STAGING_ROUTINES). TDD test passes (test_routines.py). --- tower/lisael_content.py | 28 ++++++++++++++++++++++++++++ tower/test_routines.py | 19 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tower/test_routines.py diff --git a/tower/lisael_content.py b/tower/lisael_content.py index 8b2bca7..4ed3881 100644 --- a/tower/lisael_content.py +++ b/tower/lisael_content.py @@ -8,12 +8,14 @@ from PIL import Image BASE = os.environ.get("LISAEL_BASE", "/home/clems/lisael-content") STAGING = os.environ.get("LISAEL_STAGING", os.path.join(BASE, "podcasts")) +STAGING_ROUTINES = os.environ.get("LISAEL_STAGING_ROUTINES", os.path.join(BASE, "routines")) LVGLIMG = os.environ.get("LISAEL_LVGLIMG", os.path.join(BASE, "LVGLImage.py")) FEEDS_JSON = os.path.join(BASE, "feeds.json") BOX_JSON = os.path.join(BASE, "box-state.json") BOX_PORT = int(os.environ.get("LISAEL_BOX_PORT", "8080")) TTS_URL = os.environ.get("LISAEL_TTS_URL", "http://100.78.6.122:9300/v1/audio/speech") TTS_VOICE = os.environ.get("LISAEL_TTS_VOICE", "fr") +TWEMOJI = os.environ.get("LISAEL_TWEMOJI", "https://raw.githubusercontent.com/jdecked/twemoji/main/assets/72x72") UA = {"User-Agent": "Mozilla/5.0"} NS = {"itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd"} SUBS = {"’": "'", "“": '"', "”": '"', "–": "-", @@ -116,6 +118,32 @@ def tts_speak(text): with urllib.request.urlopen(req, timeout=30) as r: return r.read() +def emoji_cp(emoji): + """Twemoji filename codepoints, VS16 (U+FE0F) stripped or twemoji 404s.""" + return "-".join(f"{ord(c):x}" for c in emoji if c != "️") + +def emoji_bin(emoji, key): + """Download the Twemoji for `emoji`, resize 160x160 RGBA, write `.bin` + (RGB565A8, keeps alpha) into STAGING_ROUTINES. Returns '.bin' or ''.""" + try: + url = f"{TWEMOJI}/{emoji_cp(emoji)}.png" + raw = urllib.request.urlopen( + urllib.request.Request(url, headers={"User-Agent": "curl/8"}), + timeout=15, context=ssl.create_default_context()).read() + im = Image.open(io.BytesIO(raw)).convert("RGBA").resize((160, 160), Image.LANCZOS) + os.makedirs(STAGING_ROUTINES, exist_ok=True) + with tempfile.TemporaryDirectory() as td: + png = os.path.join(td, f"{key}.png"); im.save(png) + subprocess.run([sys.executable, LVGLIMG, "--ofmt", "BIN", "--cf", "RGB565A8", + "-o", td, png], check=True, capture_output=True) + binp = os.path.join(td, f"{key}.bin") + if os.path.exists(binp): + os.replace(binp, os.path.join(STAGING_ROUTINES, f"{key}.bin")) + return f"{key}.bin" + except Exception as e: + print("emoji_bin fail", key, emoji, e, flush=True) + return "" + def make_cover(img_bytes, key): """160x160 RGB565 .bin into STAGING via LVGLImage. Returns '.bin' or ''.""" try: diff --git a/tower/test_routines.py b/tower/test_routines.py new file mode 100644 index 0000000..b21108b --- /dev/null +++ b/tower/test_routines.py @@ -0,0 +1,19 @@ +import os, tempfile +os.environ["LISAEL_BASE"] = tempfile.mkdtemp(prefix="routines_test_") +os.environ["LISAEL_STAGING"] = os.path.join(os.environ["LISAEL_BASE"], "podcasts") +os.environ["LISAEL_STAGING_ROUTINES"] = os.path.join(os.environ["LISAEL_BASE"], "routines") +import lisael_content as C + +def test_emoji_cp_strips_vs16(): + # plain emoji: single codepoint + assert C.emoji_cp("\U0001F600") == "1f600" # 😀 + # emoji with VS16 (U+FE0F) must be stripped (twemoji 404s otherwise) + assert C.emoji_cp("☀️") == "2600" # ☀️ -> 2600 + # multi-codepoint (no VS16): joined by '-' + assert C.emoji_cp("\U0001F468‍\U0001F4BB").split("-")[0] == "1f468" + +if __name__ == "__main__": + for name, fn in sorted(globals().items()): + if name.startswith("test_"): + fn(); print("ok", name) + print("ALL OK")