Wake word working

This commit is contained in:
David Rice
2026-04-18 05:25:28 +01:00
parent bfa7f988c4
commit 3ed974a2a5
3 changed files with 112 additions and 9 deletions

BIN
Jarvis.onnx Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -23,6 +23,7 @@ import urllib.parse
import xml.etree.ElementTree as ET
from datetime import datetime
_parser = argparse.ArgumentParser(description='JARVIS display', add_help=True)
_parser.add_argument('--fullscreen', '-fs', action='store_true',
help='Run fullscreen under X11/Wayland')
@@ -30,6 +31,8 @@ _parser.add_argument('--width', type=int, default=1280,
help='Screen width (default 1280)')
_parser.add_argument('--height', type=int, default=800,
help='Screen height (default 800)')
_parser.add_argument('--mic', type=int, default=0,
help='Microphone device index (default 0)')
_args, _ = _parser.parse_known_args()
import pygame
@@ -231,6 +234,77 @@ def _weather_worker(locations: list[tuple[float, float, str]]) -> None:
_time.sleep(1800)
# ═══════════════════════════════════════════════════════════════════════════════
# WAKE WORD
# ═══════════════════════════════════════════════════════════════════════════════
_OWW_MODEL = '/home/dfr84/Python/JARVIS/Jarvis.onnx'
_OWW_THRESHOLD = 0.5
_OWW_CHUNK = 1280
# Shared wake state — written by audio thread, read by render thread
_wake: dict = {'active': False, 'detected_at': 0.0}
def _wake_worker() -> None:
try:
import pyaudio
import numpy as np
from openwakeword.model import Model
except ImportError as e:
print(f'[WAKE] Missing dependency: {e} — wake word disabled')
return
try:
model = Model(wakeword_model_paths=[_OWW_MODEL])
audio = pyaudio.PyAudio()
dev_info = audio.get_device_info_by_index(_args.mic)
n_ch = int(dev_info['maxInputChannels'])
native_hz = int(dev_info['defaultSampleRate'])
target_hz = 16000
# frames_per_buffer scaled so we always get ~_OWW_CHUNK samples at 16 kHz
buf_frames = int(_OWW_CHUNK * native_hz / target_hz)
stream = audio.open(
format=pyaudio.paInt16,
channels=n_ch,
rate=native_hz,
input=True,
input_device_index=_args.mic,
frames_per_buffer=buf_frames,
)
while True:
data = stream.read(buf_frames, exception_on_overflow=False)
audio_data = np.frombuffer(data, dtype=np.int16)
if n_ch > 1:
audio_data = audio_data.reshape(-1, n_ch)[:, 0]
# resample to 16 kHz
if native_hz != target_hz:
ratio = target_hz / native_hz
new_len = int(len(audio_data) * ratio)
indices = np.round(np.linspace(0, len(audio_data) - 1, new_len)).astype(int)
audio_data = audio_data[indices]
prediction = model.predict(audio_data)
for score in prediction.values():
if score >= _OWW_THRESHOLD:
_wake['active'] = True
_wake['detected_at'] = _time.time()
break
except Exception as e:
import traceback
print(f'[WAKE] {type(e).__name__}: {e}')
traceback.print_exc()
def _lerp_color(c1: tuple, c2: tuple, t: float) -> tuple:
return tuple(int(c1[i] + (c2[i] - c1[i]) * t) for i in range(3))
_FACE_GREEN = ( 40, 200, 80)
_FACE_BLUE = ( 40, 80, 220)
_WAKE_DURATION = 8.0 # seconds before returning to idle
# ═══════════════════════════════════════════════════════════════════════════════
# WIREFRAME FACE
# ═══════════════════════════════════════════════════════════════════════════════
@@ -240,7 +314,8 @@ def _hw(t: float, rx: float) -> float:
return rx * math.sqrt(max(0.0, 1.0 - t * t))
return rx * math.sqrt(max(0.0, 1.0 - t * t)) * (1.0 - 0.30 * t)
def draw_wireframe_face(surface, cx, cy, rx, ry):
def draw_wireframe_face(surface, cx, cy, rx, ry,
col_wire=WIRE_COL, col_eye=WIRE_EYE):
N_H, N_V, PTS = 13, 11, 40
def sc(xf, t):
@@ -248,15 +323,15 @@ def draw_wireframe_face(surface, cx, cy, rx, ry):
bow = int(0.06 * w * (1.0 - t*t) * (1.0 - xf*xf))
return (int(cx + xf*w), int(cy + t*ry) - bow)
def draw(pts, col=WIRE_COL):
def draw(pts, col=col_wire):
if len(pts) >= 2:
pygame.draw.lines(surface, col, False, pts, 1)
t_vals = [-1.0 + 2.0*i/(PTS-1) for i in range(PTS)]
draw([sc(-1.0, t) for t in t_vals])
draw([sc( 1.0, t) for t in t_vals])
pygame.draw.lines(surface, WIRE_COL, False, [sc(-1.0,-1.0), sc(1.0,-1.0)], 1)
pygame.draw.lines(surface, WIRE_COL, False, [sc(-0.6,1.0), sc(0.0,1.02), sc(0.6,1.0)], 1)
pygame.draw.lines(surface, col_wire, False, [sc(-1.0,-1.0), sc(1.0,-1.0)], 1)
pygame.draw.lines(surface, col_wire, False, [sc(-0.6,1.0), sc(0.0,1.02), sc(0.6,1.0)], 1)
for i in range(1, N_H):
t = -1.0 + 2.0*i/N_H
@@ -274,7 +349,7 @@ def draw_wireframe_face(surface, cx, cy, rx, ry):
ecy = int(cy + eye_t * ry)
pts = [(int(ecx + e_rx*math.cos(2*math.pi*k/28)),
int(ecy + e_ry*math.sin(2*math.pi*k/28))) for k in range(29)]
pygame.draw.lines(surface, WIRE_EYE, True, pts, 1)
pygame.draw.lines(surface, col_eye, True, pts, 1)
inner = int(eye_w * 0.12)
e_btm = int(cy + eye_t*ry) + e_ry
@@ -489,6 +564,7 @@ def main() -> None:
# ── Background threads ────────────────────────────────────────────────────
threading.Thread(target=_news_worker, daemon=True).start()
threading.Thread(target=_stocks_worker, daemon=True).start()
threading.Thread(target=_wake_worker, daemon=True).start()
try:
resolved = [_geocode(city) for city in _LOCATIONS]
@@ -507,6 +583,9 @@ def main() -> None:
face_img = pygame.transform.smoothscale(
raw, (max(1, int(iw*scale)), max(1, int(ih*scale))))
# Reusable surface for PNG tinting (fill + BLEND_MULT each frame)
face_tint_surf = pygame.Surface(face_img.get_size()) if face_img else None
pg_clock = pygame.time.Clock()
while True:
@@ -516,14 +595,38 @@ def main() -> None:
if event.type == pygame.KEYDOWN and event.key in (pygame.K_ESCAPE, pygame.K_q):
pygame.quit(); return
# ── Wake-word face colour ─────────────────────────────────────────────
if _wake['active']:
elapsed = _time.time() - _wake['detected_at']
if elapsed >= _WAKE_DURATION:
_wake['active'] = False
face_color = None
else:
t = (math.sin(elapsed * math.pi * 2.0) + 1.0) / 2.0 # 0→1, 1 Hz
face_color = _lerp_color(_FACE_GREEN, _FACE_BLUE, t)
else:
face_color = None
screen.fill(BLACK)
# Face
# Face — tinted when wake active, normal otherwise
if face_img:
screen.blit(face_img, face_img.get_rect(center=(face_cx, face_cy)),
if face_color and face_tint_surf:
face_tint_surf.fill(face_color)
face_tint_surf.blit(face_img, (0, 0),
special_flags=pygame.BLEND_MULT)
screen.blit(face_tint_surf,
face_tint_surf.get_rect(center=(face_cx, face_cy)),
special_flags=pygame.BLEND_ADD)
else:
draw_wireframe_face(screen, face_cx, face_cy, face_rx, face_ry)
screen.blit(face_img,
face_img.get_rect(center=(face_cx, face_cy)),
special_flags=pygame.BLEND_ADD)
else:
cw = face_color or WIRE_COL
ce = face_color or WIRE_EYE
draw_wireframe_face(screen, face_cx, face_cy, face_rx, face_ry,
col_wire=cw, col_eye=ce)
# Clock (top-left)
draw_clock(screen, fonts, 20, clock_y)