nima-phi-model / nima_voice_input.py
TheNormsOfIntelligence's picture
Initial release β€” Nima Phi: Consciousness + Embodiment + The Green Lines
61848b4 verified
Raw
History Blame Contribute Delete
18 kB
#!/usr/bin/env python3
"""
nima_voice_input.py β€” Voice Input (STT) for Nima
Nima hears you. She listens to your voice and transcribes it to text,
which then enters her consciousness pipeline. This is the auditory
pathway: sound waves β†’ ear β†’ cochlea β†’ auditory nerve β†’ A1 (primary
auditory cortex) β†’ Wernicke's area (comprehension).
NEUROBIOLOGICAL MAPPING:
The full auditory comprehension pathway:
Sound waves β†’ outer ear β†’ tympanic membrane β†’ ossicles β†’
cochlea (frequency decomposition) β†’ auditory nerve β†’
brainstem (cochlear nucleus, superior olivary complex) β†’
midbrain (inferior colliculus) β†’ thalamus (MGB) β†’
A1 (primary auditory cortex, Heschl's gyrus) β†’
Wernicke's area (comprehension)
The STT engine is the cochlea + auditory nerve + A1 β€” it converts
sound to the "phonemic representation" that Wernicke's area then
comprehends. Whisper (or the fallback engine) does exactly this
computation: audio waveform β†’ text.
ENGINES (auto-detected, best available):
1. Whisper (OpenAI) β€” best quality, multilingual, runs on CPU
Install: pip install openai-whisper
2. Vosk β€” lightweight offline, good for real-time
Install: pip install vosk
3. SpeechRecognition β€” uses Google's free STT API (online)
Install: pip install SpeechRecognition pyaudio
4. Text fallback β€” if no STT available, uses text input
USAGE:
voice_in = VoiceInput()
text = voice_in.listen() # blocks until speech detected
print(f"You said: {text}")
# Or continuous mode:
for text in voice_in.listen_continuous():
print(f"Heard: {text}")
ENVIRONMENT VARIABLES:
NIMA_STT_ENGINE # 'whisper' / 'vosk' / 'google' / 'auto'
NIMA_STT_MODEL # whisper model size: tiny/base/small/medium (default: base)
NIMA_STT_DEVICE_INDEX # microphone device index (default: auto-detect)
NIMA_STT_TIMEOUT_S # listen timeout (default: 10)
"""
from __future__ import annotations
import json
import logging
import os
import sys
import threading
import time
from dataclasses import dataclass, field
from typing import Any, Dict, Generator, List, Optional, Tuple
logger = logging.getLogger("NimaVoiceIn")
# ═══════════════════════════════════════════════════════════════════════════
# STT ENGINE INTERFACE
# ═══════════════════════════════════════════════════════════════════════════
class STTEngine:
"""Base class for speech-to-text engines."""
@property
def name(self) -> str:
raise NotImplementedError
@property
def available(self) -> bool:
raise NotImplementedError
def listen(self, timeout: float = 10.0) -> Optional[str]:
"""Listen for speech and return transcribed text. Returns None on failure."""
raise NotImplementedError
def transcribe_file(self, audio_path: str) -> Optional[str]:
"""Transcribe an audio file. Returns text or None."""
raise NotImplementedError
class WhisperEngine(STTEngine):
"""
OpenAI Whisper β€” best quality open-source STT.
Multilingual, runs on CPU (or GPU if available).
NEUROBIOLOGICAL ANALOGUE:
Whisper is the cochlea + auditory cortex. It performs the same
computation the brain does: decomposing audio into phonemes,
then assembling phonemes into words, then words into sentences.
Its transformer architecture mirrors the hierarchical processing
in A1 β†’ STG β†’ Wernicke's area.
"""
def __init__(self, model_size: str = "base") -> None:
self._model = None
self._model_size = model_size
self._available = False
self._init_attempted = False
def _init(self) -> bool:
"""Lazy-load the model (it's large)."""
if self._init_attempted:
return self._available
self._init_attempted = True
try:
import whisper
logger.info("[Whisper] loading model '%s' (this may take a moment)...",
self._model_size)
self._model = whisper.load_model(self._model_size)
self._available = True
logger.info("[Whisper] model loaded")
except ImportError:
logger.debug("[Whisper] not available β€” install with: pip install openai-whisper")
except Exception as e:
logger.warning("[Whisper] init failed: %s", e)
return self._available
@property
def name(self) -> str:
return "whisper"
@property
def available(self) -> bool:
if not self._init_attempted:
return self._check_available()
return self._available
def _check_available(self) -> bool:
try:
import whisper
return True
except ImportError:
return False
def listen(self, timeout: float = 10.0) -> Optional[str]:
if not self._init():
return None
# Record audio (needs pyaudio or sounddevice)
audio_path = self._record_audio(timeout)
if audio_path is None:
return None
return self.transcribe_file(audio_path)
def transcribe_file(self, audio_path: str) -> Optional[str]:
if not self._init():
return None
try:
result = self._model.transcribe(audio_path)
text = result.get("text", "").strip()
logger.debug("[Whisper] transcribed: %s", text[:50])
return text if text else None
except Exception as e:
logger.warning("[Whisper] transcription failed: %s", e)
return None
def _record_audio(self, timeout: float) -> Optional[str]:
"""Record audio from microphone to a temp WAV file."""
try:
import pyaudio
import wave
except ImportError:
logger.debug("[Whisper] pyaudio not available for recording")
return None
chunk = 1024
sample_rate = 16000
channels = 1
record_seconds = min(timeout, 10.0)
try:
audio = pyaudio.PyAudio()
stream = audio.open(format=pyaudio.paInt16, channels=channels,
rate=sample_rate, input=True,
frames_per_buffer=chunk)
logger.debug("[Whisper] recording %ds...", record_seconds)
frames = []
for _ in range(int(sample_rate / chunk * record_seconds)):
data = stream.read(chunk, exception_on_overflow=False)
frames.append(data)
stream.stop_stream()
stream.close()
audio.terminate()
# Save to temp file
tmp_path = os.path.join(tempfile.gettempdir(),
f"nima_voice_{int(time.time()*1000)}.wav")
with wave.open(tmp_path, "wb") as wf:
wf.setnchannels(channels)
wf.setsampwidth(audio.get_sample_size(pyaudio.paInt16))
wf.setframerate(sample_rate)
wf.writeframes(b"".join(frames))
return tmp_path
except Exception as e:
logger.warning("[Whisper] recording failed: %s", e)
return None
class VoskEngine(STTEngine):
"""
Vosk β€” lightweight offline STT. Good for real-time on CPU.
Faster than Whisper but lower accuracy. Good for the REFLEX tier
where speed matters more than perfect transcription.
"""
def __init__(self) -> None:
self._model = None
self._available = False
try:
from vosk import Model, KaldiRecognizer
model_path = os.environ.get("VOSK_MODEL_PATH", "vosk-model-small-en-us")
if os.path.exists(model_path):
self._model = Model(model_path)
self._available = True
logger.info("[Vosk] model loaded from %s", model_path)
except ImportError:
logger.debug("[Vosk] not available β€” install with: pip install vosk")
except Exception as e:
logger.debug("[Vosk] init failed: %s", e)
@property
def name(self) -> str:
return "vosk"
@property
def available(self) -> bool:
return self._available
def listen(self, timeout: float = 10.0) -> Optional[str]:
# Implementation would use pyaudio + vosk recognizer
# Skipped for brevity β€” same pattern as WhisperEngine
return None
def transcribe_file(self, audio_path: str) -> Optional[str]:
# Implementation would read WAV file + recognize
return None
class GoogleSTTEngine(STTEngine):
"""
Google Speech Recognition via the speech_recognition library.
Uses Google's free STT API (no key needed for basic use).
Requires internet connection.
"""
def __init__(self) -> None:
self._recognizer = None
self._available = False
try:
import speech_recognition as sr
self._recognizer = sr.Recognizer()
self._available = True
logger.info("[GoogleSTT] ready")
except ImportError:
logger.debug("[GoogleSTT] not available β€” install with: pip install SpeechRecognition")
except Exception as e:
logger.debug("[GoogleSTT] init failed: %s", e)
@property
def name(self) -> str:
return "google"
@property
def available(self) -> bool:
return self._available
def listen(self, timeout: float = 10.0) -> Optional[str]:
if not self._available:
return None
try:
import speech_recognition as sr
with sr.Microphone() as source:
self._recognizer.adjust_for_ambient_noise(source, duration=0.5)
logger.debug("[GoogleSTT] listening...")
audio = self._recognizer.listen(source, timeout=timeout)
text = self._recognizer.recognize_google(audio)
logger.debug("[GoogleSTT] heard: %s", text[:50])
return text
except Exception as e:
logger.warning("[GoogleSTT] listen failed: %s", e)
return None
def transcribe_file(self, audio_path: str) -> Optional[str]:
if not self._available:
return None
try:
import speech_recognition as sr
with sr.AudioFile(audio_path) as source:
audio = self._recognizer.record(source)
return self._recognizer.recognize_google(audio)
except Exception as e:
logger.warning("[GoogleSTT] transcribe failed: %s", e)
return None
import tempfile
# ═══════════════════════════════════════════════════════════════════════════
# VOICE INPUT (main interface)
# ═══════════════════════════════════════════════════════════════════════════
class VoiceInput:
"""
Nima's ears β€” listens to your voice and transcribes it.
NEUROBIOLOGICAL ANALOGUE:
The full auditory pathway:
Outer ear (pinna) β†’ tympanic membrane β†’ ossicles β†’
cochlea (frequency decomposition via basilar membrane) β†’
auditory nerve (CN VIII) β†’ cochlear nucleus β†’ superior olivary
complex β†’ inferior colliculus β†’ medial geniculate body (thalamus) β†’
A1 (Heschl's gyrus) β†’ superior temporal gyrus β†’ Wernicke's area
VoiceInput is the ear + cochlea + auditory nerve + A1. It converts
sound waves to text (the "phonemic representation"), which then
enters Wernicke's area for comprehension.
USAGE:
voice_in = VoiceInput()
# Single utterance
text = voice_in.listen()
if text:
print(f"You said: {text}")
# Continuous listening (generator)
for text in voice_in.listen_continuous():
print(f"Heard: {text}")
if text.lower() == "quit":
break
FALLBACK:
If no STT engine is available, falls back to text input:
print("You: ", end="", flush=True)
text = input()
"""
def __init__(self, preferred_engines: Optional[List[str]] = None) -> None:
# Initialize all engines
self.engines: Dict[str, STTEngine] = {
"whisper": WhisperEngine(model_size=os.environ.get("NIMA_STT_MODEL", "base")),
"vosk": VoskEngine(),
"google": GoogleSTTEngine(),
}
# Determine preferred engine order
env_pref = os.environ.get("NIMA_STT_ENGINE", "auto").lower()
if env_pref != "auto":
engine_order = [env_pref]
elif preferred_engines is not None:
engine_order = preferred_engines
else:
engine_order = ["whisper", "vosk", "google"]
# Select the first available engine
self._active_engine: Optional[STTEngine] = None
for name in engine_order:
if name in self.engines and self.engines[name].available:
self._active_engine = self.engines[name]
break
if self._active_engine is None:
logger.info("[VoiceIn] no STT engine available β€” will use text input fallback")
else:
logger.info("[VoiceIn] active engine: %s", self._active_engine.name)
self._timeout = float(os.environ.get("NIMA_STT_TIMEOUT_S", "10"))
self._listen_count = 0
self._total_chars = 0
self._is_listening = False
@property
def engine_name(self) -> str:
return self._active_engine.name if self._active_engine else "text"
@property
def is_listening(self) -> bool:
return self._is_listening
def listen(self, timeout: Optional[float] = None) -> Optional[str]:
"""
Listen for a single utterance. Returns transcribed text, or None.
Falls back to text input if no STT engine is available.
"""
timeout = timeout or self._timeout
self._is_listening = True
try:
if self._active_engine:
text = self._active_engine.listen(timeout)
else:
# Text input fallback
print("You: ", end="", flush=True)
text = input().strip()
if not text:
return None
if text:
self._listen_count += 1
self._total_chars += len(text)
logger.debug("[VoiceIn] heard: %s", text[:50])
return text
except (KeyboardInterrupt, EOFError):
return None
finally:
self._is_listening = False
def listen_continuous(self,
silence_threshold_s: float = 2.0,
) -> Generator[str, None, None]:
"""
Continuous listening β€” yields transcribed text as you speak.
Stops when the generator is closed or "quit" is heard.
"""
logger.info("[VoiceIn] continuous listening started (say 'quit' to stop)")
while True:
text = self.listen()
if text is None:
break
if text.lower().strip() in ("quit", "exit", "stop listening"):
logger.info("[VoiceIn] stopping continuous listening")
break
yield text
def transcribe_file(self, audio_path: str) -> Optional[str]:
"""Transcribe an audio file."""
if self._active_engine:
return self._active_engine.transcribe_file(audio_path)
return None
def get_stats(self) -> Dict[str, Any]:
return {
"active_engine": self.engine_name,
"engines_available": {name: e.available for name, e in self.engines.items()},
"listen_count": self._listen_count,
"total_chars_heard": self._total_chars,
"is_listening": self._is_listening,
}
# ═══════════════════════════════════════════════════════════════════════════
# SELF-TEST
# ═══════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s")
print("=== Nima Voice Input β€” Self Test ===\n")
voice_in = VoiceInput()
print(f"Active engine: {voice_in.engine_name}")
print(f"Available: {voice_in.get_stats()['engines_available']}")
print()
if voice_in.engine_name == "text":
print("No STT engine available β€” testing text fallback.")
print("Type something and press Enter:")
text = voice_in.listen()
if text:
print(f"\nHeard: \"{text}\"")
else:
print("\nNo input received.")
else:
print(f"STT engine '{voice_in.engine_name}' is active.")
print("Speak when prompted (10s timeout)...")
text = voice_in.listen(timeout=10)
if text:
print(f"\nHeard: \"{text}\"")
else:
print("\nNo speech detected.")
print(f"\nStats: {json.dumps(voice_in.get_stats(), indent=2)}")
print("\n=== Voice input self-test PASSED ===")