"""Causal bounded PCM speech gate; no ASR weights or transcript decisions.""" import hashlib from pathlib import Path import time import numpy as np from .stream import PCMReader, StreamProfile, require MODEL_SHA256='1a153a22f4509e292a94e67d6f9b85e8deb25b4988682b7e174c65279d8788e3' POLICY='silero_v5_threshold0.5_silence2000ms_preroll400ms_v1' class Silero: def __init__(self,path): import onnxruntime as ort require(hashlib.sha256(Path(path).read_bytes()).hexdigest()==MODEL_SHA256,'Silero model identity') options=ort.SessionOptions(); options.intra_op_num_threads=1; options.inter_op_num_threads=1 options.execution_mode=ort.ExecutionMode.ORT_SEQUENTIAL options.graph_optimization_level=ort.GraphOptimizationLevel.ORT_DISABLE_ALL self.session=ort.InferenceSession(str(path),sess_options=options,providers=['CPUExecutionProvider']) self.session.disable_fallback() require(self.session.get_providers()==['CPUExecutionProvider'],'Silero CPU execution required') self.state=np.zeros((2,1,128),np.float32); self.context=np.zeros(64,np.float32) self.sr=np.array(16000,dtype=np.int64); self.version=ort.__version__ def probability(self,frame): require(frame.shape==(512,) and frame.dtype==np.float32 and np.isfinite(frame).all(),'VAD new512 finite samples') window=np.concatenate((self.context,frame))[None,:] probability,state=self.session.run(['output','stateN'],{'input':window,'state':self.state,'sr':self.sr}) require(probability.shape==(1,1) and probability.dtype==np.float32 and np.isfinite(probability).all(),'VAD probability shape/type') require(state.shape==(2,1,128) and state.dtype==np.float32 and np.isfinite(state).all(),'VAD state shape/type') p=float(probability[0,0]); require(0<=p<=1,'VAD probability range') self.state=state; self.context=frame[-64:].copy(); return p class GatedPCM: def __init__(self,stream,detector,pace_start=None): self.reader=PCMReader(stream); self.detector=detector; self.pace_start=pace_start self.profile=StreamProfile('realtime18',3) self.read_seconds=self.sleep_seconds=self.vad_seconds=0. self.vad_frames=self.segment_count=self.selected_samples=self.unconsumed_samples=0 self.finished=False def __iter__(self): require(not self.finished,'gated source already consumed') self.finished=True reader=self.reader; active=None; previous_end=0; next_index=0; closing=False while True: if active is not None: start,end=self.profile.bounds(next_index) source_start=active['start_sample']+max(0,start-self.profile.left_samples) source_end=active['start_sample']+end-self.profile.left_samples terminal=self.profile.emissions_at_eof(reader.samples-active['start_sample']) eligible=(next_indexsource_start: require(source_start>=reader.base,'gated source overlap discarded') offset=source_start-reader.base; count=stop-source_start require(offset+count<=len(reader.buffer),'gated overlap unavailable') dst=self.profile.left_samples+source_start-active['start_sample']-start output[dst:dst+count]=reader.buffer[offset:offset+count] consumed=max(consumed,stop) event=dict(kind='window',segment=dict(active),index=next_index,audio=output, nominal_source_end_sample=source_end,available_sample=reader.samples,eof_drain=source_end>reader.samples) next_index+=1; yield event; continue if closing or reader.eof: unconsumed=reader.samples-consumed self.selected_samples+=reader.samples-active['start_sample']; self.unconsumed_samples+=unconsumed yield dict(kind='end',segment=dict(active),end_sample=reader.samples,final_eof=reader.eof, unconsumed_samples=unconsumed,windows=next_index) previous_end=reader.samples; active=None if reader.eof: break keep=max(0,reader.samples-6400) if active is not None: next_start=self.profile.bounds(next_index)[0] keep=min(keep,active['start_sample']+max(0,next_start-self.profile.left_samples)) keep=max(reader.base,min(keep,reader.samples)) reader.buffer=reader.buffer[keep-reader.base:].copy(); reader.base=keep before=reader.samples; began=time.monotonic(); reader.ensure(before+512) self.read_seconds+=time.monotonic()-began require(len(reader.buffer)<=6912,'gated bounded PCM retention') if self.pace_start is not None: duration=max(0.,self.pace_start+reader.samples/16000-time.monotonic()) began=time.monotonic() if duration:time.sleep(duration) self.sleep_seconds+=time.monotonic()-began if reader.samples-before<512:continue frame=reader.buffer[before-reader.base:before-reader.base+512] began=time.monotonic(); p=self.detector(frame); self.vad_seconds+=time.monotonic()-began require(type(p) is not bool and np.isfinite(p) and 0<=p<=1,'VAD finite probability') self.vad_frames+=1 if p>=.5: last_voice=reader.samples if active is None: active=dict(id=self.segment_count,start_sample=max(previous_end,reader.samples-6400),decision_sample=reader.samples) self.segment_count+=1; next_index=0; closing=False; consumed=active['start_sample'] yield dict(kind='start',segment=dict(active)) elif active is not None and reader.samples-last_voice>=32000:closing=True yield dict(kind='complete')