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

Add omni_o_terminal.py: terminal voice chat script

Browse files

Command-line voice conversation using mic + Silero VAD + SenseVoice ASR
+ omni-o model + Mimi audio decoding. Supports auto VAD mode (silence
detection) and manual push-to-talk mode (--wait_key 1).

Files changed (1) hide show
  1. scripts/omni_o_terminal.py +274 -0
scripts/omni_o_terminal.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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')
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 SileroVAD
10
+
11
+ SAMPLE_RATE = 16000
12
+ AUDIO_SR = 24000
13
+ SAMPLES_PER_FRAME = 1920
14
+
15
+
16
+ def asr_run(model, samples):
17
+ from funasr.utils.postprocess_utils import rich_transcription_postprocess
18
+ r = model.generate(input=samples, cache={}, language='auto', use_itn=True)
19
+ return rich_transcription_postprocess(r[0]['text']).strip() if r else ''
20
+
21
+
22
+ def init_model(args):
23
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24
+ print('Loading ASR...')
25
+ with contextlib.redirect_stdout(io.StringIO()):
26
+ from funasr import AutoModel
27
+ asr = AutoModel(model=os.path.join(root, args.sensevoice_dir),
28
+ trust_remote_code=True, device=args.device,
29
+ disable_update=True, batch_size=1)
30
+
31
+ print('Loading model...')
32
+ config = VAMConfig(
33
+ hidden_size=args.hidden_size,
34
+ num_hidden_layers=args.num_hidden_layers,
35
+ num_attention_heads=args.hidden_size // 96,
36
+ num_key_value_heads=args.hidden_size // 192,
37
+ use_moe=args.use_moe,
38
+ )
39
+ ckpt_dir = os.path.join(root, args.load_from)
40
+ weight = args.weight
41
+ if not weight.endswith('.pth'):
42
+ if args.use_moe and not weight.endswith('_moe'):
43
+ weight = f'{weight}_moe.pth'
44
+ else:
45
+ weight = f'{weight}.pth'
46
+ ckpt_path = os.path.join(ckpt_dir, weight)
47
+
48
+ model = VAM(config,
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)
60
+
61
+ from transformers import AutoTokenizer
62
+ tokenizer = AutoTokenizer.from_pretrained(os.path.join(root, args.tokenizer_dir))
63
+ params = sum(p.numel() for p in model.parameters()) / 1e6
64
+ print(f' {args.weight}: {params:.2f}M')
65
+
66
+ print('Loading Mimi...')
67
+ from transformers import MimiModel
68
+ mimi = MimiModel.from_pretrained(os.path.join(root, args.mimi_dir)).eval().to(args.device)
69
+ if args.device != 'cpu':
70
+ mimi = mimi.half()
71
+
72
+ print('Loading VAD...')
73
+ vad = SileroVAD()
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)
141
+ with torch.no_grad():
142
+ audio = model.decode(codes).audio_values.squeeze().float().cpu().numpy()
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):]
163
+ if new_text:
164
+ print(new_text, end='', flush=True)
165
+ text_out = ans
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)
173
+ pcm = mimi_decode(mimi, codes_t, device)
174
+ return text_out, pcm
175
+ return text_out, None
176
+
177
+
178
+ def warmup(model, mimi, device):
179
+ with torch.no_grad():
180
+ ids = torch.tensor([[1, 2, 3]], device=device)
181
+ au = torch.full((1, 8, 3), 2049, dtype=torch.long, device=device)
182
+ model.forward(torch.cat((au, ids.unsqueeze(1)), dim=1))
183
+ if model.audio_encoder is not None:
184
+ try:
185
+ model.audio_encoder.model(
186
+ torch.zeros(1, 100, 560, device=device),
187
+ torch.tensor([100], device=device))
188
+ except Exception:
189
+ pass
190
+ if mimi is not None:
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')
199
+ parser.add_argument('--sensevoice_dir', default='checkpoint/sensevoice')
200
+ parser.add_argument('--siglip_dir', default='checkpoint/siglip')
201
+ parser.add_argument('--mimi_dir', default='checkpoint/mimi')
202
+ parser.add_argument('--hidden_size', default=768, type=int)
203
+ parser.add_argument('--num_hidden_layers', default=8, type=int)
204
+ parser.add_argument('--use_moe', default=0, type=int, choices=[0, 1])
205
+ parser.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
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__':
274
+ main()