Files
lisael-box/tools/gen_lire_content.py
Clément Saillant d8cbf35572 feat: WiFi settings tile, 366 words, icon cleanup
Three follow-ups after the reading game landed.

- WiFi: a "Réglages" home tile (gear) opens LISAEL_MODE_WIFI which forces
  the SoftAP + captive portal up on demand (lisael_ui_goto starts them;
  the screen's "Fermer" button tears them down), so the provisioning
  portal can be tested/used without waiting for the station to fail.
- Lire: the content generator now also takes words that have no emoji
  (optional img in the regex) -> 366 words instead of 286 (the extra
  ~80 are abstract words: text + spoken audio, no picture).
- Cleanup: removed 21 baked word icons that became dead weight once the
  reading game started loading its pictures from the SD (~315 KB flash),
  plus their declarations and gen_icons.py specs.
2026-06-14 23:54:58 +02:00

85 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Build the Lisael Box "Lire" content from the website curriculum:
# - 366 words (mot + emoji) from lisael.saillant.cc js/games/lecture.data.js
# - per word: a Twemoji .bin icon (RGB565A8 72px) + a French TTS .mp3
# - words.json manifest [{w, id}]
# Output in /tmp/lire_content (img/ for icons, root for mp3 + words.json).
import io, json, os, re, ssl, subprocess, sys, unicodedata, urllib.request
WEB = "/Users/electron/Documents/Projets_Techniques/GitHub/electron-rare/lisael.saillant.cc/js/games/lecture.data.js"
OUT = "/tmp/lire_content"
PNG = "/tmp/lire_png"
IMG = os.path.join(OUT, "img")
LVGLIMG = "/tmp/lisael-box/managed_components/lvgl__lvgl/scripts/LVGLImage.py"
TWEMOJI = "https://raw.githubusercontent.com/jdecked/twemoji/main/assets/72x72"
GW = "http://100.78.6.122:9300/v1/audio/speech"
from PIL import Image
os.makedirs(IMG, exist_ok=True); os.makedirs(PNG, exist_ok=True)
ctx = ssl.create_default_context()
UA = {"User-Agent": "curl/8"}
data = open(WEB, encoding="utf-8").read()
m = re.search(r"export const WORDS = \[(.*?)\];", data, re.S)
entries = re.findall(r"\{\s*mot:\s*'([^']*)'(?:\s*,\s*img:\s*'([^']*)')?", m.group(1))
print(f"parsed {len(entries)} words")
def slug(s):
s = s.replace("œ", "oe").replace("Œ", "OE")
s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode()
return re.sub(r"[^a-zA-Z]", "", s).lower()
def cp(e):
return "-".join(f"{ord(c):x}" for c in e if c != "")
words = []
seen = set()
n_icon = n_audio = 0
for mot, emoji in entries:
wid = slug(mot) or "mot"
if wid in seen:
i = 2
while f"{wid}{i}" in seen:
i += 1
wid = f"{wid}{i}"
seen.add(wid)
words.append({"w": mot, "id": wid})
# icon PNG (skip if the word has no emoji, or already downloaded)
if emoji and not os.path.exists(os.path.join(PNG, f"{wid}.png")):
try:
raw = urllib.request.urlopen(
urllib.request.Request(f"{TWEMOJI}/{cp(emoji)}.png", headers=UA),
timeout=15, context=ctx).read()
im = Image.open(io.BytesIO(raw)).convert("RGBA").resize((72, 72), Image.LANCZOS)
im.save(os.path.join(PNG, f"{wid}.png"))
n_icon += 1
except Exception as e:
print("ICON FAIL", mot, emoji, str(e)[:80])
elif emoji:
n_icon += 1
# TTS audio (skip if already generated)
if os.path.exists(os.path.join(OUT, f"{wid}.mp3")):
n_audio += 1
else:
try:
body = json.dumps({"model": "tts-1", "input": mot, "voice": "fr",
"response_format": "mp3"}).encode()
au = urllib.request.urlopen(
urllib.request.Request(GW, data=body,
headers={"Content-Type": "application/json"}),
timeout=25).read()
open(os.path.join(OUT, f"{wid}.mp3"), "wb").write(au)
n_audio += 1
except Exception as e:
print("TTS FAIL", mot, str(e)[:80])
json.dump(words, open(os.path.join(OUT, "words.json"), "w", encoding="utf-8"),
ensure_ascii=False)
print(f"downloaded {n_icon} icons png, {n_audio} audio; converting icons...")
# one batch LVGLImage call over the folder of PNGs
r = subprocess.run([sys.executable, LVGLIMG, "--ofmt", "BIN", "--cf", "RGB565A8",
"-o", IMG, PNG], capture_output=True, text=True)
print("LVGLImage rc", r.returncode, (r.stderr[-300:] if r.returncode else ""))
nbin = len([f for f in os.listdir(IMG) if f.endswith(".bin")])
print(f"DONE: {len(words)} words, {nbin} .bin icons, {n_audio} mp3")