chenbhao commited on
Commit
f268594
·
1 Parent(s): cecc5b1

Add omni_o_call.py: real-time voice call server for omni-o checkpoint

Browse files

- New script scripts/omni_o_call.py: Flask WebSocket server for real-time
voice conversation with VAD-based interrupt/barge-in detection
- Fix SileroVAD: use silero-vad v6.2.1 OnnxWrapper (handles context
and state correctly)
- Fix RealtimeSession: reduce VAD threshold 0.8→0.5 (Silero default),
reduce window size 1024→512 (required by silero-vad v6)
- Fix omni_web_demo.py: update VAD path to checkpoint/vad/

scripts/omni_o_call.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse, os, sys, json, time, math, torch, threading, queue, base64, io, logging, contextlib, warnings
2
+ import numpy as np
3
+ from PIL import Image
4
+ warnings.filterwarnings('ignore')
5
+ logging.getLogger().setLevel(logging.ERROR)
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
8
+ from models.vam import VAM, VAMConfig
9
+ from serve.realtime import RealtimeSession
10
+
11
+ M = {}
12
+ V = {} # voice_name -> {ref_codes, spk_emb}
13
+ MODEL_LOCK = threading.Lock()
14
+ VOICES_BUILTIN, VOICES_UNSEEN, VOICES_MANUAL = [], [], []
15
+ SAMPLES_PER_FRAME = 1920
16
+ REF_FRAMES = 300
17
+
18
+ def sse(d): return f"data: {json.dumps(d)}\n\n"
19
+
20
+ def asr_run(samples):
21
+ from funasr.utils.postprocess_utils import rich_transcription_postprocess
22
+ r = M['asr'].generate(input=samples, cache={}, language='auto', use_itn=True)
23
+ return rich_transcription_postprocess(r[0]['text']).strip() if r else ''
24
+
25
+ def prep_audio(samples):
26
+ m, dev = M['model'], M['device']
27
+ proc = m.audio_processor(samples, sampling_rate=16000, return_tensors="pt", return_attention_mask=True)
28
+ mel = proc.input_features.squeeze(0).unsqueeze(0).to(dev)
29
+ vlen = proc.attention_mask.sum().item()
30
+ return mel, torch.tensor([vlen], device=dev), m.config.audio_special_token * (vlen or 1)
31
+
32
+ def prep_image(b64):
33
+ img = Image.open(io.BytesIO(base64.b64decode(b64))).convert('RGB')
34
+ return {k: v.to(M['device']) for k, v in M['model'].vision_processor(images=img, return_tensors="pt").items()}
35
+
36
+ def build_ids(prompt, history):
37
+ tok, dev = M['tokenizer'], M['device']
38
+ cfg = M['cfg']
39
+ hist = history[-cfg.max_history_turns:] if cfg.max_history_turns > 0 else []
40
+ msgs = hist + [{"role": "user", "content": prompt}]
41
+ t = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
42
+ return torch.tensor(tok(t)['input_ids'], dtype=torch.long, device=dev)[None, ...]
43
+
44
+ def _mimi_decode(frames):
45
+ codes = [f for f in frames if f and len(f) == 8]
46
+ if not codes or not M['mimi']: return None
47
+ mc = torch.tensor(codes, dtype=torch.long, device=M['device']).T.unsqueeze(0)
48
+ mc = torch.where(mc >= 2049, torch.zeros_like(mc), mc)
49
+ with torch.no_grad():
50
+ au = M['mimi'].decode(mc).audio_values.squeeze().cpu().numpy()
51
+ return au, mc.shape[-1]
52
+
53
+ def pcm_bytes(frames, ov):
54
+ r = _mimi_decode(frames)
55
+ if r is None: return None
56
+ au, T = r
57
+ if ov > 0: au = au[int(ov * len(au) / T):]
58
+ return (au * 32767).astype('int16').tobytes()
59
+
60
+ def stream_pcm(frames, flush=False):
61
+ if not M['mimi']: return
62
+ cf, ov_max, n = M['cfg'].audio_chunk_frames, M['cfg'].audio_overlap, len(frames)
63
+ if not flush and n >= cf and n % cf == 0:
64
+ ov = min(ov_max, n - cf)
65
+ p = pcm_bytes(frames[-(cf + ov):], ov)
66
+ if p: yield p
67
+ elif flush:
68
+ rem = n % cf
69
+ if rem:
70
+ ov = min(ov_max, n - rem)
71
+ p = pcm_bytes(frames[-(rem + ov):], ov)
72
+ if p: yield p
73
+
74
+ def register_voice(name, value, group='manual'):
75
+ V[name] = value
76
+ groups = {'builtin': VOICES_BUILTIN, 'unseen': VOICES_UNSEEN, 'manual': VOICES_MANUAL}
77
+ dst = groups[group]
78
+ if name not in dst: dst.append(name)
79
+ for k, lst in groups.items():
80
+ if k != group and name in lst: lst.remove(name)
81
+
82
+ def voice_args(name):
83
+ if name and name != 'default' and name in V:
84
+ v = V[name]
85
+ dev = M['device']
86
+ rc = v['ref_codes'].unsqueeze(0).to(dev)
87
+ se = v['spk_emb'].half().unsqueeze(0).to(dev) if 'spk_emb' in v else None
88
+ return {'ref_codes': rc, 'spk_emb': se}
89
+ return {}
90
+
91
+ def run_generate(x, audio_inputs, audio_lens, pixel_values, **kw):
92
+ with MODEL_LOCK, torch.no_grad():
93
+ yield from M['model'].generate(
94
+ x, M['tokenizer'].eos_token_id, stream=True, return_audio_codes=True,
95
+ audio_inputs=audio_inputs, audio_lens=audio_lens, pixel_values=pixel_values, **kw)
96
+
97
+ def prepare_turn(text, samples, image_b64, do_asr_for_image):
98
+ audio_inputs = audio_lens = pixel_values = None
99
+ prompt = text or ''
100
+ user_text = text or ''
101
+ asr_thread, asr_result = None, [None]
102
+ if samples is not None:
103
+ if image_b64 and do_asr_for_image:
104
+ user_text = asr_run(samples)
105
+ prompt = user_text
106
+ else:
107
+ audio_inputs, audio_lens, prompt = prep_audio(samples)
108
+ if M['cfg'].max_history_turns > 0:
109
+ sa = samples.copy()
110
+ def _a(): asr_result[0] = asr_run(sa)
111
+ asr_thread = threading.Thread(target=_a); asr_thread.start()
112
+ if image_b64:
113
+ pixel_values = prep_image(image_b64)
114
+ m = M['model']
115
+ prompt = (prompt + "\n\n" if prompt else "") + "请描述这张图片\n\n" + m.config.image_special_token * m.config.image_token_len
116
+ return audio_inputs, audio_lens, pixel_values, prompt, user_text, asr_thread, asr_result
117
+
118
+
119
+ def init_web_app():
120
+ from flask import Flask, request, Response, send_from_directory
121
+ from flask_cors import CORS
122
+ from flask_sock import Sock
123
+
124
+ app = Flask(__name__, static_folder='.')
125
+ CORS(app)
126
+ sock = Sock(app)
127
+
128
+ @app.route('/')
129
+ def index(): return send_from_directory('.', 'web_demo.html')
130
+ @app.route('/call')
131
+ def call_page(): return send_from_directory('.', 'web_demo.html')
132
+
133
+ @app.route('/voices')
134
+ def get_voices():
135
+ return json.dumps({'builtin': sorted(VOICES_BUILTIN), 'unseen': sorted(VOICES_UNSEEN), 'manual': sorted(VOICES_MANUAL)})
136
+
137
+ @app.route('/models')
138
+ def get_models():
139
+ return json.dumps({'models': [M.get('model_name', 'omni-o')], 'current': M.get('model_name', 'omni-o')})
140
+
141
+ @app.route('/chat', methods=['POST'])
142
+ def chat():
143
+ d = request.json
144
+ history = d.get('history', [])
145
+ samples = None
146
+ if d.get('audio'):
147
+ from pydub import AudioSegment
148
+ seg = AudioSegment.from_file(io.BytesIO(base64.b64decode(d['audio']))).set_frame_rate(16000).set_channels(1).set_sample_width(2)
149
+ samples = np.frombuffer(seg.raw_data, dtype=np.int16).astype(np.float32) / 32768.0
150
+ va = voice_args(d.get('voice', 'default'))
151
+
152
+ def gen():
153
+ audio_inputs, audio_lens, pixel_values, prompt, user_text, asr_th, asr_res = prepare_turn(
154
+ d.get('text', ''), samples, d.get('image'), do_asr_for_image=True)
155
+ x = build_ids(prompt, history)
156
+ asr_sent = False
157
+ if user_text and samples is not None and d.get('image'):
158
+ yield sse({'type': 'user_prompt', 'content': user_text}); asr_sent = True
159
+ frames, text_ttft, audio_ttft = [], None, None
160
+ t0 = time.time(); hi = 0
161
+ for y, af in run_generate(x, audio_inputs, audio_lens, pixel_values,
162
+ max_new_tokens=d.get('max_tokens', 512),
163
+ temperature=d.get('temperature', 1), top_p=0.85, **va):
164
+ if not asr_sent and asr_th and not asr_th.is_alive():
165
+ asr_th.join()
166
+ if asr_res[0]: yield sse({'type': 'user_prompt', 'content': asr_res[0]})
167
+ asr_sent = True
168
+ if y is not None:
169
+ if text_ttft is None:
170
+ text_ttft = (time.time() - t0) * 1000
171
+ yield sse({'type': 'ttft', 'text_ttft': round(text_ttft, 1)})
172
+ ans = M['tokenizer'].decode(y[0].tolist(), skip_special_tokens=True)
173
+ if ans and ans[-1] != '\ufffd' and len(ans) > hi:
174
+ yield sse({'type': 'text', 'content': ans[hi:]}); hi = len(ans)
175
+ if af:
176
+ if audio_ttft is None:
177
+ audio_ttft = (time.time() - t0) * 1000
178
+ yield sse({'type': 'ttft', 'audio_ttft': round(audio_ttft, 1)})
179
+ frames.append(af)
180
+ for pcm in stream_pcm(frames):
181
+ b64 = base64.b64encode(pcm).decode()
182
+ for i in range(0, len(b64), 2000):
183
+ yield sse({'type': 'pcm', 'c': b64[i:i+2000], 'd': i+2000 >= len(b64)})
184
+ for pcm in stream_pcm(frames, flush=True):
185
+ b64 = base64.b64encode(pcm).decode()
186
+ for i in range(0, len(b64), 2000):
187
+ yield sse({'type': 'pcm', 'c': b64[i:i+2000], 'd': i+2000 >= len(b64)})
188
+ if not asr_sent:
189
+ if asr_th:
190
+ asr_th.join()
191
+ if asr_res[0]: yield sse({'type': 'user_prompt', 'content': asr_res[0]})
192
+ else:
193
+ yield sse({'type': 'user_prompt', 'content': prompt})
194
+ yield sse({'type': 'done'})
195
+ return Response(gen(), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
196
+
197
+ @sock.route('/ws/realtime')
198
+ def realtime(ws):
199
+ session = RealtimeSession(M['vad_path'])
200
+ q = queue.Queue(); alive = [True]; state = {'history': [], 'image': None}
201
+ n_hist = M['cfg'].max_history_turns
202
+
203
+ def push_audio(data):
204
+ return session.push_chunk(np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0)
205
+
206
+ def set_ctx(msg):
207
+ h = msg.get('history') or []
208
+ state['history'] = h[-n_hist:] if n_hist > 0 else []
209
+ if 'image' in msg: state['image'] = msg.get('image')
210
+ if 'voice' in msg: state['voice'] = msg.get('voice', 'default')
211
+
212
+ def poll_interrupt():
213
+ while True:
214
+ try: data = q.get_nowait()
215
+ except queue.Empty: return False
216
+ if isinstance(data, bytes):
217
+ if push_audio(data) == 'interrupt': return True
218
+ ws.send(json.dumps({'type': 'vad', 'speaking': session.speaking}))
219
+ else:
220
+ m = json.loads(data)
221
+ if m.get('type') == 'context': set_ctx(m)
222
+ elif m.get('type') in ('stop', 'end'):
223
+ if m['type'] == 'end': alive[0] = False
224
+ session.interrupt = True; return True
225
+
226
+ def recv_loop():
227
+ while alive[0]:
228
+ try:
229
+ data = ws.receive(timeout=1)
230
+ if data is None: alive[0] = False; break
231
+ q.put(data)
232
+ except: alive[0] = False; break
233
+
234
+ threading.Thread(target=recv_loop, daemon=True).start()
235
+ try:
236
+ while alive[0]:
237
+ try: data = q.get(timeout=0.05)
238
+ except queue.Empty: continue
239
+ if isinstance(data, str):
240
+ m = json.loads(data)
241
+ if m.get('type') == 'context': set_ctx(m)
242
+ elif m.get('type') == 'stop': session.interrupt = True
243
+ elif m.get('type') == 'end': break
244
+ continue
245
+ if session.generating:
246
+ push_audio(data); ws.send(json.dumps({'type': 'vad', 'speaking': session.speaking})); continue
247
+ status = push_audio(data)
248
+ ws.send(json.dumps({'type': 'vad', 'speaking': session.speaking}))
249
+ if status != 'speech_end': continue
250
+
251
+ session.generating = True
252
+ audio = session.get_audio()
253
+ ws.send(json.dumps({'type': 'generating'}))
254
+ audio_inputs, audio_lens, pixel_values, prompt, user_text, asr_th, asr_res = prepare_turn(
255
+ '', audio, state['image'], do_asr_for_image=True)
256
+ if state['image']: state['image'] = None
257
+ x = build_ids(prompt, state['history'])
258
+ va_rt = voice_args(state.get('voice', 'default'))
259
+
260
+ frames, full_text, interrupted = [], '', False
261
+ for y, af in run_generate(x, audio_inputs, audio_lens, pixel_values,
262
+ max_new_tokens=512, temperature=0.7, **va_rt):
263
+ if poll_interrupt() or session.interrupt: interrupted = True; break
264
+ if y is not None:
265
+ ans = M['tokenizer'].decode(y[0].tolist(), skip_special_tokens=True)
266
+ if ans and ans[-1] != '\ufffd' and len(ans) > len(full_text):
267
+ ws.send(json.dumps({'type': 'text', 'content': ans[len(full_text):]})); full_text = ans
268
+ if af:
269
+ frames.append(af)
270
+ for pcm in stream_pcm(frames):
271
+ ws.send(json.dumps({'type': 'pcm', 'data': base64.b64encode(pcm).decode()}))
272
+ if not interrupted:
273
+ for pcm in stream_pcm(frames, flush=True):
274
+ ws.send(json.dumps({'type': 'pcm', 'data': base64.b64encode(pcm).decode()}))
275
+ if asr_th:
276
+ asr_th.join(); user_text = asr_res[0] or user_text
277
+ if n_hist > 0:
278
+ if user_text: state['history'].append({'role': 'user', 'content': user_text})
279
+ if full_text: state['history'].append({'role': 'assistant', 'content': full_text})
280
+ state['history'] = state['history'][-n_hist:]
281
+ ws.send(json.dumps({'type': 'done', 'interrupted': interrupted or session.interrupt}))
282
+ session.generating = False; session.interrupt = False
283
+ finally:
284
+ alive[0] = False
285
+ return app
286
+
287
+
288
+ def init_model(args):
289
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
290
+ M['cfg'] = args
291
+ M['device'] = args.device
292
+
293
+ with contextlib.redirect_stdout(io.StringIO()):
294
+ from funasr import AutoModel
295
+ M['asr'] = AutoModel(model=os.path.join(root, args.sensevoice_dir), trust_remote_code=True, device=args.device, disable_update=True)
296
+
297
+ config = VAMConfig(
298
+ hidden_size=args.hidden_size,
299
+ num_hidden_layers=args.num_hidden_layers,
300
+ num_attention_heads=args.hidden_size // 96,
301
+ num_key_value_heads=args.hidden_size // 192,
302
+ use_moe=args.use_moe,
303
+ )
304
+ ckpt_dir = os.path.join(root, args.load_from)
305
+ weight = args.weight
306
+ if not weight.endswith('.pth'):
307
+ if args.use_moe and not weight.endswith('_moe'):
308
+ weight = f'{weight}_moe.pth'
309
+ else:
310
+ weight = f'{weight}.pth'
311
+ ckpt_path = os.path.join(ckpt_dir, weight)
312
+
313
+ model = VAM(config,
314
+ audio_encoder_path=os.path.join(root, args.sensevoice_dir),
315
+ vision_model_path=os.path.join(root, args.siglip_dir))
316
+ state = torch.load(ckpt_path, map_location='cpu', weights_only=True)
317
+ missing, unexpected = model.load_state_dict(state, strict=False)
318
+ if missing:
319
+ print(f' Missing keys (expected for encoders): {len(missing)}')
320
+ if unexpected:
321
+ print(f' Unexpected keys: {len(unexpected)}')
322
+ if model.audio_encoder is not None:
323
+ model.audio_encoder.to(args.device)
324
+ if model.vision_encoder is not None:
325
+ model.vision_encoder.to(args.device)
326
+ M['model'] = model.half().eval().to(args.device)
327
+
328
+ tok_dir = os.path.join(root, args.tokenizer_dir)
329
+ from transformers import AutoTokenizer
330
+ M['tokenizer'] = AutoTokenizer.from_pretrained(tok_dir)
331
+
332
+ M['model_name'] = args.weight
333
+ params = sum(p.numel() for p in model.parameters()) / 1e6
334
+ print(f'Loaded omni-o ({args.weight}): {params:.2f}M')
335
+
336
+ try:
337
+ from transformers import MimiModel
338
+ mimi_path = os.path.join(root, args.mimi_dir)
339
+ M['mimi'] = MimiModel.from_pretrained(mimi_path).eval().to(args.device)
340
+ if args.device != 'cpu':
341
+ M['mimi'] = M['mimi'].half()
342
+ print('Mimi loaded')
343
+ except Exception as e:
344
+ M['mimi'] = None
345
+ print(f'Mimi load failed: {e}')
346
+
347
+ try:
348
+ from modelscope.models.audio.sv.DTDNN import CAMPPlus
349
+ import torchaudio
350
+ M['campplus'] = CAMPPlus(feat_dim=80, embedding_size=192, growth_rate=32, bn_size=4,
351
+ init_channels=128, config_str='batchnorm-relu', memory_efficient=True)
352
+ camp_path = os.path.join(root, 'checkpoint/campplus/campplus_cn_common.pt')
353
+ sd = torch.load(camp_path, map_location='cpu')
354
+ M['campplus'].load_state_dict({k: v.float() for k, v in sd.items()})
355
+ M['campplus'] = M['campplus'].eval().to(args.device)
356
+ M['mel_fn'] = torchaudio.transforms.MelSpectrogram(
357
+ sample_rate=16000, n_fft=512, win_length=400, hop_length=160,
358
+ n_mels=80, f_min=20, f_max=7600, norm='slaney', mel_scale='slaney',
359
+ ).to(args.device)
360
+ print('CAM++ loaded')
361
+ except Exception as e:
362
+ M['campplus'] = M['mel_fn'] = None
363
+ print(f'CAM++ load failed (voice clone will be unavailable): {e}')
364
+
365
+ M['vad_path'] = os.path.join(root, args.vad_dir, 'silero_vad.onnx')
366
+
367
+ spk_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'model', 'speaker')
368
+ for fn, group in [('voices.pt', 'builtin'), ('voices_unseen.pt', 'unseen')]:
369
+ fp = os.path.join(spk_dir, fn)
370
+ if os.path.exists(fp):
371
+ for speaker, v in torch.load(fp, map_location='cpu').items():
372
+ if speaker not in V:
373
+ register_voice(speaker, v, group=group)
374
+ if V: print(f'Loaded {len(V)} voices')
375
+
376
+ print('Warmup...')
377
+ with torch.no_grad():
378
+ ids = torch.tensor([[1, 2, 3]], device=args.device)
379
+ au = torch.full((1, 8, 3), 2049, dtype=torch.long, device=args.device)
380
+ M['model'].forward(torch.cat((au, ids.unsqueeze(1)), dim=1))
381
+ if M['model'].audio_encoder is not None:
382
+ try:
383
+ M['model'].audio_encoder.model(torch.zeros(1, 100, 560, device=args.device), torch.tensor([100], device=args.device))
384
+ except Exception:
385
+ pass
386
+ if M['mimi']:
387
+ M['mimi'].decode(torch.zeros(1, 8, 1, dtype=torch.long, device=args.device))
388
+ print('Warmup done! Ready.')
389
+
390
+
391
+ if __name__ == '__main__':
392
+ p = argparse.ArgumentParser(description='Omni-O Real-time Voice Call')
393
+ p.add_argument('--load_from', default='checkpoint/omni-o', help='模型权重目录')
394
+ p.add_argument('--weight', default='omni-o', help='权重文件名(不含.pth后缀)')
395
+ p.add_argument('--tokenizer_dir', default='checkpoint/omni/native_hf', help='tokenizer目录')
396
+ p.add_argument('--sensevoice_dir', default='checkpoint/sensevoice', help='SenseVoice ASR目录')
397
+ p.add_argument('--siglip_dir', default='checkpoint/siglip', help='SigLIP视觉编码器目录')
398
+ p.add_argument('--mimi_dir', default='checkpoint/mimi', help='Mimi解码器目录')
399
+ p.add_argument('--vad_dir', default='checkpoint/vad', help='VAD模型目录')
400
+ p.add_argument('--hidden_size', default=768, type=int)
401
+ p.add_argument('--num_hidden_layers', default=8, type=int)
402
+ p.add_argument('--use_moe', default=0, type=int, choices=[0, 1])
403
+ p.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
404
+ p.add_argument('--port', default=7860, type=int)
405
+ p.add_argument('--audio_chunk_frames', default=4, type=int)
406
+ p.add_argument('--audio_overlap', default=2, type=int)
407
+ p.add_argument('--max_history_turns', default=0, type=int)
408
+ args = p.parse_args()
409
+
410
+ init_model(args)
411
+ app = init_web_app()
412
+ print(f'Omni-O Call server started at http://0.0.0.0:{args.port}/')
413
+ app.run(host='0.0.0.0', port=args.port, threaded=True)
scripts/omni_web_demo.py CHANGED
@@ -478,7 +478,7 @@ def init_model(args):
478
  except Exception as e:
