chenbhao commited on
Commit
f53960b
·
1 Parent(s): d91f099

Rewrite omni_o_terminal.py: add real-time interrupt/barge-in

Browse files

- Background RealtimeRecorder thread captures mic continuously + VAD
- During generation, each step checks recorder.interrupt flag
- During audio playback, poll sd.get_stream().active with interrupt check
- Interrupt: stop generation immediately + stop playback via sd.stop()
- Completed audio delivered to main thread via queue.Queue

Files changed (1) hide show
  1. scripts/omni_o_terminal.py +151 -123
scripts/omni_o_terminal.py CHANGED
@@ -1,4 +1,4 @@
1
- import argparse, os, sys, json, io, time, math, torch, threading, queue, logging, contextlib, warnings
2
  import numpy as np
3
 
4
  warnings.filterwarnings('ignore')
@@ -10,7 +10,74 @@ from serve.realtime import SileroVAD
10
 
11
  SAMPLE_RATE = 16000
12
  AUDIO_SR = 24000
13
- SAMPLES_PER_FRAME = 1920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
 
16
  def asr_run(model, samples):
@@ -49,11 +116,7 @@ def init_model(args):
49
  audio_encoder_path=os.path.join(root, args.sensevoice_dir),
50
  vision_model_path=os.path.join(root, args.siglip_dir))
51
  state = torch.load(ckpt_path, map_location='cpu', weights_only=True)
52
- missing, unexpected = model.load_state_dict(state, strict=False)
53
- if missing:
54
- print(f' Missing keys (encoders): {len(missing)}')
55
- if unexpected:
56
- print(f' Unexpected keys: {len(unexpected)}')
57
  if model.audio_encoder is not None:
58
  model.audio_encoder.to(args.device)
59
  model = model.half().eval().to(args.device)
@@ -74,67 +137,6 @@ def init_model(args):
74
  return model, tokenizer, asr, mimi, vad
75
 
76
 
77
- def record_audio(args, vad):
78
- import sounddevice as sd
79
- vad.reset()
80
- buffer = []
81
- ring = []
82
- speaking = False
83
- speech_samples = 0
84
- silence_samples = 0
85
- tail_silence = 0
86
- min_speech = int(SAMPLE_RATE * args.min_speech_ms / 1000)
87
- min_silence = int(SAMPLE_RATE * args.min_silence_ms / 1000)
88
-
89
- stream = sd.InputStream(samplerate=SAMPLE_RATE, channels=1, dtype='float32',
90
- blocksize=512, device=args.mic)
91
- with stream:
92
- while True:
93
- chunk, _ = stream.read(512)
94
- chunk = chunk.flatten()
95
-
96
- for i in range(0, len(chunk), 512):
97
- w = chunk[i:i + 512]
98
- if len(w) < 512:
99
- w = np.pad(w, (0, 512 - len(w)))
100
- prob = vad(w, SAMPLE_RATE)
101
-
102
- if prob > args.vad_threshold:
103
- silence_samples = tail_silence = 0
104
- speech_samples += len(w)
105
- buffer.append(w)
106
- if speech_samples >= min_speech and not speaking:
107
- speaking = True
108
- buffer = ring + buffer
109
- ring = []
110
- elif speaking:
111
- silence_samples += len(w)
112
- tail_silence += 1
113
- buffer.append(w)
114
- if silence_samples >= min_silence:
115
- if tail_silence > 1:
116
- del buffer[-(tail_silence - 1):]
117
- audio = np.concatenate(buffer)
118
- return audio
119
- else:
120
- if speech_samples > 0:
121
- buffer.clear()
122
- speech_samples = 0
123
- ring = [w]
124
-
125
- if args.wait_key and not speaking:
126
- import select
127
- if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
128
- sys.stdin.read(1)
129
- return None
130
-
131
-
132
- def play_audio(pcm, rate=AUDIO_SR):
133
- import sounddevice as sd
134
- sd.play(pcm, rate)
135
- sd.wait()
136
-
137
-
138
  def mimi_decode(model, codes_2d, device):
139
  codes = codes_2d.T.unsqueeze(0).to(device)
140
  codes = torch.where(codes >= 2049, torch.zeros_like(codes), codes)
@@ -143,20 +145,19 @@ def mimi_decode(model, codes_2d, device):
143
  return audio
144
 
145
 
146
- def build_prompt(tokenizer, history, text):
147
- msgs = history + [{"role": "user", "content": text}]
148
- t = tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
149
- return torch.tensor(tokenizer(t)['input_ids'], dtype=torch.long, device='cpu')[None, ...]
150
-
151
-
152
- def generate_response(model, tokenizer, mimi, x, device, max_new_tokens=512):
153
  audio_frames = []
