Files
lisael-box/tools/gen_lire_content.py
T
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 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")