Files
2026-06-21 21:19:51 -04:00

144 lines
4.5 KiB
Python

# Hermes ESP32 voice-state animations for MicroPython screen boards.
# Uploaded by the API server; can also be run manually with:
# exec(open('hermes_voice_animations.py').read()); run_state('ack')
import time
try:
import board_config
except Exception:
board_config = None
def _display():
return getattr(board_config, 'display_instance', None) if board_config else None
def _sleep(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000)
def _text(d, msg, x, y, scale=1, color=1):
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale=scale, color=color)
return
except TypeError:
pass
d.text(msg, x, y, color)
def _center_text(d, msg, y, scale=1, color=1):
width = getattr(d, 'width', 320)
char_w = 8 * scale
x = max(0, (width - len(msg) * char_w) // 2)
_text(d, msg, x, y, scale=scale, color=color)
def _wrap(text, width_chars=34, max_lines=4):
words = str(text or '').replace('\n', ' ').split()
lines, line = [], ''
for w in words:
candidate = (line + ' ' + w).strip()
if len(candidate) > width_chars and line:
lines.append(line)
line = w
if len(lines) >= max_lines:
break
else:
line = candidate
if line and len(lines) < max_lines:
lines.append(line)
return lines
def _spinner(d, title, subtitle='', frames=24, delay=70):
width, height = getattr(d, 'width', 320), getattr(d, 'height', 240)
cx, cy = width // 2, height // 2 - 8
spokes = [(-22,0),(-16,-16),(0,-22),(16,-16),(22,0),(16,16),(0,22),(-16,16)]
for frame in range(frames):
d.clear(0)
_center_text(d, title, 18, scale=2)
for i, (dx, dy) in enumerate(spokes):
color = 1 if (i + frame) % len(spokes) in (0, 1, 2, 3) else 0
size = 4 if i == frame % len(spokes) else 2
d.fill_rect(cx + dx - size // 2, cy + dy - size // 2, size, size, color)
d.rect(8, height - 34, width - 16, 18, 1)
d.fill_rect(10, height - 32, ((width - 20) * ((frame % 16) + 1)) // 16, 14, 1)
if subtitle:
_center_text(d, subtitle[:32], height - 56, scale=1)
d.show()
_sleep(delay)
def ack(frames=18):
d = _display()
if not d:
return
width, height = d.width, d.height
for i in range(frames):
d.clear(0)
_center_text(d, 'GOT IT', 18, scale=2)
# expanding check pulse
box = 48 + (i % 6) * 8
x, y = width // 2 - box // 2, height // 2 - box // 2
d.rect(x, y, box, box, 1)
d.line(width//2 - 24, height//2, width//2 - 6, height//2 + 18, 1)
d.line(width//2 - 6, height//2 + 18, width//2 + 30, height//2 - 22, 1)
_center_text(d, 'Listening captured', height - 36, scale=1)
d.show()
_sleep(55)
def captioning(frames=36):
d = _display()
if not d:
return
width, height = d.width, d.height
for i in range(frames):
d.clear(0)
_center_text(d, 'CAPTIONING', 16, scale=2)
for bar in range(9):
h = 8 + ((i * 5 + bar * 13) % 42)
x = width // 2 - 72 + bar * 18
d.fill_rect(x, height // 2 - h // 2, 10, h, 1)
dots = '.' * ((i // 5) % 4)
_center_text(d, 'Transcribing' + dots, height - 42, scale=1)
d.show()
_sleep(65)
def waiting(text='', frames=44):
d = _display()
if not d:
return
width, height = d.width, d.height
for i in range(frames):
d.clear(0)
_center_text(d, 'THINKING', 12, scale=2)
r = 18 + (i % 10) * 2
cx, cy = width // 2, height // 2 - 10
d.rect(cx - r, cy - r, r * 2, r * 2, 1)
d.rect(cx - r//2, cy - r//2, r, r, 1)
if text:
d.text('Heard:', 10, height - 68, 1)
for idx, line in enumerate(_wrap(text, 36, 3)):
d.text(line[:38], 10, height - 54 + idx * 12, 1)
else:
_center_text(d, 'Waiting for reply...', height - 42, scale=1)
d.show()
_sleep(75)
def run_state(state='ack', text='', frames=None):
state = str(state or 'ack').lower()
if state in ('ack', 'listening', 'got_it'):
ack(frames or 18)
elif state in ('caption', 'captioning', 'transcribing'):
captioning(frames or 36)
elif state in ('wait', 'waiting', 'thinking', 'reply'):
waiting(text, frames or 44)
else:
_spinner(_display(), 'HERMES', state[:28], frames or 20)