154
  text_out = ''
 
155
  with torch.no_grad():
156
  for y, af in model.generate(
157
  x, tokenizer.eos_token_id, stream=True, return_audio_codes=True,
158
  max_new_tokens=max_new_tokens, temperature=0.7, top_p=0.85,
159
  ):
 
 
 
 
160
  if y is not None:
161
  ans = tokenizer.decode(y[0].tolist(), skip_special_tokens=True)
162
  new_text = ans[len(text_out):]
@@ -166,7 +167,9 @@ def generate_response(model, tokenizer, mimi, x, device, max_new_tokens=512):
166
  if af:
167
  audio_frames.append(af)
168
  print()
169
- if audio_frames:
 
 
170
  codes = [f for f in audio_frames if f and len(f) == 8]
171
  if codes:
172
  codes_t = torch.tensor(codes, dtype=torch.long)
@@ -191,8 +194,14 @@ def warmup(model, mimi, device):
191
  mimi.decode(torch.zeros(1, 8, 1, dtype=torch.long, device=device))
192
 
193
 
 
 
 
 
 
 
194
  def main():
195
- parser = argparse.ArgumentParser(description='Omni-O Terminal Voice Chat')
196
  parser.add_argument('--load_from', default='checkpoint/omni-o')
197
  parser.add_argument('--weight', default='omni-o')
198
  parser.add_argument('--tokenizer_dir', default='checkpoint/omni/native_hf')
@@ -206,68 +215,87 @@ def main():
206
  parser.add_argument('--max_new_tokens', default=256, type=int)
207
  parser.add_argument('--vad_threshold', default=0.5, type=float)
208
  parser.add_argument('--min_speech_ms', default=128, type=int)
209
- parser.add_argument('--min_silence_ms', default=800, type=int)
210
  parser.add_argument('--mic', default=None, type=int, help='Microphone device index')
211
- parser.add_argument('--wait_key', default=0, type=int,
212
- help='Press Enter to start recording (0=auto VAD)')
213
  args = parser.parse_args()
214
 
215
  model, tokenizer, asr, mimi, vad = init_model(args)
216
  device = args.device
217
- x = build_prompt(tokenizer, [], 'Please introduce yourself.')
218
  print('Warmup...')
219
  warmup(model, mimi, device)
220
  print('Warmup done!\n')
221
 
222
  import sounddevice as sd
223
- history = []
 
 
224
 
225
- print('=== Omni-O Terminal Voice Chat ===')
226
- print(f'Mic: {sd.query_devices(args.mic, "input")["name"] if args.mic is not None else "default"}')
227
- print(f'Say something (VAD: silence>{args.min_silence_ms}ms = end of speech)')
 
 
 
228
  print()
229
 
230
- while True:
231
- if args.wait_key:
232
- input('Press Enter to record...')
233
- print('Recording... (speak now)')
234
- audio = record_audio(args, vad)
235
- if audio is None:
236
- continue
237
- else:
238
- audio = record_audio(args, vad)
239
- if audio is None:
240
  continue
241
 
242
- print(f'\r Captured {len(audio) / SAMPLE_RATE:.1f}s audio')
243
-
244
- print(' ASR...', end=' ', flush=True)
245
- st = time.time()
246
- text = asr_run(asr, audio)
247
- print(f'"{text}" ({time.time() - st:.1f}s)')
248
- if not text:
249
- print(' (no speech detected)')
250
- continue
251
-
252
- history.append({"role": "user", "content": text})
253
-
254
- print(' Generating...', end=' ', flush=True)
255
- st = time.time()
256
- x = build_prompt(tokenizer, history[:-1], text)
257
- x = x.to(device)
258
- text_resp, pcm = generate_response(model, tokenizer, mimi, x, device,
259
- max_new_tokens=args.max_new_tokens)
260
- print(f' ({time.time() - st:.1f}s)')
261
-
262
- if text_resp:
263
- history.append({"role": "assistant", "content": text_resp})
264
-
265
- if pcm is not None and len(pcm) > 0:
266
- print(' Playing...', end=' ', flush=True)
267
- play_audio(pcm)
268
- print('done')
269
 
270
- print()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
 
273
  if __name__ == '__main__':
 
1
+ import argparse, os, sys, io, time, torch, threading, queue, logging, contextlib, warnings
2
  import numpy as np
3
 
4
  warnings.filterwarnings('ignore')
 
