-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathaudio_cli.py
More file actions
257 lines (211 loc) · 8.55 KB
/
Copy pathaudio_cli.py
File metadata and controls
257 lines (211 loc) · 8.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
from __future__ import annotations
import os
import asyncio
import logging
import argparse
import readline
import sounddevice as sd
import io
import wave
import numpy as np
import torch
from rich.console import Console
from rich.panel import Panel
from rich.theme import Theme
from rich.table import Table
from rich.live import Live
from rich.spinner import Spinner
from openai import AsyncOpenAI
from silero_vad import load_silero_vad
import aioconsole
from agent_manager import AgentManager
from dotenv import load_dotenv
parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true', help='Enable debug logging')
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.debug else logging.ERROR,
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
)
for logger_name in logging.root.manager.loggerDict:
logging.getLogger(logger_name).setLevel(logging.DEBUG if args.debug else logging.ERROR)
console = Console(theme=Theme({
"info": "grey70",
"warning": "yellow",
"error": "red",
"success": "grey74",
"command": "bold blue",
"highlight": "dark_orange3",
}))
print_queue = asyncio.Queue()
async def logger():
prompt = "\033[1;32m > \033[0m"
while True:
message = await print_queue.get()
if message is None:
break
if message == "__SHOW_PROMPT__":
console.file.write(prompt)
console.file.flush()
continue
# Clear current line and move to the beginning before printing
console.file.write("\r\033[K") # Clear current line
console.print(message)
console.file.flush()
async def safe_print(message):
await print_queue.put(message)
await print_queue.put("__SHOW_PROMPT__")
async def run_with_spinner(coroutine):
"""Run a coroutine with a thinking spinner displayed."""
spinner = Spinner("point", text="", style="bold green")
with Live(spinner, console=console, refresh_per_second=10, transient=True):
return await coroutine
client = AsyncOpenAI(api_key=os.environ['OPENAI_API_KEY'])
SAMPLE_RATE = 16000
CHANNELS = 1
RECORD_SECONDS = 5
audio_queue = asyncio.Queue()
vad_model = load_silero_vad()
def is_too_quiet(audio_np, rms_threshold=0.005):
return np.sqrt(np.mean(audio_np ** 2)) < rms_threshold
def is_voice_present(audio_np, sample_rate=16000, frame_size=512, threshold=0.9):
audio_tensor = torch.from_numpy(audio_np).float()
num_frames = len(audio_tensor) // frame_size
chunks = audio_tensor[:num_frames * frame_size].reshape(num_frames, frame_size)
with torch.no_grad():
probs = vad_model(chunks, sample_rate)
return (probs > threshold).any().item()
async def audio_stream():
loop = asyncio.get_event_loop()
while True:
audio = await loop.run_in_executor(
None, sd.rec, int(RECORD_SECONDS * SAMPLE_RATE), SAMPLE_RATE, CHANNELS, 'int16'
)
await loop.run_in_executor(None, sd.wait)
audio_np = audio.flatten().astype(np.float32) / 32768.0
if is_too_quiet(audio_np):
continue
if is_voice_present(audio_np):
await audio_queue.put(audio_np) # Put numpy array directly in queue instead of file path
async def transcribe_audio_buffer(audio_np):
audio_buffer = io.BytesIO()
with wave.open(audio_buffer, 'wb') as wf:
wf.setnchannels(CHANNELS)
wf.setsampwidth(2) # 16-bit audio
wf.setframerate(SAMPLE_RATE)
wf.writeframes((audio_np * 32767).astype(np.int16).tobytes())
audio_buffer.seek(0)
response = await client.audio.transcriptions.create(
model="whisper-1",
file=("audio.wav", audio_buffer), # Passing as tuple with filename
language="en"
)
return response.text
async def process_voice_input(agent_manager):
while True:
audio_np = await audio_queue.get()
voice_text = await transcribe_audio_buffer(audio_np)
await safe_print(f"[highlight] > [/] {voice_text}")
result, _ = await run_with_spinner(agent_manager.run_command(voice_text))
await safe_print(result)
async def process_text_input(agent_manager, cli):
loop = asyncio.get_event_loop()
while True:
user_input = await loop.run_in_executor(None, lambda: input(""))
if user_input.lower() in ['/exit', '/quit', '/bye']:
await safe_print("[info]Shutting down...[/info]")
raise asyncio.CancelledError()
elif user_input.lower() == '/help':
help_text = cli.get_help_text()
await safe_print(help_text)
elif user_input.lower() == '/clear':
console.clear()
elif user_input.lower() == '/reset':
agent_manager.reset_history()
await safe_print("[success]History reset[/success]")
elif user_input.lower() == '/tools':
tools_text = agent_manager.get_tools_table()
await safe_print(tools_text)
elif user_input.lower() == '/history':
history_text = agent_manager.get_history_table()
await safe_print(history_text)
elif user_input.startswith('/'):
await safe_print(f"[error]Unknown command: {user_input}[/error]")
else:
result, _ = await run_with_spinner(agent_manager.run_command(user_input))
await safe_print(result)
class CLI:
commands = ["/help", "/tools", "/history", "/clear", "/reset", "/exit", "/quit", "/bye"]
def display_help(self):
table = Table(title="Commands")
table.add_column("Command", style="command")
table.add_column("Description", style="info")
table.add_row("/help", "Show help")
table.add_row("/tools", "Show available tools")
table.add_row("/history", "Show conversation history")
table.add_row("/clear", "Clear screen")
table.add_row("/reset", "Reset conversation")
table.add_row("/exit", "Exit")
console.print(table)
def get_help_text(self):
table = Table(title="Commands")
table.add_column("Command", style="command")
table.add_column("Description", style="info")
table.add_row("/help", "Show help")
table.add_row("/tools", "Show available tools")
table.add_row("/history", "Show conversation history")
table.add_row("/clear", "Clear screen")
table.add_row("/reset", "Reset conversation")
table.add_row("/exit", "Exit")
return table
async def run_cli():
load_dotenv()
cli = CLI()
agent_manager = await AgentManager.initialize()
await safe_print(Panel.fit("[highlight]CLI with Audio Initialized[/highlight]"))
await safe_print(agent_manager.get_tools_table())
log_task = asyncio.create_task(logger())
tasks = []
try:
audio_task = asyncio.create_task(audio_stream())
voice_task = asyncio.create_task(process_voice_input(agent_manager))
text_task = asyncio.create_task(process_text_input(agent_manager, cli))
tasks = [audio_task, voice_task, text_task]
# Wait for any task to complete (which shouldn't happen unless there's an error)
await asyncio.gather(*tasks)
except asyncio.CancelledError:
pass
except KeyboardInterrupt:
pass
finally:
# Cancel all running tasks with a timeout
for task in tasks:
if not task.done():
task.cancel()
# Clean up with timeout
try:
# Wait briefly for tasks to be cancelled
if tasks:
await asyncio.wait(tasks, timeout=1.0)
# Modify the agent manager cleanup process to be more robust
cleanup_task = asyncio.create_task(agent_manager.cleanup())
await asyncio.wait_for(cleanup_task, timeout=3.0)
# Close the logger last
await print_queue.put(None) # Close logger
await asyncio.wait_for(log_task, timeout=1.0)
console.print("[success]Resources cleaned up successfully[/success]")
# Force exit to avoid hanging
os._exit(0)
except asyncio.TimeoutError:
console.print("[warning]Cleanup timed out, forcing exit[/warning]")
os._exit(1)
except Exception as e:
console.print(f"[error]Error during cleanup: {e}[/error]")
os._exit(1)
if __name__ == "__main__":
try:
asyncio.run(run_cli())
except KeyboardInterrupt:
print("\nExiting due to keyboard interrupt")
except Exception as e:
print(f"\nError: {e}")