bhai-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bhai_cli/__init__.py +9 -0
- bhai_cli/audio_engine.py +269 -0
- bhai_cli/cli.py +305 -0
- bhai_cli/config_manager.py +138 -0
- bhai_cli/orchestrator.py +233 -0
- bhai_cli/tools/__init__.py +124 -0
- bhai_cli/tools/bash.py +86 -0
- bhai_cli/tools/codebase.py +193 -0
- bhai_cli/tools/search.py +49 -0
- bhai_cli-0.1.0.dist-info/METADATA +228 -0
- bhai_cli-0.1.0.dist-info/RECORD +14 -0
- bhai_cli-0.1.0.dist-info/WHEEL +4 -0
- bhai_cli-0.1.0.dist-info/entry_points.txt +3 -0
- bhai_cli-0.1.0.dist-info/licenses/LICENSE +190 -0
bhai_cli/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BHAI-CLI: The Dual-Brain AI Coding Agent with Punjabi Swagger.
|
|
3
|
+
|
|
4
|
+
Lean heritage. Zero-state resilience. Universal LLM support via litellm.
|
|
5
|
+
Indic-native voice interface via Sarvam AI (Saaras v3 STT / Bulbul v3 TTS).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__app_name__ = "bhai-cli"
|
bhai_cli/audio_engine.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audio Engine for BHAI-CLI.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Live microphone recording via sounddevice (primary)
|
|
6
|
+
- File-based audio fallback (configurable)
|
|
7
|
+
- STT via Sarvam Saaras v3 (translate mode for code-mixed Punjabi/Hindi → English)
|
|
8
|
+
- TTS via Sarvam Bulbul v3 (English → Indian-accented speech)
|
|
9
|
+
|
|
10
|
+
Edge Case: If Sarvam fails, gracefully returns error flags so the orchestrator
|
|
11
|
+
can downgrade to text-only mode without crashing.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import base64
|
|
18
|
+
import io
|
|
19
|
+
import tempfile
|
|
20
|
+
import wave
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Sarvam API Constants
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
SARVAM_BASE = "https://api.sarvam.ai"
|
|
33
|
+
STT_ENDPOINT = f"{SARVAM_BASE}/speech-to-text"
|
|
34
|
+
TTS_ENDPOINT = f"{SARVAM_BASE}/text-to-speech"
|
|
35
|
+
|
|
36
|
+
SAMPLE_RATE = 16000 # Optimal for Saaras v3
|
|
37
|
+
CHANNELS = 1
|
|
38
|
+
DTYPE = "int16"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Audio Recording
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
class AudioRecorder:
|
|
46
|
+
"""Thread-safe live microphone recorder using sounddevice."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, sample_rate: int = SAMPLE_RATE, channels: int = CHANNELS):
|
|
49
|
+
self.sample_rate = sample_rate
|
|
50
|
+
self.channels = channels
|
|
51
|
+
self._frames: list = []
|
|
52
|
+
self._recording = False
|
|
53
|
+
self._stream = None
|
|
54
|
+
|
|
55
|
+
def _check_sounddevice(self):
|
|
56
|
+
"""Check if sounddevice is available (optional dependency)."""
|
|
57
|
+
try:
|
|
58
|
+
import sounddevice # noqa: F401
|
|
59
|
+
return True
|
|
60
|
+
except ImportError:
|
|
61
|
+
console.print(
|
|
62
|
+
"[bold red]⚠ Audio support not installed.[/] "
|
|
63
|
+
"Run: [cyan]pip install bhai-cli\\[audio][/]"
|
|
64
|
+
)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
def start(self) -> bool:
|
|
68
|
+
"""Start recording from microphone. Returns False if unavailable."""
|
|
69
|
+
if not self._check_sounddevice():
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
import sounddevice as sd
|
|
73
|
+
import numpy as np
|
|
74
|
+
|
|
75
|
+
self._frames = []
|
|
76
|
+
self._recording = True
|
|
77
|
+
|
|
78
|
+
def callback(indata, frames, time_info, status):
|
|
79
|
+
if self._recording:
|
|
80
|
+
self._frames.append(indata.copy())
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
self._stream = sd.InputStream(
|
|
84
|
+
samplerate=self.sample_rate,
|
|
85
|
+
channels=self.channels,
|
|
86
|
+
dtype=DTYPE,
|
|
87
|
+
callback=callback,
|
|
88
|
+
)
|
|
89
|
+
self._stream.start()
|
|
90
|
+
return True
|
|
91
|
+
except Exception as e:
|
|
92
|
+
console.print(f"[bold red]⚠ Microphone error:[/] {e}")
|
|
93
|
+
self._recording = False
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def stop(self) -> Optional[bytes]:
|
|
97
|
+
"""Stop recording and return WAV bytes, or None on failure."""
|
|
98
|
+
self._recording = False
|
|
99
|
+
if self._stream:
|
|
100
|
+
self._stream.stop()
|
|
101
|
+
self._stream.close()
|
|
102
|
+
self._stream = None
|
|
103
|
+
|
|
104
|
+
if not self._frames:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
import numpy as np
|
|
108
|
+
|
|
109
|
+
audio_data = np.concatenate(self._frames, axis=0)
|
|
110
|
+
|
|
111
|
+
# Convert to WAV bytes
|
|
112
|
+
buf = io.BytesIO()
|
|
113
|
+
with wave.open(buf, "wb") as wf:
|
|
114
|
+
wf.setnchannels(self.channels)
|
|
115
|
+
wf.setsampwidth(2) # 16-bit = 2 bytes
|
|
116
|
+
wf.setframerate(self.sample_rate)
|
|
117
|
+
wf.writeframes(audio_data.tobytes())
|
|
118
|
+
|
|
119
|
+
return buf.getvalue()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_audio_file(filepath: str) -> Optional[bytes]:
|
|
123
|
+
"""Load audio from a file path as bytes. Returns None on failure."""
|
|
124
|
+
path = Path(filepath)
|
|
125
|
+
if not path.exists():
|
|
126
|
+
console.print(f"[bold red]⚠ Audio file not found:[/] {filepath}")
|
|
127
|
+
return None
|
|
128
|
+
try:
|
|
129
|
+
return path.read_bytes()
|
|
130
|
+
except Exception as e:
|
|
131
|
+
console.print(f"[bold red]⚠ Failed to read audio file:[/] {e}")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Sarvam STT (Saaras v3)
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
async def speech_to_text(
|
|
140
|
+
audio_bytes: bytes,
|
|
141
|
+
api_key: str,
|
|
142
|
+
model: str = "saaras:v3",
|
|
143
|
+
mode: str = "translate",
|
|
144
|
+
) -> tuple[bool, str]:
|
|
145
|
+
"""
|
|
146
|
+
Transcribe/translate audio using Sarvam Saaras v3.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
audio_bytes: WAV audio data.
|
|
150
|
+
api_key: Sarvam API subscription key.
|
|
151
|
+
model: STT model identifier.
|
|
152
|
+
mode: transcribe | translate | verbatim | translit | codemix
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
(success: bool, text_or_error: str)
|
|
156
|
+
"""
|
|
157
|
+
if not api_key:
|
|
158
|
+
return False, "[STT_ERROR] No Sarvam API key configured. Run: bhai-code setup"
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
162
|
+
response = await client.post(
|
|
163
|
+
STT_ENDPOINT,
|
|
164
|
+
headers={"api-subscription-key": api_key},
|
|
165
|
+
files={"file": ("audio.wav", audio_bytes, "audio/wav")},
|
|
166
|
+
data={"model": model, "mode": mode},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if response.status_code == 200:
|
|
170
|
+
data = response.json()
|
|
171
|
+
transcript = data.get("transcript", "")
|
|
172
|
+
if transcript:
|
|
173
|
+
return True, transcript
|
|
174
|
+
return False, "[STT_ERROR] Empty transcript returned."
|
|
175
|
+
else:
|
|
176
|
+
return False, f"[STT_ERROR] Sarvam API returned {response.status_code}: {response.text}"
|
|
177
|
+
|
|
178
|
+
except httpx.TimeoutException:
|
|
179
|
+
return False, "[STT_ERROR] Sarvam STT request timed out."
|
|
180
|
+
except Exception as e:
|
|
181
|
+
return False, f"[STT_ERROR] {type(e).__name__}: {e}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Sarvam TTS (Bulbul v3)
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
async def text_to_speech(
|
|
189
|
+
text: str,
|
|
190
|
+
api_key: str,
|
|
191
|
+
model: str = "bulbul:v3",
|
|
192
|
+
speaker: str = "shubh",
|
|
193
|
+
language: str = "en-IN",
|
|
194
|
+
) -> tuple[bool, Optional[bytes]]:
|
|
195
|
+
"""
|
|
196
|
+
Convert text to speech using Sarvam Bulbul v3.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
text: Text to speak (max 2500 chars).
|
|
200
|
+
api_key: Sarvam API subscription key.
|
|
201
|
+
model: TTS model identifier.
|
|
202
|
+
speaker: Voice speaker name.
|
|
203
|
+
language: Target language code.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
(success: bool, audio_bytes_or_none)
|
|
207
|
+
"""
|
|
208
|
+
if not api_key:
|
|
209
|
+
return False, None
|
|
210
|
+
|
|
211
|
+
# Truncate to API limit
|
|
212
|
+
text = text[:2500]
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
payload = {
|
|
216
|
+
"text": text,
|
|
217
|
+
"target_language_code": language,
|
|
218
|
+
"speaker": speaker,
|
|
219
|
+
"model": model,
|
|
220
|
+
"speech_sample_rate": 24000,
|
|
221
|
+
"pace": 1.0,
|
|
222
|
+
"temperature": 0.6,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
226
|
+
response = await client.post(
|
|
227
|
+
TTS_ENDPOINT,
|
|
228
|
+
headers={
|
|
229
|
+
"api-subscription-key": api_key,
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
},
|
|
232
|
+
json=payload,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if response.status_code == 200:
|
|
236
|
+
data = response.json()
|
|
237
|
+
audios = data.get("audios", [])
|
|
238
|
+
if audios:
|
|
239
|
+
audio_bytes = base64.b64decode(audios[0])
|
|
240
|
+
return True, audio_bytes
|
|
241
|
+
return False, None
|
|
242
|
+
else:
|
|
243
|
+
console.print(
|
|
244
|
+
f"[dim]TTS warning: Sarvam returned {response.status_code}[/]"
|
|
245
|
+
)
|
|
246
|
+
return False, None
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
console.print(f"[dim]TTS fallback: {type(e).__name__}: {e}[/]")
|
|
250
|
+
return False, None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
async def play_audio(audio_bytes: bytes) -> None:
|
|
254
|
+
"""Play audio bytes through the default output device."""
|
|
255
|
+
try:
|
|
256
|
+
import sounddevice as sd
|
|
257
|
+
import soundfile as sf
|
|
258
|
+
|
|
259
|
+
buf = io.BytesIO(audio_bytes)
|
|
260
|
+
data, samplerate = sf.read(buf)
|
|
261
|
+
sd.play(data, samplerate)
|
|
262
|
+
sd.wait()
|
|
263
|
+
except ImportError:
|
|
264
|
+
# Save to temp file and play with system player as fallback
|
|
265
|
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
|
|
266
|
+
f.write(audio_bytes)
|
|
267
|
+
console.print(f"[dim]Audio saved to: {f.name}[/]")
|
|
268
|
+
except Exception as e:
|
|
269
|
+
console.print(f"[dim]Audio playback failed: {e}[/]")
|
bhai_cli/cli.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BHAI-CLI — Typer CLI Application.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
bhai-code setup — Interactive configuration wizard
|
|
6
|
+
bhai-code text — Send a text prompt to BHAI
|
|
7
|
+
bhai-code listen — Voice input via Sarvam STT → BHAI pipeline
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
import pyfiglet
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.prompt import Prompt
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
from rich.text import Text
|
|
22
|
+
|
|
23
|
+
from bhai_cli import __version__
|
|
24
|
+
from bhai_cli.config_manager import (
|
|
25
|
+
load_config,
|
|
26
|
+
save_config,
|
|
27
|
+
CONFIG_FILE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# CLI Setup
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
# "BHAI" in 6 Indian scripts
|
|
37
|
+
BHAI_LANGS = [
|
|
38
|
+
("भाई", "Hindi"),
|
|
39
|
+
("ভাই", "Bengali"),
|
|
40
|
+
("ભાઈ", "Gujarati"),
|
|
41
|
+
("ਭਾਈ", "Punjabi"),
|
|
42
|
+
("భాయ్", "Telugu"),
|
|
43
|
+
("ಭಾಯ್", "Kannada"),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def print_banner() -> None:
|
|
48
|
+
"""Print the BHAI-CLI ASCII banner with multilingual flair."""
|
|
49
|
+
ascii_banner = pyfiglet.figlet_format("BHAI - CLI", font="slant")
|
|
50
|
+
banner_text = Text(ascii_banner, style="bold bright_cyan")
|
|
51
|
+
console.print(banner_text)
|
|
52
|
+
|
|
53
|
+
# Show "BHAI" in multiple Indian scripts
|
|
54
|
+
lang_line = " · ".join(f"[bold]{word}[/][dim]({lang})[/]" for word, lang in BHAI_LANGS)
|
|
55
|
+
console.print(f" {lang_line}\n")
|
|
56
|
+
|
|
57
|
+
console.print(
|
|
58
|
+
"[bold yellow]🇮🇳 The Dual-Brain AI Coding Agent — 22 Indian Languages.[/]\n"
|
|
59
|
+
"[dim]Powered by litellm & Sarvam AI | v" + __version__ + "[/]\n"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
app = typer.Typer(
|
|
63
|
+
name="bhai-code",
|
|
64
|
+
help="BHAI — Your 10x Senior SDE from India. 22 Languages. One CLI.",
|
|
65
|
+
add_completion=False,
|
|
66
|
+
rich_markup_mode="rich",
|
|
67
|
+
no_args_is_help=True,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _version_callback(value: bool) -> None:
|
|
71
|
+
if value:
|
|
72
|
+
print_banner()
|
|
73
|
+
raise typer.Exit()
|
|
74
|
+
|
|
75
|
+
@app.callback(invoke_without_command=True)
|
|
76
|
+
def main(
|
|
77
|
+
ctx: typer.Context,
|
|
78
|
+
version: bool = typer.Option(
|
|
79
|
+
False, "--version", "-v",
|
|
80
|
+
help="Show version and exit.",
|
|
81
|
+
callback=_version_callback,
|
|
82
|
+
is_eager=True,
|
|
83
|
+
),
|
|
84
|
+
) -> None:
|
|
85
|
+
"""[bold cyan]BHAI-CLI[/] — 10x Senior SDE Agent from India. 22 Languages."""
|
|
86
|
+
# Only print banner if no subcommand was invoked (which means help will be shown automatically via no_args_is_help)
|
|
87
|
+
if ctx.invoked_subcommand is None:
|
|
88
|
+
print_banner()
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# bhai-code setup
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
@app.command()
|
|
95
|
+
def setup() -> None:
|
|
96
|
+
"""[bold yellow]⚙️ setup[/] — Interactive wizard for VibeLLM, CoderAgent, and Sarvam AI."""
|
|
97
|
+
print_banner()
|
|
98
|
+
|
|
99
|
+
console.print(
|
|
100
|
+
Panel(
|
|
101
|
+
"[bold white]Let's get your dual-brain engine running, veere.[/]\n"
|
|
102
|
+
"[dim]We need to configure your Persona (Brain A) and your Execution Engine (Brain B).[/]",
|
|
103
|
+
title="[bold yellow]Setup Wizard[/]",
|
|
104
|
+
border_style="yellow",
|
|
105
|
+
padding=(1, 2),
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
cfg = load_config()
|
|
110
|
+
|
|
111
|
+
console.print("\n[bold cyan]━━━ Brain A: VibeLLM (Persona & Intent) ━━━[/]")
|
|
112
|
+
console.print(
|
|
113
|
+
"[dim]Handles the BHAI persona and parses your intent.\n"
|
|
114
|
+
"Format: provider/model-name (e.g., sarvam/sarvam-105b, groq/llama-3.3-70b-versatile)[/]\n"
|
|
115
|
+
)
|
|
116
|
+
cfg.vibe.model = Prompt.ask("VibeLLM model", default=cfg.vibe.model)
|
|
117
|
+
cfg.vibe.api_key = Prompt.ask("VibeLLM API key", default=cfg.vibe.api_key or "", password=True)
|
|
118
|
+
cfg.vibe.api_base = Prompt.ask("VibeLLM API base URL [dim](optional)[/]", default=cfg.vibe.api_base or "")
|
|
119
|
+
|
|
120
|
+
console.print("\n[bold cyan]━━━ Brain B: CoderAgent (Tool Execution) ━━━[/]")
|
|
121
|
+
console.print(
|
|
122
|
+
"[dim]Executes coding tasks using MCP-style tools. Must support tool-calling.\n"
|
|
123
|
+
"Recommended: groq/llama-3.3-70b-versatile, openai/gpt-4o, anthropic/claude-sonnet-4-20250514[/]\n"
|
|
124
|
+
)
|
|
125
|
+
cfg.coder.model = Prompt.ask("CoderAgent model", default=cfg.coder.model)
|
|
126
|
+
cfg.coder.api_key = Prompt.ask("CoderAgent API key", default=cfg.coder.api_key or "", password=True)
|
|
127
|
+
cfg.coder.api_base = Prompt.ask("CoderAgent API base URL [dim](optional)[/]", default=cfg.coder.api_base or "")
|
|
128
|
+
|
|
129
|
+
console.print("\n[bold cyan]━━━ Sarvam AI (Voice Interface) ━━━[/]")
|
|
130
|
+
console.print("[dim]Optional: Enables voice input (STT) and output (TTS).[/]\n")
|
|
131
|
+
cfg.sarvam.api_key = Prompt.ask("Sarvam API key [dim](leave empty to skip)[/]", default=cfg.sarvam.api_key or "", password=True)
|
|
132
|
+
|
|
133
|
+
path = save_config(cfg)
|
|
134
|
+
|
|
135
|
+
console.print()
|
|
136
|
+
console.print(
|
|
137
|
+
Panel(
|
|
138
|
+
f"[bold green]✅ All set, boss! Config saved to:[/] {path}\n\n"
|
|
139
|
+
f"🧠 [bold]VibeLLM:[/] {cfg.vibe.model}\n"
|
|
140
|
+
f"🛠️ [bold]CoderAgent:[/] {cfg.coder.model}\n"
|
|
141
|
+
f"🎙️ [bold]Sarvam:[/] {'Active ✓' if cfg.sarvam.api_key else 'Skipped (Text-only)'}",
|
|
142
|
+
title="[bold green]Setup Complete[/]",
|
|
143
|
+
border_style="green",
|
|
144
|
+
padding=(1, 2),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# bhai-code text
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
@app.command()
|
|
153
|
+
def text(
|
|
154
|
+
prompt: str = typer.Argument(..., help="Your message to BHAI."),
|
|
155
|
+
vibe_model: Optional[str] = typer.Option(None, "--vibe-model", "-vm", help="Override VibeLLM model."),
|
|
156
|
+
coder_model: Optional[str] = typer.Option(None, "--coder-model", "-cm", help="Override CoderAgent model."),
|
|
157
|
+
interactive: bool = typer.Option(False, "--interactive", "-i", help="Enter interactive chat mode."),
|
|
158
|
+
) -> None:
|
|
159
|
+
"""[bold magenta]💬 text[/] — Send a text prompt to BHAI."""
|
|
160
|
+
print_banner()
|
|
161
|
+
from bhai_cli.orchestrator import BhaiOrchestrator
|
|
162
|
+
|
|
163
|
+
cfg = load_config(vibe_model_override=vibe_model, coder_model_override=coder_model)
|
|
164
|
+
orchestrator = BhaiOrchestrator(cfg)
|
|
165
|
+
|
|
166
|
+
asyncio.run(orchestrator.process(prompt))
|
|
167
|
+
|
|
168
|
+
if interactive:
|
|
169
|
+
console.print("\n[bold cyan]Interactive mode — type 'exit' or 'quit' to leave.[/]\n")
|
|
170
|
+
while True:
|
|
171
|
+
try:
|
|
172
|
+
user_input = Prompt.ask("[bold green]You[/]")
|
|
173
|
+
if user_input.strip().lower() in ("exit", "quit", "bye", "chal hatja", "nikal"):
|
|
174
|
+
console.print("\n[bold yellow]BHAI:[/] Accha bhai, phir milenge! 🤙")
|
|
175
|
+
break
|
|
176
|
+
asyncio.run(orchestrator.process(user_input))
|
|
177
|
+
except (KeyboardInterrupt, EOFError):
|
|
178
|
+
console.print("\n[bold yellow]BHAI:[/] Theek hai boss, baad mein baat karte hain! 👋")
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# bhai-code listen
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
@app.command()
|
|
186
|
+
def listen(
|
|
187
|
+
vibe_model: Optional[str] = typer.Option(None, "--vibe-model", "-vm", help="Override VibeLLM model."),
|
|
188
|
+
coder_model: Optional[str] = typer.Option(None, "--coder-model", "-cm", help="Override CoderAgent model."),
|
|
189
|
+
file: Optional[str] = typer.Option(None, "--file", "-f", help="Path to an audio file instead of live mic."),
|
|
190
|
+
) -> None:
|
|
191
|
+
"""[bold red]🎙️ listen[/] — Voice input — speak to BHAI in any Indian language."""
|
|
192
|
+
print_banner()
|
|
193
|
+
from bhai_cli.orchestrator import BhaiOrchestrator
|
|
194
|
+
from bhai_cli.audio_engine import (
|
|
195
|
+
AudioRecorder, load_audio_file, speech_to_text, text_to_speech, play_audio,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
cfg = load_config(vibe_model_override=vibe_model, coder_model_override=coder_model)
|
|
199
|
+
|
|
200
|
+
if not cfg.sarvam.api_key:
|
|
201
|
+
console.print(
|
|
202
|
+
Panel(
|
|
203
|
+
"[bold red]⚠ Sarvam API key not configured.[/]\n"
|
|
204
|
+
"Run [cyan]bhai-code setup[/] to add your Sarvam key,\n"
|
|
205
|
+
"or use [cyan]bhai-code text[/] for text-only mode.",
|
|
206
|
+
title="[bold red]Error[/]", border_style="red"
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
raise typer.Exit(1)
|
|
210
|
+
|
|
211
|
+
orchestrator = BhaiOrchestrator(cfg)
|
|
212
|
+
audio_bytes = None
|
|
213
|
+
audio_file = file or cfg.audio_fallback_file
|
|
214
|
+
|
|
215
|
+
if audio_file:
|
|
216
|
+
console.print(f"[dim]Loading audio from: {audio_file}[/]")
|
|
217
|
+
audio_bytes = load_audio_file(audio_file)
|
|
218
|
+
if audio_bytes is None:
|
|
219
|
+
raise typer.Exit(1)
|
|
220
|
+
else:
|
|
221
|
+
recorder = AudioRecorder()
|
|
222
|
+
console.print(
|
|
223
|
+
Panel(
|
|
224
|
+
"[bold white]Press ENTER to start recording, then ENTER again to stop.[/]",
|
|
225
|
+
title="[bold magenta]🎙️ Live Recording[/]",
|
|
226
|
+
border_style="magenta", padding=(1, 2),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
input()
|
|
230
|
+
|
|
231
|
+
if not recorder.start():
|
|
232
|
+
console.print("[bold red]Failed to start recording. Falling back to text mode.[/]")
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
|
|
235
|
+
console.print("[bold red blink]🔴 Recording... Press ENTER to stop.[/]")
|
|
236
|
+
input()
|
|
237
|
+
|
|
238
|
+
audio_bytes = recorder.stop()
|
|
239
|
+
if audio_bytes is None:
|
|
240
|
+
console.print("[bold red]No audio captured.[/]")
|
|
241
|
+
raise typer.Exit(1)
|
|
242
|
+
console.print("[bold green]✅ Audio captured successfully.[/]")
|
|
243
|
+
|
|
244
|
+
with console.status("[bold cyan]Transcribing with Sarvam Saaras v3... 🎧[/]"):
|
|
245
|
+
success, transcript = asyncio.run(
|
|
246
|
+
speech_to_text(audio_bytes, api_key=cfg.sarvam.api_key, model=cfg.sarvam.stt_model, mode=cfg.sarvam.stt_mode)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if not success:
|
|
250
|
+
console.print(f"[bold red]STT failed:[/] {transcript}")
|
|
251
|
+
console.print("[dim]Falling back to text mode. Use bhai-code text instead.[/]")
|
|
252
|
+
raise typer.Exit(1)
|
|
253
|
+
|
|
254
|
+
console.print(f"[bold green]📝 You said:[/] {transcript}\n")
|
|
255
|
+
result = asyncio.run(orchestrator.process(transcript))
|
|
256
|
+
|
|
257
|
+
async def _speak(text: str) -> None:
|
|
258
|
+
ok, audio = await text_to_speech(
|
|
259
|
+
text, api_key=cfg.sarvam.api_key, model=cfg.sarvam.tts_model,
|
|
260
|
+
speaker=cfg.sarvam.tts_speaker, language=cfg.sarvam.tts_language,
|
|
261
|
+
)
|
|
262
|
+
if ok and audio:
|
|
263
|
+
with console.status("[bold magenta]Speaking... 🔊[/]"):
|
|
264
|
+
await play_audio(audio)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
asyncio.run(_speak(result[:2500]))
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
# bhai-code config
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
@app.command()
|
|
276
|
+
def config() -> None:
|
|
277
|
+
"""[bold cyan]📋 config[/] — Show current configuration."""
|
|
278
|
+
print_banner()
|
|
279
|
+
cfg = load_config()
|
|
280
|
+
|
|
281
|
+
table = Table(title="BHAI Configuration Engine", show_header=True, header_style="bold cyan", border_style="dim", expand=True)
|
|
282
|
+
table.add_column("Key", style="bold white")
|
|
283
|
+
table.add_column("Value", style="cyan")
|
|
284
|
+
|
|
285
|
+
table.add_row("Config File", str(CONFIG_FILE))
|
|
286
|
+
table.add_section()
|
|
287
|
+
table.add_row("[yellow]VibeLLM Model[/]", cfg.vibe.model)
|
|
288
|
+
table.add_row("VibeLLM API Key", "••••" + cfg.vibe.api_key[-4:] if len(cfg.vibe.api_key) > 4 else "[red](not set)[/]")
|
|
289
|
+
table.add_row("VibeLLM API Base", cfg.vibe.api_base or "[dim](default)[/]")
|
|
290
|
+
table.add_section()
|
|
291
|
+
table.add_row("[yellow]CoderAgent Model[/]", cfg.coder.model)
|
|
292
|
+
table.add_row("CoderAgent API Key", "••••" + cfg.coder.api_key[-4:] if len(cfg.coder.api_key) > 4 else "[red](not set)[/]")
|
|
293
|
+
table.add_row("CoderAgent API Base", cfg.coder.api_base or "[dim](default)[/]")
|
|
294
|
+
table.add_section()
|
|
295
|
+
table.add_row("[yellow]Sarvam API Key[/]", "••••" + cfg.sarvam.api_key[-4:] if len(cfg.sarvam.api_key) > 4 else "[red](not set)[/]")
|
|
296
|
+
table.add_row("STT Model", cfg.sarvam.stt_model)
|
|
297
|
+
table.add_row("TTS Model", cfg.sarvam.tts_model)
|
|
298
|
+
table.add_section()
|
|
299
|
+
table.add_row("Command Timeout", f"{cfg.command_timeout}s")
|
|
300
|
+
|
|
301
|
+
console.print(table)
|
|
302
|
+
console.print()
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
app()
|