10
 
11
  SAMPLE_RATE = 16000
12
  AUDIO_SR = 24000
13
+
14
+
15
+ class RealtimeRecorder:
16
+ def __init__(self, vad, threshold=0.5, min_speech_ms=128, min_silence_ms=800, mic=None):
17
+ self.vad = vad
18
+ self.threshold = threshold
19
+ self.min_speech = int(SAMPLE_RATE * min_speech_ms / 1000)
20
+ self.min_silence = int(SAMPLE_RATE * min_silence_ms / 1000)
21
+ self.mic = mic
22
+ self.q = queue.Queue()
23
+ self.lock = threading.Lock()
24
+ self.reset()
25
+
26
+ def reset(self):
27
+ self.state = 'idle'
28
+ self.buffer = []
29
+ self.ring = []
30
+ self.speaking = False
31
+ self.speech_samples = 0
32
+ self.silence_samples = 0
33
+ self.tail_silence = 0
34
+ self.interrupt = False
35
+
36
+ def _feed(self, w):
37
+ prob = self.vad(w, SAMPLE_RATE)
38
+ with self.lock:
39
+ if prob > self.threshold:
40
+ self.silence_samples = self.tail_silence = 0
41
+ self.speech_samples += len(w)
42
+ self.buffer.append(w)
43
+ if self.speech_samples >= self.min_speech and not self.speaking:
44
+ self.speaking = True
45
+ self.buffer = self.ring + self.buffer
46
+ self.ring = []
47
+ if self.speaking and self.state in ('processing', 'playing'):
48
+ self.interrupt = True
49
+ elif self.speaking:
50
+ self.silence_samples += len(w)
51
+ self.tail_silence += 1
52
+ self.buffer.append(w)
53
+ if self.silence_samples >= self.min_silence:
54
+ if self.tail_silence > 1:
55
+ del self.buffer[-(self.tail_silence - 1):]
56
+ audio = np.concatenate(self.buffer)
57
+ self.buffer.clear()
58
+ self.speaking = False
59
+ self.speech_samples = self.silence_samples = self.tail_silence = 0
60
+ self.q.put(audio)
61
+ else:
62
+ if self.speech_samples > 0:
63
+ self.buffer.clear()
64
+ self.speech_samples = 0
65
+ self.ring = [w]
66
+
67
+ def start(self):
68
+ import sounddevice as sd
69
+ def _run():
70
+ with sd.InputStream(samplerate=SAMPLE_RATE, channels=1, dtype='float32',
71
+ blocksize=512, device=self.mic) as stream:
72
+ while getattr(self, '_running', True):
73
+ chunk, _ = stream.read(512)
74
+ self._feed(chunk.flatten())
75
+ self._running = True
76
+ self.thread = threading.Thread(target=_run, daemon=True)
77
+ self.thread.start()
78
+
79
+ def stop(self):
80
+ self._running = False
81
 
82
 
83
  def asr_run(model, samples):
 
116
  audio_encoder_path=os.path.join(root, args.sensevoice_dir),
117
  vision_model_path=os.path.join(root, args.siglip_dir))
118
  state = torch.load(ckpt_path, map_location='cpu', weights_only=True)
119
+ model.load_state_dict(state, strict=False)
 
 
 
 
120
  if model.audio_encoder is not None:
121
  model.audio_encoder.to(args.device)
122
  model = model.half().eval().to(args.device)
 
137
  return model, tokenizer, asr, mimi, vad
138
 
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  def mimi_decode(model, codes_2d, device):
141
  codes = codes_2d.T.unsqueeze(0).to(device)
142
  codes = torch.where(codes >= 2049, torch.zeros_like(codes), codes)
 
145
  return audio
146
 
147
 
148
+ def generate_response(recorder, model, tokenizer, mimi, x, device, max_new_tokens=512):
 
 
 
 
 
 
149
  audio_frames = []
150
  text_out = ''
151
+ interrupted = False
152
  with torch.no_grad():
153
  for y, af in model.generate(
154
  x, tokenizer.eos_token_id, stream=True, return_audio_codes=True,
155
  max_new_tokens=max_new_tokens, temperature=0.7, top_p=0.85,
156
  ):
157
+ with recorder.lock:
158
+ if recorder.interrupt:
159
+ interrupted = True
160
+ break
161
  if y is not None:
162
  ans = tokenizer.decode(y[0].tolist(), skip_special_tokens=True)
163
  new_text = ans[len(text_out):]
 
167
  if af:
168
  audio_frames.append(af)
169
  print()