479
  M['campplus'], M['mel_fn'] = None, None
480
  print(f'CAM++ load failed: {e}')
481
- M['vad_path'] = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'model', 'vad', 'silero_vad.onnx')
482
  spk_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'model', 'speaker')
483
  for fn, group in [('voices.pt', 'builtin'), ('voices_unseen.pt', 'unseen'), (CLONE_FILE, 'manual')]:
484
  fp = os.path.join(spk_dir, fn)
 
478
  except Exception as e:
479
  M['campplus'], M['mel_fn'] = None, None
480
  print(f'CAM++ load failed: {e}')
481
+ M['vad_path'] = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'checkpoint', 'vad', 'silero_vad.onnx')
482
  spk_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'model', 'speaker')
483
  for fn, group in [('voices.pt', 'builtin'), ('voices_unseen.pt', 'unseen'), (CLONE_FILE, 'manual')]:
484
  fp = os.path.join(spk_dir, fn)
src/serve/realtime.py CHANGED
@@ -1,25 +1,24 @@
1
  import numpy as np
 
2
 
3
 
4
  class SileroVAD:
5
- def __init__(self, path):
6
- import onnxruntime as ort
7
- opts = ort.SessionOptions()
8
- opts.inter_op_num_threads = opts.intra_op_num_threads = 1
9
- opts.log_severity_level = 4
10
- self.session = ort.InferenceSession(path, providers=["CPUExecutionProvider"], sess_options=opts)
11
- self.h, self.c = np.zeros((2, 1, 64), dtype=np.float32), np.zeros((2, 1, 64), dtype=np.float32)
12
 
