Wake word working
This commit is contained in:
121
display.py
121
display.py
@@ -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)),
|
||||
special_flags=pygame.BLEND_ADD)
|
||||
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:
|
||||
screen.blit(face_img,
|
||||
face_img.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)
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user