144 lines
4.5 KiB
Python
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)
|