Make file more readable and some adjustments to responsiveness

This commit is contained in:
Apher 2026-07-23 00:28:31 +00:00
parent 2c975003b0
commit 8a65d22c12

View File

@ -1,7 +1,3 @@
# Refactored template based on your original.
# NOTE: Replace MODEL_CONFIG loading as needed.
import collections, json, os, sys import collections, json, os, sys
from dataclasses import dataclass from dataclasses import dataclass
from time import monotonic from time import monotonic
@ -14,43 +10,89 @@ from openwakeword.model import Model
CONFIG = json.loads(sys.argv[1]) CONFIG = json.loads(sys.argv[1])
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
RATE = CONFIG.get("sampleRate", 16000) RATE = CONFIG.get("sampleRate", 16000)
CHUNK = 640 CHUNK = 640
WAKEWORD=CONFIG.get("wakeword","alexa")
THRESHOLD=CONFIG.get("wakewordThreshold",0.75) # Wakeword
WAKEWORD = CONFIG.get("wakeword", "hey jarvis")
THRESHOLD = CONFIG.get("wakewordThreshold", 0.80)
COOLDOWN = CONFIG.get("cooldownSeconds", 2.5) COOLDOWN = CONFIG.get("cooldownSeconds", 2.5)
CONFIRM_FRAMES = CONFIG.get("confirmFrames", 3) CONFIRM_FRAMES = CONFIG.get("confirmFrames", 3)
LISTEN_SECONDS=CONFIG.get("listenSeconds",8) LISTEN_SECONDS = CONFIG.get("listenSeconds", 12)
# Speech/silence gating (prevents wakeword firing right after transcript)
# Values assume threshold is on a normalized amplitude scale (0..1).
SILENCE_THRESHOLD = CONFIG.get("silenceThreshold", 0.03)
SILENCE_FRAMES = CONFIG.get("silenceFrames", 6)
# After transcribe finishes, ignore wakeword for a short period
POST_TRANSCRIPT_IGNORE_SECONDS = CONFIG.get("postTranscriptIgnoreSeconds", 1.5)
# ---- Transcription endpointing ----
ENDPOINT_RULE1 = CONFIG.get("endpoint_rule1_min_trailing_silence", 2.4)
ENDPOINT_RULE2 = CONFIG.get("endpoint_rule2_min_trailing_silence", 1.2)
ENDPOINT_RULE3 = CONFIG.get("endpoint_rule3_min_utterance_length", 30)
MIN_TRANSCRIBE_SECONDS_BEFORE_ENDPOINT = CONFIG.get("min_transcribe_before_endpoint", 1.0)
ENABLE_ENDPOINT_DETECTION = CONFIG.get("enable_endpoint_detection", True)
MODE_WAKE = "wakeword" MODE_WAKE = "wakeword"
MODE_TRANSCRIBE = "transcribe" MODE_TRANSCRIBE = "transcribe"
# ---- Sherpa model paths ----
mc = CONFIG["sherpaModelPath"] mc = CONFIG["sherpaModelPath"]
paths = {k: os.path.join(SCRIPT_DIR, v.lstrip("./")) for k, v in mc.items()} paths = {k: os.path.join(SCRIPT_DIR, v.lstrip("./")) for k, v in mc.items()}
recognizer = sherpa_onnx.OnlineRecognizer.from_transducer( recognizer = sherpa_onnx.OnlineRecognizer.from_transducer(
encoder=paths["encoder"],decoder=paths["decoder"],joiner=paths["joiner"], encoder=paths["encoder"],
tokens=paths["tokens"],sample_rate=RATE,feature_dim=80, decoder=paths["decoder"],
decoding_method="greedy_search",num_threads=max(1,(os.cpu_count() or 1)//2), joiner=paths["joiner"],
enable_endpoint_detection=True, tokens=paths["tokens"],
rule1_min_trailing_silence=1.5, sample_rate=RATE,
rule2_min_trailing_silence=0.3, feature_dim=80,
rule3_min_utterance_length=10, decoding_method="greedy_search",
num_threads=max(1, (os.cpu_count() or 1) // 2),
enable_endpoint_detection=ENABLE_ENDPOINT_DETECTION,
rule1_min_trailing_silence=ENDPOINT_RULE1,
rule2_min_trailing_silence=ENDPOINT_RULE2,
rule3_min_utterance_length=ENDPOINT_RULE3,
) )
# ---- openwakeword model ----
oww = Model(wakeword_models=[WAKEWORD], inference_framework="onnx") oww = Model(wakeword_models=[WAKEWORD], inference_framework="onnx")
# ---- Audio ----
pa = pyaudio.PyAudio() pa = pyaudio.PyAudio()
mic=pa.open(format=pyaudio.paInt16,channels=1,rate=RATE,input=True,frames_per_buffer=CHUNK) mic = pa.open(
format=pyaudio.paInt16,
channels=1,
rate=RATE,
input=True,
frames_per_buffer=CHUNK
)
@dataclass @dataclass
class State: class State:
mode: str = MODE_WAKE mode: str = MODE_WAKE
high_count: int = 0 high_count: int = 0
last_trigger: float = 0.0 last_trigger: float = 0.0
# Transcription timing
listen_deadline: float | None = None listen_deadline: float | None = None
sherpa_stream=None transcribe_start: float | None = None
# Wakeword anti-double-trigger
post_transcript_ignore_deadline: float | None = None
# Silence gate
silence_count: int = 0
sherpa_stream: object = None
last_partial: str = "" last_partial: str = ""
state = State() state = State()
float_buf = np.empty(CHUNK, np.float32) float_buf = np.empty(CHUNK, np.float32)
def emit(event, **kw): def emit(event, **kw):
@ -65,59 +107,123 @@ def reset_oww():
oww.accumulated_predictions[WAKEWORD].clear() oww.accumulated_predictions[WAKEWORD].clear()
def flush(): def flush():
# Drain mic so next phase starts with fresh audio
while True: while True:
try:a=mic.get_read_available() try:
except:break available = mic.get_read_available()
if a<=0:break except Exception:
mic.read(min(a,CHUNK),exception_on_overflow=False) break
if available <= 0:
break
mic.read(min(available, CHUNK), exception_on_overflow=False)
def start(now): def start(now):
state.mode = MODE_TRANSCRIBE state.mode = MODE_TRANSCRIBE
state.high_count = 0 state.high_count = 0
state.last_trigger = now state.last_trigger = now
state.listen_deadline = now + LISTEN_SECONDS state.listen_deadline = now + LISTEN_SECONDS
state.transcribe_start = now
state.last_partial = "" state.last_partial = ""
state.sherpa_stream = recognizer.create_stream() state.sherpa_stream = recognizer.create_stream()
def stop(): def stop(now):
reset_oww(); flush() # Reset back to wake mode
reset_oww()
flush()
state.mode = MODE_WAKE state.mode = MODE_WAKE
state.high_count = 0 state.high_count = 0
state.listen_deadline = None state.listen_deadline = None
state.transcribe_start = None
state.last_partial = "" state.last_partial = ""
state.sherpa_stream = None state.sherpa_stream = None
# Cooldown baseline should start AFTER the transcript ends
state.last_trigger = now
# And ignore wakeword for a short grace period
state.post_transcript_ignore_deadline = now + POST_TRANSCRIPT_IGNORE_SECONDS
emit("ready") emit("ready")
try: try:
while True: while True:
samples=np.frombuffer(mic.read(CHUNK,exception_on_overflow=False),dtype=np.int16) pcm_i16 = mic.read(CHUNK, exception_on_overflow=False)
samples_i16 = np.frombuffer(pcm_i16, dtype=np.int16)
now = monotonic() now = monotonic()
if state.mode == MODE_WAKE: if state.mode == MODE_WAKE:
score=oww.predict(samples).get(WAKEWORD,0.0) # -------- Silence gate (prevents wake right after transcript) --------
state.high_count=state.high_count+1 if score>=THRESHOLD else 0 # normalized mean absolute amplitude
if state.high_count>=CONFIRM_FRAMES and now-state.last_trigger>COOLDOWN: amp = float(np.mean(np.abs(samples_i16)) / 32768.0)
start(now)
emit("wakeword",model=WAKEWORD,score=float(score)) if amp <= SILENCE_THRESHOLD:
state.silence_count += 1
else:
state.silence_count = 0
silence_ok = state.silence_count >= SILENCE_FRAMES
# Ignore wakeword right after transcript ends
ignore_ok = not (
state.post_transcript_ignore_deadline is not None
and now < state.post_transcript_ignore_deadline
)
if not (silence_ok and ignore_ok):
continue continue
np.multiply(samples,1/32768.0,out=float_buf,casting="unsafe") # -------- Wakeword scoring --------
score = oww.predict(samples_i16).get(WAKEWORD, 0.0)
state.high_count = (state.high_count + 1) if score >= THRESHOLD else 0
if state.high_count >= CONFIRM_FRAMES and (now - state.last_trigger) > COOLDOWN:
start(now)
emit("wakeword", model=WAKEWORD, score=float(score))
continue
# ---- Transcribe mode ----
np.multiply(samples_i16, 1 / 32768.0, out=float_buf, casting="unsafe")
st = state.sherpa_stream st = state.sherpa_stream
st.accept_waveform(RATE, float_buf) st.accept_waveform(RATE, float_buf)
decode_pending(st) decode_pending(st)
txt = recognizer.get_result(st) txt = recognizer.get_result(st)
if txt and txt != state.last_partial: if txt and txt != state.last_partial:
state.last_partial = txt state.last_partial = txt
emit("partial", text=txt) emit("partial", text=txt)
if not(recognizer.is_endpoint(st) or now>=state.listen_deadline):
endpoint_ok = False
if state.listen_deadline is not None and now >= state.listen_deadline:
endpoint_ok = True
elif ENABLE_ENDPOINT_DETECTION and recognizer.is_endpoint(st):
# Dont allow endpointing immediately after switching to transcribe
if state.transcribe_start is not None and now >= (state.transcribe_start + MIN_TRANSCRIBE_SECONDS_BEFORE_ENDPOINT):
endpoint_ok = True
if not endpoint_ok:
continue continue
st.input_finished() st.input_finished()
decode_pending(st) decode_pending(st)
final = recognizer.get_result(st) final = recognizer.get_result(st)
if final: if final:
emit("final", text=final) emit("final", text=final)
emit("done") emit("done")
stop() stop(now)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
finally: finally:
mic.stop_stream(); mic.close(); pa.terminate() mic.stop_stream()
mic.close()
pa.terminate()