13
  def reset(self):
14
- self.h[:], self.c[:] = 0, 0
15
 
16
  def __call__(self, chunk, sr=16000):
17
- out, self.h, self.c = self.session.run(None, {"input": chunk.reshape(1, -1).astype(np.float32), "h": self.h, "c": self.c, "sr": np.array(sr, dtype="int64")})
18
- return float(out[0][0])
 
 
19
 
20
 
21
  class RealtimeSession:
22
- def __init__(self, vad_path, sr=16000, threshold=0.8, min_speech_ms=128, min_silence_ms=800):
23
  self.vad, self.sr, self.threshold = SileroVAD(vad_path), sr, threshold
24
  self.min_speech, self.min_silence = int(sr * min_speech_ms / 1000), int(sr * min_silence_ms / 1000)
25
  self.reset()
@@ -29,7 +28,7 @@ class RealtimeSession:
29
  self.buffer, self.ring, self.speaking, self.generating, self.interrupt = [], [], False, False, False
30
  self.speech_samples = self.silence_samples = self.tail_silence = 0
31
 
32
- def push_chunk(self, chunk, W=1024):
33
  for i in range(0, max(len(chunk), 1), W):
34
  w = chunk[i:i + W]
35
  if len(w) < W:
 
1
  import numpy as np