170
+ if interrupted:
171
+ print(' [interrupted]')
172
+ if audio_frames and not interrupted:
173
  codes = [f for f in audio_frames if f and len(f) == 8]
174
  if codes:
175
  codes_t = torch.tensor(codes, dtype=torch.long)
 
194
  mimi.decode(torch.zeros(1, 8, 1, dtype=torch.long, device=device))
195
 
196
 
197
+ def build_prompt(tokenizer, history, text):
198
+ msgs = history + [{"role": "user", "content": text}]
199
+ t = tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
200
+ return torch.tensor(tokenizer(t)['input_ids'], dtype=torch.long, device='cpu')[None, ...]
201
+
202
+
203
  def main():
204
+ parser = argparse.ArgumentParser(description='Omni-O Terminal Voice Chat (with interrupt)')
205
  parser.add_argument('--load_from', default='checkpoint/omni-o')
206
  parser.add_argument('--weight', default='omni-o')
207
  parser.add_argument('--tokenizer_dir', default='checkpoint/omni/native_hf')
 
215
  parser.add_argument('--max_new_tokens', default=256, type=int)
216
  parser.add_argument('--vad_threshold', default=0.5, type=float)
217
  parser.add_argument('--min_speech_ms', default=128, type=int)
218
+ parser.add_argument('--min_silence_ms', default=600, type=int)
219
  parser.add_argument('--mic', default=None, type=int, help='Microphone device index')
 
 
220
  args = parser.parse_args()
221
 
222
  model, tokenizer, asr, mimi, vad = init_model(args)
223
  device = args.device
224
+
225
  print('Warmup...')
226
  warmup(model, mimi, device)
227
  print('Warmup done!\n')
228
 
229
  import sounddevice as sd
230
+ recorder = RealtimeRecorder(vad, args.vad_threshold, args.min_speech_ms,
231
+ args.min_silence_ms, args.mic)
232
+ recorder.start()
233
 
234
+ history = []
235
+ mic_name = sd.query_devices(args.mic, 'input')['name'] if args.mic is not None else 'default'
236
+ print('=== Omni-O Terminal Voice Chat (interruptible) ===')
237
+ print(f'Mic: {mic_name}')
238
+ print('Speak to start — silence >=600ms = end of turn')
239
+ print('Speak during playback to interrupt')
240
  print()
241
 
242
+ try:
243
+ while True:
244
+ audio = recorder.q.get()
245
+ if len(audio) < SAMPLE_RATE * 0.1:
 
 
 
 
 
 
246
  continue
247
 
248
+ seconds = len(audio) / SAMPLE_RATE
249
+ print(f'\r {seconds:.1f}s audio ASR...', end=' ', flush=True)
250
+ st = time.time()
251
+ text = asr_run(asr, audio)
252
+ print(f'"{text}" ({time.time() - st:.1f}s)')
253
+ if not text.strip():
254
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
+ history.append({"role": "user", "content": text})
257
+
258
+ with recorder.lock:
259
+ recorder.state = 'processing'
260
+
261
+ x = build_prompt(tokenizer, history[:-1], text).to(device)
262
+ print(' ', end='', flush=True)
263
+ st = time.time()
264
+ text_resp, pcm = generate_response(recorder, model, tokenizer, mimi,
265
+ x, device, args.max_new_tokens)
266
+
267
+ with recorder.lock:
268
+ interrupted = recorder.interrupt
269
+ recorder.interrupt = False
270
+ recorder.state = 'playing' if not interrupted else 'idle'
271
+
272
+ if text_resp:
273
+ if not interrupted:
274
+ history.append({"role": "assistant", "content": text_resp})
275
+
276
+ if pcm is not None and len(pcm) > 0:
277
+ print(f' Playing... ({time.time() - st:.1f}s gen)', end=' ', flush=True)
278
+ sd.play(pcm, AUDIO_SR)
279
+ # Poll playback with interrupt check
280
+ while sd.get_stream().active:
281
+ with recorder.lock:
282
+ if recorder.interrupt:
283
+ sd.stop()
284
+ print('[interrupted]', end=' ')
285
+ with recorder.lock:
286
+ recorder.interrupt = False
287
+ recorder.state = 'idle'
288
+ break
289
+ time.sleep(0.05)
290
+ print('done')
291
+
292
+ with recorder.lock:
293
+ recorder.state = 'idle'
294
+
295
+ except KeyboardInterrupt:
296
+ print('\nBye!')
297
+ finally:
298
+ recorder.stop()
299
 
300
 
301
  if __name__ == '__main__':