# 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)