d8cbf35572
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.
85 lines
3.6 KiB
Python
85 lines
3.6 KiB
Python
#!/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")
|