2
+ import torch
3
 
4
 
5
  class SileroVAD:
6
+ def __init__(self, path=None):
7
+ from silero_vad import load_silero_vad
8
+ self.vad = load_silero_vad(onnx=True)
 
 
 
 
9
 
10
  def reset(self):
11
+ self.vad.reset_states()
12
 
13
  def __call__(self, chunk, sr=16000):
14
+ if chunk.shape[-1] not in (256, 512):
15
+ return 0.0
16
+ t = torch.from_numpy(chunk.reshape(1, -1).astype(np.float32))
17
+ return float(self.vad(t, sr))
18
 
19
 
20
  class RealtimeSession:
21
+ def __init__(self, vad_path, sr=16000, threshold=0.5, min_speech_ms=128, min_silence_ms=800):
22
  self.vad, self.sr, self.threshold = SileroVAD(vad_path), sr, threshold
23
  self.min_speech, self.min_silence = int(sr * min_speech_ms / 1000), int(sr * min_silence_ms / 1000)
24
  self.reset()
 
28
  self.buffer, self.ring, self.speaking, self.generating, self.interrupt = [], [], False, False, False
29
  self.speech_samples = self.silence_samples = self.tail_silence = 0
30
 
31
+ def push_chunk(self, chunk, W=512):
32
  for i in range(0, max(len(chunk), 1), W):
33
  w = chunk[i:i + W]
34
  if len(w) < W: