Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ jobs:
set -eo pipefail
bash scripts/test_tmux_integration.sh

- name: bash scripts/test_mypy.sh
run: |
set -eo pipefail
bash scripts/test_mypy.sh

- name: debug
if: failure()
run: |
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
python3-sympy \
python3-typing-extensions

# Install pytest dependencies
# Install test dependencies
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3-iniconfig \
python3-mypy \
python3-pluggy \
python3-pygments \
python3-pytest
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ End-to-end integration test verifies the functionality of injecting text into th
bash scripts/test_tmux_integration.sh
```

## Type checking

Run mypy static type checking:

```
bash scripts/test_mypy.sh
```

# Implementation overview

The system uses object composition with separated responsibilities across multiple classes:
Expand Down
10 changes: 10 additions & 0 deletions scripts/test_mypy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash

set -Eeuo pipefail

bash scripts/build_docker_image.sh

docker run --rm --tty --name stt-mcp-server-linux-mypy \
--volume ./stt_mcp_server_linux.py:/app/stt_mcp_server_linux.py \
--volume ./tests:/app/tests \
stt-mcp-server-linux bash -ci "python -m mypy ."
14 changes: 7 additions & 7 deletions stt_mcp_server_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import datetime
import evdev
import queue
import sounddevice
import sounddevice # type: ignore[import-untyped]
import inspect
import unicodedata

Expand Down Expand Up @@ -242,7 +242,7 @@ def __init__(self, language: str = "en") -> None:
self.language = language
self.logger = create_logger(__name__)
self.logger.info(f"Loading Whisper model with language: {language}")
import whisper
import whisper # type: ignore[import-untyped]
self.model = whisper.load_model("tiny")
self.logger.info("Whisper model loaded successfully")

Expand All @@ -267,7 +267,7 @@ class VoskEngine(TranscriptionEngine):
def __init__(self) -> None:
self.logger = create_logger(__name__)
self.logger.info("Loading Vosk model")
import vosk
import vosk # type: ignore[import-untyped]
self.model = vosk.Model("/vosk")
self.recognizer = vosk.KaldiRecognizer(self.model, 16000)
self.logger.info("Vosk model loaded successfully")
Expand Down Expand Up @@ -397,11 +397,11 @@ async def monitor_device(self, dev_path: str, on_key_press: Callable[[], None],
async for event in dev.async_read_loop():
if event.type == evdev.ecodes.EV_KEY:
key_event = evdev.categorize(event)
if key_event.keycode == 'KEY_RIGHTCTRL':
if key_event.keystate == key_event.key_down:
if key_event.keycode == 'KEY_RIGHTCTRL': # type: ignore[attr-defined]
if key_event.keystate == key_event.key_down: # type: ignore[attr-defined]
self.logger.info("Right Ctrl key pressed")
on_key_press()
elif key_event.keystate == key_event.key_up:
elif key_event.keystate == key_event.key_up: # type: ignore[attr-defined]
self.logger.info("Right Ctrl key released")
on_key_release()
except Exception as e:
Expand All @@ -414,7 +414,7 @@ async def start_monitoring(self, on_key_press: Callable[[], None], on_key_releas
raise RuntimeError("No keyboard input devices found.")

await asyncio.gather(*(
self.monitor_device(dev.path, on_key_press, on_key_release)
self.monitor_device(str(dev.path), on_key_press, on_key_release)
for dev in keyboards
))

Expand Down
16 changes: 9 additions & 7 deletions tests/test_whisper.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,18 @@ def load_real_audio(self) -> bytes:
if channels != 1 or sample_width != 2 or framerate != 16000:
try:
import numpy as np
# Convert bytes to numpy array
from numpy.typing import NDArray

audio_np: NDArray[np.float32]
if sample_width == 1:
audio_np = np.frombuffer(audio_data, dtype=np.uint8)
audio_np = (audio_np.astype(np.float32) - 128) / 128.0
raw_audio = np.frombuffer(audio_data, dtype=np.uint8)
audio_np = (raw_audio.astype(np.float32) - 128) / 128.0
elif sample_width == 2:
audio_np = np.frombuffer(audio_data, dtype=np.int16)
audio_np = audio_np.astype(np.float32) / 32768.0
raw_audio = np.frombuffer(audio_data, dtype=np.int16)
audio_np = raw_audio.astype(np.float32) / 32768.0
elif sample_width == 4:
audio_np = np.frombuffer(audio_data, dtype=np.int32)
audio_np = audio_np.astype(np.float32) / 2147483648.0
raw_audio = np.frombuffer(audio_data, dtype=np.int32)
audio_np = raw_audio.astype(np.float32) / 2147483648.0
else:
return audio_data # Use as-is if unknown format

Expand Down