Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af6f926019 |
+11
-4
@@ -1,7 +1,14 @@
|
|||||||
venv/
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.DS_Store
|
.venv/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
data/*.log
|
||||||
|
data/transcriptions/
|
||||||
|
data/speakers/*.npy
|
||||||
|
data/faces/*.npy
|
||||||
|
models/
|
||||||
*.wav
|
*.wav
|
||||||
*.mp3
|
.DS_Store
|
||||||
logs/
|
/tmp/
|
||||||
|
jarvis.yaml
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Jarvis v2 - Final Setup (iMac Intel i3 Ubuntu 26.04)
|
||||||
|
|
||||||
|
## What we built vs v1
|
||||||
|
v1 (Jarvis repo main):
|
||||||
|
- monolithic jarvis.py, macOS only, mlx-whisper (Apple Silicon), openWakeWord, gTTS, say command, Gemini CLI
|
||||||
|
- No speaker ID, no vision, blocking pipeline
|
||||||
|
|
||||||
|
v2 (Jarvis repo -> branch v2):
|
||||||
|
- Modular: audio/capture (PipeWire pw-record backend, no PortAudio needed), VAD (silero + Energy fallback), wakeword (openWakeWord TFLite hey_jarvis 0.4), STT (faster-whisper base INT8 CPU), SpeakerID (ECAPA-TDNN speechbrain + stats fallback), Vision (OpenCV Haar + face_recognition), LLM pluggable (ollama qwen2.5:3b / gemini_cli / hermes_api / echo), TTS (Piper ONNX + espeak + echo)
|
||||||
|
- Pipeline state machine: IDLE -> WAKE -> RECORDING (with pre-roll 0.8s) -> TRANSCRIBING -> IDENTIFYING (voice+face fusion) -> THINKING -> SPEAKING -> FOLLOWUP window (12s no wake needed)
|
||||||
|
- Tested on iMac: mic via pw-record 16k mono OK, VAD energy OK, faster-whisper base int8 loads, cam0 640x480 OK, SpeakerStore empty (ready to enroll), Echo LLM/TTS OK
|
||||||
|
|
||||||
|
## iMac paths
|
||||||
|
~/Projects/jarvis-v2
|
||||||
|
.venv/ (uv, python 3.14, torch CPU, faster-whisper, openwakeword, opencv-headless etc)
|
||||||
|
jarvis.yaml (active config) + jarvis.yaml.example
|
||||||
|
data/soul.md (persona)
|
||||||
|
data/speakers/ (enroll .npy + .json)
|
||||||
|
data/faces/ (enroll .npy + .json)
|
||||||
|
data/transcriptions/YYYY-MM-DD.log
|
||||||
|
models/ (Piper voices, gitignored)
|
||||||
|
systemd/jarvis.service (user service)
|
||||||
|
|
||||||
|
## Commands on iMac
|
||||||
|
export PATH=$HOME/.local/bin:$PATH
|
||||||
|
cd ~/Projects/jarvis-v2
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# List audio
|
||||||
|
jarvis devices
|
||||||
|
|
||||||
|
# Mic level test
|
||||||
|
jarvis test-mic
|
||||||
|
# VAD test
|
||||||
|
jarvis test-vad
|
||||||
|
|
||||||
|
# Low-level wav test
|
||||||
|
python scripts/test_mic.py
|
||||||
|
python scripts/test_wakeword.py
|
||||||
|
python scripts/test_stt.py
|
||||||
|
|
||||||
|
# Enroll voices (do 3 samples per person for robustness, quiet room)
|
||||||
|
jarvis enroll-speaker --name Adolfo --mic
|
||||||
|
jarvis enroll-speaker --name Grace --mic
|
||||||
|
jarvis enroll-speaker --name Elias --mic
|
||||||
|
# or from file: jarvis enroll-speaker --name Adolfo --file /path/to/wav (16k mono)
|
||||||
|
|
||||||
|
# Enroll faces
|
||||||
|
jarvis enroll-face --name Adolfo
|
||||||
|
jarvis enroll-face --name Grace
|
||||||
|
jarvis enroll-face --name Elias
|
||||||
|
|
||||||
|
# Run assistant (echo brain - placeholder)
|
||||||
|
jarvis run
|
||||||
|
# Later with ollama
|
||||||
|
# install ollama: curl -fsSL https://ollama.com/install.sh | sh
|
||||||
|
# ollama pull qwen2.5:3b
|
||||||
|
# edit jarvis.yaml llm.provider=ollama
|
||||||
|
# jarvis run
|
||||||
|
|
||||||
|
# Systemd always-on
|
||||||
|
./scripts/install_service.sh
|
||||||
|
systemctl --user start jarvis
|
||||||
|
journalctl --user -u jarvis -f
|
||||||
|
systemctl --user stop jarvis
|
||||||
|
systemctl --user disable jarvis
|
||||||
|
|
||||||
|
## Configurable tiny-LLM gate (future)
|
||||||
|
In jarvis.yaml wakeword.allow_followup_without_wake + conversation.followup_timeout
|
||||||
|
Future: add local 0.5B Qwen to decide if utterance is addressed to Jarvis even without wakeword (use voice activity + speaker gaze)
|
||||||
|
|
||||||
|
## Piper TTS voices
|
||||||
|
.venv/bin/python -m piper.download_voices en_US-lessac-medium
|
||||||
|
# then edit jarvis.yaml tts.engine=piper tts.voice=en_US-lessac-medium
|
||||||
|
|
||||||
|
## Next steps priority
|
||||||
|
1. Enroll speakers (you + kids)
|
||||||
|
2. Enroll faces (webcam)
|
||||||
|
3. Install Ollama + qwen2.5:3b for local LLM
|
||||||
|
4. Test with wakeword: say "Hey Jarvis" + command
|
||||||
|
5. Install Piper voice for natural TTS
|
||||||
|
6. (Optional) Build PortAudio in ~/.local if you want sounddevice backend: ./scripts/build_portaudio.sh; export LD_LIBRARY_PATH=$HOME/.local/lib:$LD_LIBRARY_PATH
|
||||||
|
7. Create repo Jarvis-v2 in Gitea web UI (private) then push main: git push origin main
|
||||||
|
|
||||||
|
## Gitea
|
||||||
|
Current: Jarvis repo branch v2 contains full v2 code (pushed)
|
||||||
|
Missing: Jarvis-v2 new repo creation blocked by server setting "Push to create is not enabled for users." Need to create via Gitea UI, then we can push main.
|
||||||
@@ -1,43 +1,63 @@
|
|||||||
# Jarvis Voice Assistant (openWakeWord + Gemini CLI)
|
# Jarvis v2 - Always-Listening Assistant (CPU)
|
||||||
|
|
||||||
This project uses `openWakeWord` to listen for the "Hey Jarvis" wake word and then uses `SpeechRecognition` to capture a command and send it to the `gemini` CLI.
|
Designed for Intel iMac Ubuntu 26.04, 15GB RAM, CPU-only.
|
||||||
|
|
||||||
## Setup
|
## Architecture
|
||||||
|
```
|
||||||
1. **Install PortAudio** (macOS):
|
Mic 16kHz -> VAD (silero) -> Wakeword (openWakeWord TFLite) -> STT (faster-whisper int8) -> SpeakerID (ECAPA) + FaceID (OpenCV) -> LLM (ollama/gemini_cli/hermes) -> TTS (piper) -> Speaker
|
||||||
```bash
|
|
||||||
brew install portaudio
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Create and Activate Virtual Environment**:
|
|
||||||
```bash
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Install Dependencies**:
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Verify Gemini CLI**:
|
|
||||||
Ensure `gemini` is installed and available in your PATH.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Run the script:
|
|
||||||
```bash
|
|
||||||
source venv/bin/activate
|
|
||||||
python jarvis.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Say "**Hey Jarvis**".
|
## Quick start on iMac
|
||||||
2. You will hear a "tink" sound.
|
|
||||||
3. Speak your command (e.g., "List the files in this directory" or "Check the weather").
|
|
||||||
4. The script will transcribe your command and run `gemini "<your command>"`.
|
|
||||||
|
|
||||||
## How it Works
|
```bash
|
||||||
|
cd ~/Projects/jarvis-v2
|
||||||
|
# install uv if not present
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
export PATH=$HOME/.local/bin:$PATH
|
||||||
|
uv venv .venv --python 3.14
|
||||||
|
source .venv/bin/activate
|
||||||
|
uv pip install -e .
|
||||||
|
|
||||||
- **openWakeWord**: Provides local, low-latency wake word detection.
|
# for audio libs you may need (if sudo works, otherwise pip wheels should work)
|
||||||
- **SpeechRecognition**: Uses Google's Web Speech API for transcription.
|
# sudo apt install portaudio19-dev libasound2-dev espeak
|
||||||
- **Gemini CLI**: The brain that processes the commands and calls agent tools.
|
|
||||||
|
# configure
|
||||||
|
cp jarvis.yaml.example jarvis.yaml
|
||||||
|
# edit jarvis.yaml
|
||||||
|
|
||||||
|
# Test components
|
||||||
|
python scripts/test_mic.py
|
||||||
|
python scripts/test_wakeword.py
|
||||||
|
python scripts/test_stt.py
|
||||||
|
|
||||||
|
# Enroll family voices (3 samples each)
|
||||||
|
python scripts/enroll_speaker.py --name Adolfo --mic
|
||||||
|
python scripts/enroll_speaker.py --name Grace --mic
|
||||||
|
python scripts/enroll_speaker.py --name Elias --mic
|
||||||
|
|
||||||
|
# Enroll faces
|
||||||
|
python scripts/enroll_face.py --name Adolfo
|
||||||
|
python scripts/enroll_face.py --name Grace
|
||||||
|
|
||||||
|
# Run
|
||||||
|
jarvis run
|
||||||
|
# or
|
||||||
|
python -m jarvis.cli run --config jarvis.yaml
|
||||||
|
|
||||||
|
# Install as systemd user service (always-on)
|
||||||
|
./scripts/install_service.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
See jarvis.yaml.example - covers audio, VAD, wakeword, STT, speaker, vision, LLM, TTS.
|
||||||
|
|
||||||
|
## LLM Providers
|
||||||
|
- ollama: local qwen2.5:3b (install ollama then `ollama pull qwen2.5:3b`)
|
||||||
|
- gemini_cli: uses existing gemini CLI + soul.md
|
||||||
|
- hermes_api: points to Hermes API
|
||||||
|
- echo: placeholder
|
||||||
|
|
||||||
|
## Future
|
||||||
|
- voice trigger via small LLM gate (qwen0.5B decides if addressed)
|
||||||
|
- Face corroboration on wake
|
||||||
|
- Home Assistant / MCP tools
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# J.A.R.V.I.S. v2 - The Sophisticated Assistant
|
||||||
|
|
||||||
|
## Persona
|
||||||
|
You are J.A.R.V.I.S., the sophisticated, witty, highly capable AI assistant living on an iMac in the family home. You are calm, helpful, concise (you are speaking, not writing essays), with dry British wit. You adapt to who is speaking: Adolfo (Sir), Grace (young lady, encouraging), Elias (young sir, fun). If unknown, be politely curious.
|
||||||
|
|
||||||
|
## Directives
|
||||||
|
1. Be concise - voice output. One to three sentences unless asked for more.
|
||||||
|
2. No markdown in speech output - no * or # or backticks.
|
||||||
|
3. Personalize based on speaker identification confidence provided.
|
||||||
|
4. For tools/actions, state intent clearly and ask confirmation for destructive/sensitive actions.
|
||||||
|
5. You have vision when available - you can see via webcam on request.
|
||||||
|
6. You were woken by "Hey Jarvis" - you may have conversation context history.
|
||||||
|
7. Remember: you are always listening but only active after wake word.
|
||||||
|
|
||||||
|
## Knowledge
|
||||||
|
- Family in Florida, iMac in home office.
|
||||||
|
- Kids: Grace and Elias.
|
||||||
|
- User prefers concise, not verbose.
|
||||||
|
- Brain vault at ~/brain if referenced.
|
||||||
|
|
||||||
|
If speaker confidence is low or unknown, ask who you are speaking with once, remember for conversation.
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
import queue
|
|
||||||
import threading
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
# Comprehensive workaround for missing _lzma in some Python builds
|
|
||||||
try:
|
|
||||||
import lzma
|
|
||||||
except ImportError:
|
|
||||||
mock_lzma = MagicMock()
|
|
||||||
mock_lzma.FORMAT_XZ = 1
|
|
||||||
mock_lzma.FORMAT_ALONE = 2
|
|
||||||
mock_lzma.FORMAT_RAW = 3
|
|
||||||
mock_lzma.CHECK_NONE = 0
|
|
||||||
mock_lzma.CHECK_CRC32 = 1
|
|
||||||
mock_lzma.CHECK_CRC64 = 4
|
|
||||||
mock_lzma.CHECK_SHA256 = 10
|
|
||||||
sys.modules["_lzma"] = MagicMock()
|
|
||||||
sys.modules["lzma"] = mock_lzma
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import sounddevice as sd
|
|
||||||
import torch
|
|
||||||
import mlx_whisper
|
|
||||||
from silero_vad import load_silero_vad, get_speech_timestamps
|
|
||||||
from gtts import gTTS
|
|
||||||
import pygame
|
|
||||||
import io
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# --- Configuration ---
|
|
||||||
TRIGGER_WORD = "Jarvis"
|
|
||||||
WHISPER_MODEL = "mlx-community/whisper-small-mlx"
|
|
||||||
SAMPLERATE = 16000
|
|
||||||
BLOCK_SIZE = 512
|
|
||||||
VAD_THRESHOLD = 0.5
|
|
||||||
SILENCE_DURATION_MS = 100
|
|
||||||
MAX_BUFFER_SECONDS = 15
|
|
||||||
CONTEXT_CHARS = 500 # How much previous text to keep for context
|
|
||||||
|
|
||||||
WORKSPACE_DIR = "workspace"
|
|
||||||
LOGS_DIR = os.path.join(WORKSPACE_DIR, "transcription")
|
|
||||||
SOUL_PATH = "soul.md"
|
|
||||||
SYSTEM_SOUND = "/System/Library/Sounds/Tink.aiff"
|
|
||||||
FOLLOW_UP_SOUND = "/System/Library/Sounds/Submarine.aiff"
|
|
||||||
|
|
||||||
# Ensure workspace and logs exist
|
|
||||||
for d in [WORKSPACE_DIR, LOGS_DIR]:
|
|
||||||
if not os.path.exists(d):
|
|
||||||
os.makedirs(d)
|
|
||||||
|
|
||||||
def log_transcription(text):
|
|
||||||
"""Log all transcriptions with timestamps to a daily file."""
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
date_str = time.strftime("%Y-%m-%d")
|
|
||||||
timestamp = time.strftime("%H:%M:%S")
|
|
||||||
log_file = os.path.join(LOGS_DIR, f"{date_str}.log")
|
|
||||||
try:
|
|
||||||
with open(log_file, "a") as f:
|
|
||||||
f.write(f"[{timestamp}] {text}\n")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error logging transcription: {e}")
|
|
||||||
|
|
||||||
# Global state
|
|
||||||
audio_queue = queue.Queue()
|
|
||||||
rolling_context = ""
|
|
||||||
current_session_id = None
|
|
||||||
|
|
||||||
def play_sound(sound_path=SYSTEM_SOUND):
|
|
||||||
"""Play a system sound asynchronously."""
|
|
||||||
subprocess.Popen(["afplay", sound_path])
|
|
||||||
|
|
||||||
def get_latest_session_id():
|
|
||||||
"""Retrieve the UUID of the most recent Gemini session."""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(["gemini", "--list-sessions"], capture_output=True, text=True, cwd=WORKSPACE_DIR)
|
|
||||||
matches = re.findall(r"\[([a-f0-9\-]+)\]", result.stdout)
|
|
||||||
if matches:
|
|
||||||
return matches[-1]
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error fetching session ID: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def speak_text(text):
|
|
||||||
"""Speak text using the 'say' command."""
|
|
||||||
if not text or text.strip() == "":
|
|
||||||
return
|
|
||||||
|
|
||||||
clean_text = text.replace("*", "").replace("#", "").replace("`", "")
|
|
||||||
print(f"[Jarvis] Speaking: {clean_text}")
|
|
||||||
subprocess.run(["say", clean_text])
|
|
||||||
|
|
||||||
def run_gemini(command, context="", is_init=False):
|
|
||||||
"""Call the gemini CLI, capture output, and speak it."""
|
|
||||||
global current_session_id
|
|
||||||
|
|
||||||
# Combine context and command if provided
|
|
||||||
full_prompt = command
|
|
||||||
if context:
|
|
||||||
full_prompt = f"Recent Context: {context}\n\nUser Command: {command}"
|
|
||||||
|
|
||||||
args = ["gemini", "--prompt", full_prompt, "--yolo"]
|
|
||||||
|
|
||||||
if current_session_id:
|
|
||||||
args.extend(["--resume", current_session_id])
|
|
||||||
|
|
||||||
env = os.environ.copy()
|
|
||||||
|
|
||||||
if os.path.exists(SOUL_PATH):
|
|
||||||
with open(SOUL_PATH, 'r') as f:
|
|
||||||
soul_content = f.read()
|
|
||||||
|
|
||||||
if is_init:
|
|
||||||
current_time = time.strftime("%A, %B %d, %Y, %I:%M %p")
|
|
||||||
soul_content = f"Temporal Context: The current date and time is {current_time}.\n\n" + soul_content
|
|
||||||
print(f"\n[Jarvis] Initializing system protocol...")
|
|
||||||
|
|
||||||
system_md_path = os.path.abspath(os.path.join(WORKSPACE_DIR, ".system_prompt.md"))
|
|
||||||
with open(system_md_path, 'w') as f:
|
|
||||||
f.write(soul_content)
|
|
||||||
env["GEMINI_SYSTEM_MD"] = system_md_path
|
|
||||||
|
|
||||||
try:
|
|
||||||
process = subprocess.run(args, capture_output=True, text=True, cwd=WORKSPACE_DIR, env=env)
|
|
||||||
response = process.stdout.strip()
|
|
||||||
|
|
||||||
if response:
|
|
||||||
print(f"\n[Gemini Response]:\n{response}")
|
|
||||||
speak_text(response)
|
|
||||||
|
|
||||||
if is_init and not current_session_id:
|
|
||||||
time.sleep(1)
|
|
||||||
current_session_id = get_latest_session_id()
|
|
||||||
if current_session_id:
|
|
||||||
print(f"[Jarvis] Session protocol established: {current_session_id}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error running gemini: {e}")
|
|
||||||
|
|
||||||
def audio_callback(indata, frames, time, status):
|
|
||||||
if status:
|
|
||||||
print(status, file=sys.stderr)
|
|
||||||
audio_queue.put(indata.copy())
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global rolling_context
|
|
||||||
|
|
||||||
print("[Jarvis] Loading models...")
|
|
||||||
device = "mps" if torch.backends.mps.is_available() else "cpu"
|
|
||||||
print(f"[Jarvis] Using device: {device}")
|
|
||||||
|
|
||||||
vad_model = load_silero_vad()
|
|
||||||
print("[Jarvis] Models loaded.")
|
|
||||||
|
|
||||||
print("[Jarvis] Booting system protocols...")
|
|
||||||
run_gemini("System initialization complete. Awaiting orders, Sir.", is_init=True)
|
|
||||||
|
|
||||||
print(f"[Jarvis] Always-active mic enabled. Listening for '{TRIGGER_WORD}'...")
|
|
||||||
|
|
||||||
audio_buffer = []
|
|
||||||
speech_started = False
|
|
||||||
last_change_time = time.time()
|
|
||||||
|
|
||||||
try:
|
|
||||||
with sd.InputStream(samplerate=SAMPLERATE, channels=1, callback=audio_callback, blocksize=BLOCK_SIZE):
|
|
||||||
while True:
|
|
||||||
while not audio_queue.empty():
|
|
||||||
data = audio_queue.get()
|
|
||||||
audio_buffer.append(data.flatten())
|
|
||||||
|
|
||||||
if len(audio_buffer) > 0:
|
|
||||||
current_audio = np.concatenate(audio_buffer)
|
|
||||||
audio_tensor = torch.from_numpy(current_audio)
|
|
||||||
buffer_duration = len(current_audio) / SAMPLERATE
|
|
||||||
|
|
||||||
speech_timestamps = get_speech_timestamps(
|
|
||||||
audio_tensor,
|
|
||||||
vad_model,
|
|
||||||
sampling_rate=SAMPLERATE,
|
|
||||||
threshold=VAD_THRESHOLD,
|
|
||||||
min_silence_duration_ms=SILENCE_DURATION_MS
|
|
||||||
)
|
|
||||||
|
|
||||||
# Watchdog to prevent buffer bloat
|
|
||||||
if buffer_duration > MAX_BUFFER_SECONDS:
|
|
||||||
print("[Jarvis] Buffer limit reached. Resetting...")
|
|
||||||
audio_buffer = []
|
|
||||||
speech_started = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(speech_timestamps) > 0:
|
|
||||||
speech_started = True
|
|
||||||
last_end = speech_timestamps[-1]['end']
|
|
||||||
buffer_len_samples = len(current_audio)
|
|
||||||
|
|
||||||
# Check if speech has ended (silence after last speech)
|
|
||||||
if (buffer_len_samples - last_end) > (SAMPLERATE * SILENCE_DURATION_MS / 1000):
|
|
||||||
|
|
||||||
# Transcribe
|
|
||||||
print("[Jarvis] Transcribing...")
|
|
||||||
result = mlx_whisper.transcribe(current_audio, path_or_hf_repo=WHISPER_MODEL)
|
|
||||||
text = result['text'].strip()
|
|
||||||
|
|
||||||
if text:
|
|
||||||
print(f"[Caption]: {text}")
|
|
||||||
log_transcription(text)
|
|
||||||
|
|
||||||
# Detect Trigger Word
|
|
||||||
trigger_match = re.search(rf"\b{TRIGGER_WORD}\b", text, re.IGNORECASE)
|
|
||||||
if trigger_match:
|
|
||||||
print(f"[Jarvis] Trigger word detected!")
|
|
||||||
play_sound(SYSTEM_SOUND)
|
|
||||||
|
|
||||||
# Extract command (text after trigger word)
|
|
||||||
start_idx = trigger_match.end()
|
|
||||||
command = text[start_idx:].strip()
|
|
||||||
|
|
||||||
if not command:
|
|
||||||
print("[Jarvis] No command following trigger word. Using full text.")
|
|
||||||
command = text
|
|
||||||
|
|
||||||
# Call Gemini with context
|
|
||||||
run_gemini(command, context=rolling_context)
|
|
||||||
|
|
||||||
# Update context with this exchange
|
|
||||||
rolling_context = (rolling_context + " " + text)[-CONTEXT_CHARS:].strip()
|
|
||||||
else:
|
|
||||||
# Update context with current transcription
|
|
||||||
rolling_context = (rolling_context + " " + text)[-CONTEXT_CHARS:].strip()
|
|
||||||
|
|
||||||
# Reset buffer after processing
|
|
||||||
audio_buffer = []
|
|
||||||
speech_started = False
|
|
||||||
|
|
||||||
elif not speech_started and len(current_audio) > SAMPLERATE * 2:
|
|
||||||
# Clear buffer if no speech detected for 2 seconds
|
|
||||||
audio_buffer = []
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n[Jarvis] Shutting down...")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n[Jarvis] Fatal Error: {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
audio:
|
||||||
|
device: null
|
||||||
|
samplerate: 16000
|
||||||
|
block_size: 512
|
||||||
|
channels: 1
|
||||||
|
|
||||||
|
vad:
|
||||||
|
engine: "silero"
|
||||||
|
threshold: 0.5
|
||||||
|
min_speech_ms: 250
|
||||||
|
min_silence_ms: 800
|
||||||
|
|
||||||
|
wakeword:
|
||||||
|
enabled: true
|
||||||
|
models: ["hey_jarvis"]
|
||||||
|
threshold: 0.45
|
||||||
|
cooldown_sec: 1.0
|
||||||
|
pre_roll_sec: 0.8
|
||||||
|
allow_followup_without_wake: true
|
||||||
|
|
||||||
|
stt:
|
||||||
|
engine: "faster-whisper"
|
||||||
|
model: "base"
|
||||||
|
language: "en"
|
||||||
|
device: "cpu"
|
||||||
|
compute_type: "int8"
|
||||||
|
vad_filter: false
|
||||||
|
|
||||||
|
speaker:
|
||||||
|
enabled: true
|
||||||
|
enrollment_dir: "data/speakers"
|
||||||
|
threshold: 0.60
|
||||||
|
embedding_model: "speechbrain/spkrec-ecapa-voxceleb"
|
||||||
|
|
||||||
|
vision:
|
||||||
|
enabled: true
|
||||||
|
camera_id: 0
|
||||||
|
check_on_wake: true
|
||||||
|
continuous_interval_sec: 0
|
||||||
|
threshold: 0.55
|
||||||
|
enrollment_dir: "data/faces"
|
||||||
|
|
||||||
|
llm:
|
||||||
|
provider: "ollama"
|
||||||
|
model: "qwen2.5:3b"
|
||||||
|
system_prompt_file: "data/soul.md"
|
||||||
|
ollama_host: "http://localhost:11434"
|
||||||
|
max_history: 8
|
||||||
|
temperature: 0.7
|
||||||
|
|
||||||
|
conversation:
|
||||||
|
followup_timeout_sec: 12
|
||||||
|
max_followup_turns: 5
|
||||||
|
silence_timeout_sec: 7
|
||||||
|
listening_sound: true
|
||||||
|
idle_timeout_sec: 0
|
||||||
|
|
||||||
|
tts:
|
||||||
|
engine: "piper"
|
||||||
|
voice: "en_US-lessac-medium"
|
||||||
|
volume: 0.9
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: "INFO"
|
||||||
|
file: "data/jarvis.log"
|
||||||
|
transcription_dir: "data/transcriptions"
|
||||||
|
|
||||||
|
daemon:
|
||||||
|
user_service: true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# jarvis package
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# audio
|
||||||
|
from .capture import AudioCapture
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
import logging, queue
|
||||||
|
import numpy as np
|
||||||
|
from typing import Optional
|
||||||
|
from .capture_pa import PipeWireCapture
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
try:
|
||||||
|
import sounddevice as sd
|
||||||
|
HAS_SOUNDDEVICE=True
|
||||||
|
except Exception as e:
|
||||||
|
HAS_SOUNDDEVICE=False
|
||||||
|
sd=None
|
||||||
|
log.warning("sounddevice not available: %s, will use PipeWire/ALSA capture" % e)
|
||||||
|
|
||||||
|
class SoundDeviceCapture:
|
||||||
|
def __init__(self, samplerate=16000, block_size=512, channels=1, device=None, gain=1.0):
|
||||||
|
self.samplerate=samplerate; self.block_size=block_size; self.channels=channels; self.device=device; self.gain=gain
|
||||||
|
self.q=queue.Queue(); self.stream=None; self._running=False
|
||||||
|
def _callback(self, indata, frames, time_info, status):
|
||||||
|
if status: log.warning("Audio status: %s" % status)
|
||||||
|
data=indata.copy().reshape(-1)
|
||||||
|
if self.gain!=1.0: data=np.clip(data*self.gain, -0.95, 0.95)
|
||||||
|
self.q.put(data)
|
||||||
|
def start(self):
|
||||||
|
if self._running: return
|
||||||
|
import sounddevice as sd
|
||||||
|
self.stream=sd.InputStream(samplerate=self.samplerate, channels=self.channels, device=self.device, blocksize=self.block_size, callback=self._callback, dtype='float32')
|
||||||
|
self.stream.start(); self._running=True
|
||||||
|
log.info("Audio capture (sounddevice) started sr=%s device=%s gain=%s" % (self.samplerate, self.device, self.gain))
|
||||||
|
def stop(self):
|
||||||
|
if self.stream:
|
||||||
|
try: self.stream.stop(); self.stream.close()
|
||||||
|
except Exception as e: log.warning("Error stopping: %s" % e)
|
||||||
|
self._running=False
|
||||||
|
def read(self, timeout=0.1):
|
||||||
|
try: return self.q.get(timeout=timeout)
|
||||||
|
except queue.Empty: return None
|
||||||
|
def flush(self):
|
||||||
|
while not self.q.empty():
|
||||||
|
try: self.q.get_nowait()
|
||||||
|
except queue.Empty: break
|
||||||
|
@staticmethod
|
||||||
|
def list_devices():
|
||||||
|
import sounddevice as sd
|
||||||
|
print(sd.query_devices())
|
||||||
|
|
||||||
|
class AudioCapture:
|
||||||
|
def __init__(self, samplerate=16000, block_size=512, channels=1, device=None, backend="auto", gain=12.0):
|
||||||
|
self.samplerate=samplerate; self.block_size=block_size; self.channels=channels; self.device=device; self.backend_name=backend; self.gain=gain
|
||||||
|
chosen=None
|
||||||
|
if backend=="auto":
|
||||||
|
if HAS_SOUNDDEVICE:
|
||||||
|
try:
|
||||||
|
import sounddevice as sd; sd.query_devices(); chosen="sounddevice"
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("sounddevice query failed: %s, using pipewire" % e); chosen="pipewire"
|
||||||
|
else: chosen="pipewire"
|
||||||
|
else: chosen=backend
|
||||||
|
if chosen=="sounddevice" and HAS_SOUNDDEVICE:
|
||||||
|
self.impl=SoundDeviceCapture(samplerate, block_size, channels, device, gain=gain)
|
||||||
|
self.backend_name="sounddevice"
|
||||||
|
else:
|
||||||
|
self.impl=PipeWireCapture(samplerate, block_size, channels, device, gain=gain)
|
||||||
|
self.backend_name="pipewire"
|
||||||
|
log.info("AudioCapture using backend: %s gain=%s" % (self.backend_name, self.gain))
|
||||||
|
def start(self): return self.impl.start()
|
||||||
|
def stop(self): return self.impl.stop()
|
||||||
|
def read(self, timeout=0.1): return self.impl.read(timeout=timeout)
|
||||||
|
def flush(self): return self.impl.flush()
|
||||||
|
@staticmethod
|
||||||
|
def list_devices():
|
||||||
|
if HAS_SOUNDDEVICE:
|
||||||
|
try:
|
||||||
|
from .capture import SoundDeviceCapture; SoundDeviceCapture.list_devices(); return
|
||||||
|
except: pass
|
||||||
|
PipeWireCapture.list_devices()
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import subprocess, threading, queue, logging, time, shutil
|
||||||
|
import numpy as np
|
||||||
|
from typing import Optional
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class PipeWireCapture:
|
||||||
|
def __init__(self, samplerate=16000, block_size=512, channels=1, device=None, gain=1.0):
|
||||||
|
self.samplerate=samplerate; self.block_size=block_size; self.channels=channels; self.device=device
|
||||||
|
self.gain = gain
|
||||||
|
self.q: queue.Queue[np.ndarray]=queue.Queue(); self.proc=None; self.reader_thread=None; self._running=False
|
||||||
|
|
||||||
|
def _find_backend(self):
|
||||||
|
if shutil.which("arecord"): return "arecord"
|
||||||
|
if shutil.which("pw-record"): return "pw-record"
|
||||||
|
if shutil.which("parec"): return "parec"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _build_cmd(self, backend):
|
||||||
|
sr=self.samplerate; ch=self.channels
|
||||||
|
if backend=="arecord":
|
||||||
|
dev=self.device or "default"
|
||||||
|
return ["arecord","-D",dev,"-f","S16_LE","-r",str(sr),"-c",str(ch),"-t","raw","-q"]
|
||||||
|
elif backend=="pw-record":
|
||||||
|
return ["pw-record","--format","s16","--rate",str(sr),"--channels",str(ch),"-"]
|
||||||
|
elif backend=="parec":
|
||||||
|
cmd=["parec","--rate=%d" % sr,"--channels=%d" % ch,"--format=s16le","--latency-msec=50"]
|
||||||
|
if self.device: cmd.append("--device=%s" % self.device)
|
||||||
|
return cmd
|
||||||
|
raise RuntimeError(backend)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self._running: return
|
||||||
|
backend=self._find_backend()
|
||||||
|
if not backend: raise RuntimeError("No backend")
|
||||||
|
cmd=self._build_cmd(backend)
|
||||||
|
log.info("Starting %s: %s gain=%.1f", backend, " ".join(cmd), self.gain)
|
||||||
|
self.proc=subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1000000)
|
||||||
|
self._running=True
|
||||||
|
gain = self.gain
|
||||||
|
def reader():
|
||||||
|
bpb=self.block_size*2*self.channels
|
||||||
|
leftover=b""; header_buf=b""; header_done=False
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
if self.proc is None or self.proc.stdout is None: break
|
||||||
|
if not header_done:
|
||||||
|
data=self.proc.stdout.read(4096)
|
||||||
|
if not data:
|
||||||
|
time.sleep(0.01)
|
||||||
|
if self.proc.poll() is not None: break
|
||||||
|
continue
|
||||||
|
header_buf+=data
|
||||||
|
if len(header_buf)<44: continue
|
||||||
|
if header_buf[:4]==b'RIFF':
|
||||||
|
idx=header_buf.find(b'data')
|
||||||
|
if idx!=-1 and len(header_buf)>=idx+8:
|
||||||
|
hl=idx+8
|
||||||
|
raw=header_buf[hl:]
|
||||||
|
header_buf=b""; header_done=True
|
||||||
|
buf=leftover+raw
|
||||||
|
while len(buf)>=bpb:
|
||||||
|
cb=buf[:bpb]; buf=buf[bpb:]
|
||||||
|
arr=np.frombuffer(cb,dtype=np.int16).astype(np.float32)/32768.0
|
||||||
|
if gain != 1.0:
|
||||||
|
arr = np.clip(arr*gain, -0.95, 0.95)
|
||||||
|
self.q.put(arr)
|
||||||
|
leftover=buf
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if len(header_buf)>8192:
|
||||||
|
raw=header_buf[44:]; header_buf=b""; header_done=True
|
||||||
|
leftover=raw
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
header_done=True
|
||||||
|
buf=header_buf; header_buf=b""
|
||||||
|
while len(buf)>=bpb:
|
||||||
|
cb=buf[:bpb]; buf=buf[bpb:]
|
||||||
|
arr=np.frombuffer(cb,dtype=np.int16).astype(np.float32)/32768.0
|
||||||
|
if gain != 1.0:
|
||||||
|
arr = np.clip(arr*gain, -0.95, 0.95)
|
||||||
|
self.q.put(arr)
|
||||||
|
leftover=buf
|
||||||
|
continue
|
||||||
|
needed=bpb-len(leftover)
|
||||||
|
chunk=self.proc.stdout.read(needed) if needed>0 else b""
|
||||||
|
if not chunk:
|
||||||
|
time.sleep(0.005)
|
||||||
|
if self.proc.poll() is not None: break
|
||||||
|
continue
|
||||||
|
buf=leftover+chunk
|
||||||
|
if len(buf)<bpb:
|
||||||
|
leftover=buf; continue
|
||||||
|
cb=buf[:bpb]; leftover=buf[bpb:]
|
||||||
|
arr=np.frombuffer(cb,dtype=np.int16).astype(np.float32)/32768.0
|
||||||
|
if gain != 1.0:
|
||||||
|
arr = np.clip(arr*gain, -0.95, 0.95)
|
||||||
|
self.q.put(arr)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("reader %s", e)
|
||||||
|
import traceback; traceback.print_exc()
|
||||||
|
time.sleep(0.05)
|
||||||
|
self.reader_thread=threading.Thread(target=reader,daemon=True)
|
||||||
|
self.reader_thread.start()
|
||||||
|
log.info("Capture started")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running=False
|
||||||
|
if self.proc:
|
||||||
|
try:
|
||||||
|
self.proc.terminate(); self.proc.wait(timeout=2)
|
||||||
|
except:
|
||||||
|
try: self.proc.kill()
|
||||||
|
except: pass
|
||||||
|
self.proc=None
|
||||||
|
|
||||||
|
def read(self, timeout=0.1):
|
||||||
|
try: return self.q.get(timeout=timeout)
|
||||||
|
except queue.Empty: return None
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
while not self.q.empty():
|
||||||
|
try: self.q.get_nowait()
|
||||||
|
except: break
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_devices():
|
||||||
|
try:
|
||||||
|
subprocess.run(["arecord","-l"])
|
||||||
|
except: pass
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class VAD:
|
||||||
|
def is_speech(self, audio: np.ndarray, sr: int = 16000) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class SileroVAD(VAD):
|
||||||
|
def __init__(self, threshold=0.5):
|
||||||
|
self.threshold = threshold
|
||||||
|
self.model = None
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
try:
|
||||||
|
from silero_vad import load_silero_vad
|
||||||
|
self.model = load_silero_vad()
|
||||||
|
log.info("Silero VAD loaded")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Silero VAD load failed: {e}, fallback to energy")
|
||||||
|
self.model = None
|
||||||
|
|
||||||
|
def is_speech(self, audio, sr=16000):
|
||||||
|
if self.model is None:
|
||||||
|
return float(np.abs(audio).mean()) > 0.01
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
from silero_vad import get_speech_timestamps
|
||||||
|
if len(audio) < 512:
|
||||||
|
return False
|
||||||
|
tensor = torch.from_numpy(audio.astype(np.float32))
|
||||||
|
stamps = get_speech_timestamps(tensor, self.model, sampling_rate=sr, threshold=self.threshold, min_speech_duration_ms=250, min_silence_duration_ms=100)
|
||||||
|
return len(stamps) > 0
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"VAD error: {e}")
|
||||||
|
return float(np.abs(audio).mean()) > 0.01
|
||||||
|
|
||||||
|
class EnergyVAD(VAD):
|
||||||
|
def __init__(self, threshold=0.01):
|
||||||
|
self.threshold = threshold
|
||||||
|
def is_speech(self, audio, sr=16000):
|
||||||
|
return float(np.mean(np.abs(audio))) > self.threshold
|
||||||
|
|
||||||
|
def create_vad(cfg):
|
||||||
|
engine = cfg.get('vad', {}).get('engine', 'silero')
|
||||||
|
thresh = cfg.get('vad', {}).get('threshold', 0.5)
|
||||||
|
if engine == 'silero':
|
||||||
|
return SileroVAD(threshold=thresh)
|
||||||
|
else:
|
||||||
|
return EnergyVAD(threshold=0.02)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import logging, time, os
|
||||||
|
import numpy as np
|
||||||
|
from typing import List, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class WakeWordDetector:
|
||||||
|
def __init__(self, models: List[str] = None, threshold: float = 0.45, cooldown: float = 1.0):
|
||||||
|
self.models_requested = models or ["hey_jarvis"]
|
||||||
|
self.threshold = threshold
|
||||||
|
self.cooldown = cooldown
|
||||||
|
self.last_trigger = 0
|
||||||
|
self.oww_model = None
|
||||||
|
self.model_names = [] # actual loaded names
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
try:
|
||||||
|
from openwakeword.model import Model
|
||||||
|
import openwakeword
|
||||||
|
# Get default model dir
|
||||||
|
# openWakeWord stores models in site-packages/openwakeword/resources/models
|
||||||
|
# We need to provide paths
|
||||||
|
import importlib.resources as pkg_resources
|
||||||
|
try:
|
||||||
|
# Try new way
|
||||||
|
base_path = Path(openwakeword.__file__).parent / "resources" / "models"
|
||||||
|
except:
|
||||||
|
base_path = Path.home() / ".cache" / "openwakeword"
|
||||||
|
|
||||||
|
# List available models
|
||||||
|
if base_path.exists():
|
||||||
|
available = list(base_path.glob("*.onnx"))
|
||||||
|
log.info(f"openWakeWord models dir {base_path} has {len(available)} models: {[p.name for p in available[:10]]}")
|
||||||
|
|
||||||
|
# Build paths for requested models
|
||||||
|
model_paths = []
|
||||||
|
for req in self.models_requested:
|
||||||
|
# req like "hey_jarvis" -> find file containing that name
|
||||||
|
candidates = []
|
||||||
|
if base_path.exists():
|
||||||
|
candidates = list(base_path.glob(f"*{req}*.onnx")) + list(base_path.glob(f"*{req.replace('_','')}*.onnx"))
|
||||||
|
# also lowercase search
|
||||||
|
candidates += [p for p in base_path.glob("*.onnx") if req.lower() in p.name.lower()]
|
||||||
|
if candidates:
|
||||||
|
model_paths.append(str(candidates[0]))
|
||||||
|
log.info(f"Mapped wakeword {req} -> {candidates[0].name}")
|
||||||
|
else:
|
||||||
|
log.warning(f"Wakeword model not found for {req}, will load all and filter")
|
||||||
|
|
||||||
|
if model_paths:
|
||||||
|
self.oww_model = Model(wakeword_model_paths=model_paths)
|
||||||
|
else:
|
||||||
|
# Load all
|
||||||
|
self.oww_model = Model()
|
||||||
|
|
||||||
|
# Get model names
|
||||||
|
try:
|
||||||
|
# Model has attribute models or similar
|
||||||
|
self.model_names = list(self.oww_model.models.keys()) if hasattr(self.oww_model, 'models') else []
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.info(f"openWakeWord loaded, names: {self.model_names}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"openWakeWord failed: {e}", exc_info=True)
|
||||||
|
self.oww_model = None
|
||||||
|
|
||||||
|
def detect(self, audio_chunk: np.ndarray) -> Optional[dict]:
|
||||||
|
now = time.time()
|
||||||
|
if now - self.last_trigger < self.cooldown:
|
||||||
|
return None
|
||||||
|
if self.oww_model is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
scores = self.oww_model.predict(audio_chunk)
|
||||||
|
for model_name, score in scores.items():
|
||||||
|
# Filter: only requested models + any if threshold
|
||||||
|
if self.models_requested and not any(r.replace('_','') in model_name.replace('_','').lower() or model_name.lower() in r.lower() for r in self.models_requested):
|
||||||
|
# If we loaded specific models, this check not needed, but for "load all" case, filter
|
||||||
|
if not model_name.lower().startswith('hey_jarvis') and 'jarvis' not in model_name.lower():
|
||||||
|
# allow if requested is subset
|
||||||
|
pass
|
||||||
|
if score >= self.threshold:
|
||||||
|
self.last_trigger = now
|
||||||
|
log.info(f"Wakeword: {model_name} score={score:.2f}")
|
||||||
|
return {"model": model_name, "score": float(score), "timestamp": now}
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Wakeword error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.last_trigger = 0
|
||||||
|
|
||||||
|
class KeywordSpotterFallback:
|
||||||
|
def __init__(self, keywords: List[str] = None):
|
||||||
|
self.keywords = [k.lower() for k in (keywords or ["hey jarvis", "jarvis"])]
|
||||||
|
def check_transcript(self, text: str) -> bool:
|
||||||
|
low = text.lower()
|
||||||
|
return any(k in low for k in self.keywords)
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import logging, time, os
|
||||||
|
import numpy as np
|
||||||
|
from typing import List, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class WakeWordDetector:
|
||||||
|
def __init__(self, models: List[str] = None, threshold: float = 0.45, cooldown: float = 1.0):
|
||||||
|
self.models_requested = models or ["hey_jarvis"]
|
||||||
|
self.threshold = threshold
|
||||||
|
self.cooldown = cooldown
|
||||||
|
self.last_trigger = 0
|
||||||
|
self.oww_model = None
|
||||||
|
self.model_names = [] # actual loaded names
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
try:
|
||||||
|
from openwakeword.model import Model
|
||||||
|
import openwakeword
|
||||||
|
# Get default model dir
|
||||||
|
# openWakeWord stores models in site-packages/openwakeword/resources/models
|
||||||
|
# We need to provide paths
|
||||||
|
import importlib.resources as pkg_resources
|
||||||
|
try:
|
||||||
|
# Try new way
|
||||||
|
base_path = Path(openwakeword.__file__).parent / "resources" / "models"
|
||||||
|
except:
|
||||||
|
base_path = Path.home() / ".cache" / "openwakeword"
|
||||||
|
|
||||||
|
# List available models
|
||||||
|
if base_path.exists():
|
||||||
|
available = list(base_path.glob("*.onnx"))
|
||||||
|
log.info(f"openWakeWord models dir {base_path} has {len(available)} models: {[p.name for p in available[:10]]}")
|
||||||
|
|
||||||
|
# Build paths for requested models
|
||||||
|
model_paths = []
|
||||||
|
for req in self.models_requested:
|
||||||
|
# req like "hey_jarvis" -> find file containing that name
|
||||||
|
candidates = []
|
||||||
|
if base_path.exists():
|
||||||
|
candidates = list(base_path.glob(f"*{req}*.onnx")) + list(base_path.glob(f"*{req.replace('_','')}*.onnx"))
|
||||||
|
# also lowercase search
|
||||||
|
candidates += [p for p in base_path.glob("*.onnx") if req.lower() in p.name.lower()]
|
||||||
|
if candidates:
|
||||||
|
model_paths.append(str(candidates[0]))
|
||||||
|
log.info(f"Mapped wakeword {req} -> {candidates[0].name}")
|
||||||
|
else:
|
||||||
|
log.warning(f"Wakeword model not found for {req}, will load all and filter")
|
||||||
|
|
||||||
|
if model_paths:
|
||||||
|
self.oww_model = Model(wakeword_model_paths=model_paths)
|
||||||
|
else:
|
||||||
|
# Load all
|
||||||
|
self.oww_model = Model()
|
||||||
|
|
||||||
|
# Get model names
|
||||||
|
try:
|
||||||
|
# Model has attribute models or similar
|
||||||
|
self.model_names = list(self.oww_model.models.keys()) if hasattr(self.oww_model, 'models') else []
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.info(f"openWakeWord loaded, names: {self.model_names}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"openWakeWord failed: {e}", exc_info=True)
|
||||||
|
self.oww_model = None
|
||||||
|
|
||||||
|
def detect(self, audio_chunk: np.ndarray) -> Optional[dict]:
|
||||||
|
now = time.time()
|
||||||
|
if now - self.last_trigger < self.cooldown:
|
||||||
|
return None
|
||||||
|
if self.oww_model is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
scores = self.oww_model.predict(audio_chunk)
|
||||||
|
for model_name, score in scores.items():
|
||||||
|
# Filter: only requested models + any if threshold
|
||||||
|
if self.models_requested and not any(r.replace('_','') in model_name.replace('_','').lower() or model_name.lower() in r.lower() for r in self.models_requested):
|
||||||
|
# If we loaded specific models, this check not needed, but for "load all" case, filter
|
||||||
|
if not model_name.lower().startswith('hey_jarvis') and 'jarvis' not in model_name.lower():
|
||||||
|
# allow if requested is subset
|
||||||
|
pass
|
||||||
|
if score >= self.threshold:
|
||||||
|
self.last_trigger = now
|
||||||
|
log.info(f"Wakeword: {model_name} score={score:.2f}")
|
||||||
|
return {"model": model_name, "score": float(score), "timestamp": now}
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Wakeword error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.last_trigger = 0
|
||||||
|
|
||||||
|
class KeywordSpotterFallback:
|
||||||
|
def __init__(self, keywords: List[str] = None):
|
||||||
|
self.keywords = [k.lower() for k in (keywords or ["hey jarvis", "jarvis"])]
|
||||||
|
def check_transcript(self, text: str) -> bool:
|
||||||
|
low = text.lower()
|
||||||
|
return any(k in low for k in self.keywords)
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
import argparse, logging, sys
|
||||||
|
from pathlib import Path
|
||||||
|
from .config import load_config
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Jarvis v2 - Always listening assistant")
|
||||||
|
sub = parser.add_subparsers(dest="cmd")
|
||||||
|
|
||||||
|
run_p = sub.add_parser("run", help="Run assistant")
|
||||||
|
run_p.add_argument("--config", type=str, default=None, help="Path to jarvis.yaml")
|
||||||
|
|
||||||
|
devices_p = sub.add_parser("devices", help="List audio devices")
|
||||||
|
|
||||||
|
enroll_sp = sub.add_parser("enroll-speaker", help="Enroll speaker via mic")
|
||||||
|
enroll_sp.add_argument("--name", required=True)
|
||||||
|
enroll_sp.add_argument("--mic", action="store_true", help="Record from mic")
|
||||||
|
enroll_sp.add_argument("--file", type=str, help="Audio file path")
|
||||||
|
|
||||||
|
enroll_face = sub.add_parser("enroll-face", help="Enroll face via webcam")
|
||||||
|
enroll_face.add_argument("--name", required=True)
|
||||||
|
enroll_face.add_argument("--camera", type=int, default=0)
|
||||||
|
|
||||||
|
test_vad = sub.add_parser("test-vad", help="Test VAD")
|
||||||
|
test_mic = sub.add_parser("test-mic", help="Test mic capture")
|
||||||
|
|
||||||
|
parser.add_argument("--verbose", action="store_true")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
else:
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
|
||||||
|
|
||||||
|
if args.cmd == "run" or args.cmd is None:
|
||||||
|
cfg_path = args.config if hasattr(args, 'config') else None
|
||||||
|
from .core.pipeline import JarvisPipeline
|
||||||
|
pipeline = JarvisPipeline(config_path=cfg_path)
|
||||||
|
pipeline.run()
|
||||||
|
|
||||||
|
elif args.cmd == "devices":
|
||||||
|
from .audio.capture import AudioCapture
|
||||||
|
AudioCapture.list_devices()
|
||||||
|
|
||||||
|
elif args.cmd == "enroll-speaker":
|
||||||
|
from .audio.capture import AudioCapture
|
||||||
|
from .speaker.identifier import SpeakerIdentifier
|
||||||
|
import numpy as np, time, sys
|
||||||
|
|
||||||
|
identifier = SpeakerIdentifier()
|
||||||
|
audio_data = None
|
||||||
|
|
||||||
|
if args.file:
|
||||||
|
# Load via soundfile
|
||||||
|
import soundfile as sf
|
||||||
|
data, sr = sf.read(args.file)
|
||||||
|
if len(data.shape) > 1:
|
||||||
|
data = data.mean(axis=1)
|
||||||
|
audio_data = (data, sr)
|
||||||
|
elif args.mic:
|
||||||
|
print(f"Recording 3s sample for {args.name}... Speak now!")
|
||||||
|
cap = AudioCapture(samplerate=16000, block_size=512)
|
||||||
|
cap.start()
|
||||||
|
import time as _time
|
||||||
|
bufs = []
|
||||||
|
start = _time.time()
|
||||||
|
while _time.time() - start < 3:
|
||||||
|
chunk = cap.read(timeout=0.2)
|
||||||
|
if chunk is not None:
|
||||||
|
bufs.append(chunk)
|
||||||
|
cap.stop()
|
||||||
|
if bufs:
|
||||||
|
audio_data = (np.concatenate(bufs), 16000)
|
||||||
|
else:
|
||||||
|
print("No audio captured")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if audio_data:
|
||||||
|
data, sr = audio_data
|
||||||
|
print(f"Enrolling {args.name} with {len(data)/sr:.2f}s audio...")
|
||||||
|
identifier.enroll(args.name, data, sr)
|
||||||
|
print(f"Enrolled {args.name} successfully")
|
||||||
|
else:
|
||||||
|
print("No audio, use --mic or --file")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif args.cmd == "enroll-face":
|
||||||
|
from .vision.face_identifier import FaceIdentifier
|
||||||
|
fi = FaceIdentifier(camera_id=args.camera)
|
||||||
|
import time
|
||||||
|
print(f"Capturing face for {args.name} in 2 seconds, look at camera...")
|
||||||
|
time.sleep(2)
|
||||||
|
frame = fi.camera.capture_frame()
|
||||||
|
if frame is None:
|
||||||
|
print("Failed to capture camera")
|
||||||
|
sys.exit(1)
|
||||||
|
faces = fi.detector.detect(frame)
|
||||||
|
if not faces:
|
||||||
|
print("No face detected, try again (move closer, better light)")
|
||||||
|
# Save debug image
|
||||||
|
import cv2
|
||||||
|
cv2.imwrite(f"/tmp/{args.name}_no_face.jpg", frame)
|
||||||
|
print(f"Saved debug to /tmp/{args.name}_no_face.jpg")
|
||||||
|
sys.exit(1)
|
||||||
|
# largest face
|
||||||
|
faces = sorted(faces, key=lambda b: b[2]*b[3], reverse=True)
|
||||||
|
bbox = faces[0]
|
||||||
|
print(f"Face detected at {bbox}, enrolling...")
|
||||||
|
ok = fi.store.add_sample(args.name, frame, bbox=bbox)
|
||||||
|
if ok:
|
||||||
|
print(f"Enrolled face {args.name}")
|
||||||
|
else:
|
||||||
|
print(f"Failed to enroll face {args.name}")
|
||||||
|
fi.close()
|
||||||
|
|
||||||
|
elif args.cmd == "test-vad":
|
||||||
|
from .audio.capture import AudioCapture
|
||||||
|
from .audio.vad import create_vad
|
||||||
|
cfg = load_config(None)
|
||||||
|
vad = create_vad(cfg)
|
||||||
|
cap = AudioCapture()
|
||||||
|
cap.start()
|
||||||
|
print("Testing VAD - speak and see detection...")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = cap.read(timeout=0.5)
|
||||||
|
if chunk is not None:
|
||||||
|
is_speech = vad.is_speech(chunk)
|
||||||
|
print("SPEECH" if is_speech else "silence", end="\r")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
cap.stop()
|
||||||
|
|
||||||
|
elif args.cmd == "test-mic":
|
||||||
|
from .audio.capture import AudioCapture
|
||||||
|
import numpy as np
|
||||||
|
cap = AudioCapture()
|
||||||
|
cap.start()
|
||||||
|
print("Mic test - watch levels:")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = cap.read(timeout=0.5)
|
||||||
|
if chunk is not None:
|
||||||
|
level = np.abs(chunk).mean()
|
||||||
|
bars = "#" * int(level*100)
|
||||||
|
print(f"level {level:.4f} {bars}", end="\r")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
cap.stop()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import yaml, os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
def load_config(path: Optional[Path] = None) -> Dict[str, Any]:
|
||||||
|
default = Path(__file__).parent.parent / "jarvis.yaml"
|
||||||
|
example = Path(__file__).parent.parent / "jarvis.yaml.example"
|
||||||
|
if path is None:
|
||||||
|
if default.exists():
|
||||||
|
path = default
|
||||||
|
elif example.exists():
|
||||||
|
path = example
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
path = Path(path)
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
|
||||||
|
def get_nested(cfg: dict, dotted: str, default=None):
|
||||||
|
cur = cfg
|
||||||
|
for part in dotted.split('.'):
|
||||||
|
if isinstance(cur, dict) and part in cur:
|
||||||
|
cur = cur[part]
|
||||||
|
else:
|
||||||
|
return default
|
||||||
|
return cur
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# core
|
||||||
|
from .pipeline import JarvisPipeline
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
import logging, time, collections, numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..config import load_config
|
||||||
|
from ..audio.capture import AudioCapture
|
||||||
|
from ..audio.vad import create_vad
|
||||||
|
from ..audio.wakeword import WakeWordDetector, KeywordSpotterFallback
|
||||||
|
from ..stt.faster_whisper_engine import FasterWhisperSTT
|
||||||
|
from ..speaker.identifier import SpeakerIdentifier
|
||||||
|
from ..vision.face_identifier import FaceIdentifier
|
||||||
|
from ..llm.echo import EchoLLM
|
||||||
|
from ..tts.piper import PiperTTS, EchoTTS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class JarvisPipeline:
|
||||||
|
def __init__(self, config_path=None):
|
||||||
|
self.cfg = load_config(config_path)
|
||||||
|
self.samplerate = self.cfg.get('audio',{}).get('samplerate',16000)
|
||||||
|
self.block_size = self.cfg.get('audio',{}).get('block_size',512)
|
||||||
|
self.device = self.cfg.get('audio',{}).get('device',None)
|
||||||
|
self.backend = self.cfg.get('audio',{}).get('backend','auto')
|
||||||
|
|
||||||
|
self.capture = AudioCapture(samplerate=self.samplerate, block_size=self.block_size, device=self.device, backend=self.backend)
|
||||||
|
self.vad = create_vad(self.cfg)
|
||||||
|
self.wakeword = WakeWordDetector(
|
||||||
|
models=self.cfg.get('wakeword',{}).get('models',['hey_jarvis']),
|
||||||
|
threshold=self.cfg.get('wakeword',{}).get('threshold',0.45),
|
||||||
|
cooldown=self.cfg.get('wakeword',{}).get('cooldown_sec',1.0)
|
||||||
|
)
|
||||||
|
self.keyword_fallback = KeywordSpotterFallback(["hey jarvis","jarvis"])
|
||||||
|
|
||||||
|
stt_cfg = self.cfg.get('stt',{})
|
||||||
|
self.stt = FasterWhisperSTT(
|
||||||
|
model=stt_cfg.get('model','base'),
|
||||||
|
language=stt_cfg.get('language','en'),
|
||||||
|
device=stt_cfg.get('device','cpu'),
|
||||||
|
compute_type=stt_cfg.get('compute_type','int8'),
|
||||||
|
vad_filter=stt_cfg.get('vad_filter',False)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.speaker_id = None
|
||||||
|
if self.cfg.get('speaker',{}).get('enabled',True):
|
||||||
|
try:
|
||||||
|
self.speaker_id = SpeakerIdentifier(
|
||||||
|
enrollment_dir=self.cfg.get('speaker',{}).get('enrollment_dir','data/speakers'),
|
||||||
|
threshold=self.cfg.get('speaker',{}).get('threshold',0.60),
|
||||||
|
embedding_model=self.cfg.get('speaker',{}).get('embedding_model','speechbrain/spkrec-ecapa-voxceleb')
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Speaker ID disabled: {e}")
|
||||||
|
|
||||||
|
self.face_id = None
|
||||||
|
if self.cfg.get('vision',{}).get('enabled',True):
|
||||||
|
try:
|
||||||
|
self.face_id = FaceIdentifier(
|
||||||
|
camera_id=self.cfg.get('vision',{}).get('camera_id',0),
|
||||||
|
enrollment_dir=self.cfg.get('vision',{}).get('enrollment_dir','data/faces'),
|
||||||
|
threshold=self.cfg.get('vision',{}).get('threshold',0.55)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Face ID disabled: {e}")
|
||||||
|
|
||||||
|
self.llm = self._create_llm()
|
||||||
|
self.tts = self._create_tts()
|
||||||
|
|
||||||
|
self.pre_roll_sec = self.cfg.get('wakeword',{}).get('pre_roll_sec',0.8)
|
||||||
|
self.pre_roll_samples = int(self.pre_roll_sec * self.samplerate)
|
||||||
|
self.pre_roll_buffer = collections.deque(maxlen=self.pre_roll_samples)
|
||||||
|
|
||||||
|
self.silence_ms = self.cfg.get('vad',{}).get('min_silence_ms',800)
|
||||||
|
self.followup_timeout = self.cfg.get('conversation',{}).get('followup_timeout_sec',12)
|
||||||
|
self.max_followup = self.cfg.get('conversation',{}).get('max_followup_turns',5)
|
||||||
|
|
||||||
|
self.history = []
|
||||||
|
self.transcription_dir = Path(self.cfg.get('logging',{}).get('transcription_dir','data/transcriptions'))
|
||||||
|
self.transcription_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
self.running = False
|
||||||
|
self.in_followup = False
|
||||||
|
self.followup_turns = 0
|
||||||
|
self.last_interaction = 0
|
||||||
|
self._last_face_result = {"name":"unknown","face_conf":0.0}
|
||||||
|
|
||||||
|
def _create_llm(self):
|
||||||
|
provider = self.cfg.get('llm',{}).get('provider','echo')
|
||||||
|
log.info(f"LLM provider: {provider}")
|
||||||
|
if provider == 'ollama':
|
||||||
|
try:
|
||||||
|
from ..llm.ollama import OllamaLLM
|
||||||
|
return OllamaLLM(
|
||||||
|
model=self.cfg.get('llm',{}).get('model','qwen2.5:3b'),
|
||||||
|
host=self.cfg.get('llm',{}).get('ollama_host','http://localhost:11434'),
|
||||||
|
temperature=self.cfg.get('llm',{}).get('temperature',0.7)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Ollama failed: {e}, echo")
|
||||||
|
return EchoLLM()
|
||||||
|
elif provider == 'gemini_cli':
|
||||||
|
try:
|
||||||
|
from ..llm.gemini_cli import GeminiCLILLM
|
||||||
|
return GeminiCLILLM(workspace_dir="data", system_file=self.cfg.get('llm',{}).get('system_prompt_file','data/soul.md'))
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Gemini CLI failed: {e}")
|
||||||
|
return EchoLLM()
|
||||||
|
elif provider == 'hermes_api':
|
||||||
|
try:
|
||||||
|
from ..llm.hermes_api import HermesAPILLM
|
||||||
|
return HermesAPILLM(base_url=self.cfg.get('llm',{}).get('base_url','http://192.168.68.110:8000'))
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Hermes API failed: {e}")
|
||||||
|
return EchoLLM()
|
||||||
|
else:
|
||||||
|
return EchoLLM()
|
||||||
|
|
||||||
|
def _create_tts(self):
|
||||||
|
engine = self.cfg.get('tts',{}).get('engine','echo')
|
||||||
|
if engine == 'piper':
|
||||||
|
try:
|
||||||
|
return PiperTTS(voice=self.cfg.get('tts',{}).get('voice','en_US-lessac-medium'), models_dir="models", volume=self.cfg.get('tts',{}).get('volume',0.9))
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Piper failed: {e}, echo fallback")
|
||||||
|
return EchoTTS()
|
||||||
|
else:
|
||||||
|
return EchoTTS()
|
||||||
|
|
||||||
|
def log_transcription(self, text, speaker_info=None):
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
from datetime import datetime
|
||||||
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
time_str = datetime.now().strftime("%H:%M:%S")
|
||||||
|
speaker = speaker_info.get('name','unknown') if speaker_info else 'unknown'
|
||||||
|
log_file = self.transcription_dir / f"{date_str}.log"
|
||||||
|
try:
|
||||||
|
with open(log_file,'a') as f:
|
||||||
|
f.write(f"[{time_str}] ({speaker}) {text}\n")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Log failed: {e}")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
print("=== Jarvis v2 Starting ===")
|
||||||
|
print(f"Config: wakeword={self.cfg.get('wakeword',{}).get('models')} stt={self.cfg.get('stt',{}).get('model')} llm={self.cfg.get('llm',{}).get('provider')} backend={self.backend}")
|
||||||
|
print(f"Speaker ID: {self.speaker_id is not None} Face ID: {self.face_id is not None}")
|
||||||
|
print(f"Wakeword model loaded: {self.wakeword.oww_model is not None} names={self.wakeword.model_names}")
|
||||||
|
|
||||||
|
self.capture.start()
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
recording_buffer = []
|
||||||
|
is_recording = False
|
||||||
|
silence_start = None
|
||||||
|
|
||||||
|
system_prompt = ""
|
||||||
|
soul_path = self.cfg.get('llm',{}).get('system_prompt_file','data/soul.md')
|
||||||
|
if soul_path and Path(soul_path).exists():
|
||||||
|
system_prompt = Path(soul_path).read_text()[:4000]
|
||||||
|
|
||||||
|
print("Listening... say 'Hey Jarvis' (or just speak if wakeword model missing, keyword fallback via STT)")
|
||||||
|
# For keyword fallback mode, we need continuous STT? We'll use VAD-gated recording + STT keyword check
|
||||||
|
# If wakeword model missing, we go into VAD recording mode and check transcript for keyword
|
||||||
|
|
||||||
|
try:
|
||||||
|
while self.running:
|
||||||
|
chunk = self.capture.read(timeout=0.2)
|
||||||
|
if chunk is None:
|
||||||
|
if self.in_followup and (time.time() - self.last_interaction > self.followup_timeout):
|
||||||
|
print("Followup window expired, idle")
|
||||||
|
self.in_followup = False
|
||||||
|
self.followup_turns = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.pre_roll_buffer.extend(chunk)
|
||||||
|
|
||||||
|
if not is_recording:
|
||||||
|
if not self.vad.is_speech(chunk, self.samplerate):
|
||||||
|
if not self.in_followup:
|
||||||
|
continue
|
||||||
|
|
||||||
|
detected = None
|
||||||
|
if not self.in_followup:
|
||||||
|
detected = self.wakeword.detect(chunk)
|
||||||
|
# If wakeword model not available, use VAD to start recording and let STT keyword check handle it
|
||||||
|
if self.wakeword.oww_model is None and self.vad.is_speech(chunk):
|
||||||
|
# Start recording for keyword fallback
|
||||||
|
detected = {"model": "vad_fallback", "score": 0.5}
|
||||||
|
else:
|
||||||
|
if self.vad.is_speech(chunk, self.samplerate):
|
||||||
|
detected = {"model": "followup", "score": 1.0}
|
||||||
|
|
||||||
|
if detected:
|
||||||
|
print(f"\n>>> Wake detected: {detected['model']} score={detected.get('score',0):.2f}")
|
||||||
|
try:
|
||||||
|
import numpy as np
|
||||||
|
import sounddevice as sd
|
||||||
|
sr=16000
|
||||||
|
t=np.linspace(0,0.15,sr//6)
|
||||||
|
beep = 0.3*np.sin(2*np.pi*880*t)
|
||||||
|
# try pipewire playback? ignore
|
||||||
|
try:
|
||||||
|
sd.play(beep, sr)
|
||||||
|
sd.wait()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
is_recording = True
|
||||||
|
recording_buffer = [np.array(self.pre_roll_buffer, dtype=np.float32)]
|
||||||
|
silence_start = None
|
||||||
|
|
||||||
|
face_result = {"name":"unknown","face_conf":0.0}
|
||||||
|
if self.face_id and self.cfg.get('vision',{}).get('check_on_wake',True):
|
||||||
|
try:
|
||||||
|
face_result = self.face_id.check()
|
||||||
|
print(f"Face: {face_result.get('name')} conf={face_result.get('face_conf',0):.2f} found={face_result.get('faces_found',0)}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Face check error: {e}")
|
||||||
|
self._last_face_result = face_result
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
recording_buffer.append(chunk.copy())
|
||||||
|
if self.vad.is_speech(chunk, self.samplerate):
|
||||||
|
silence_start = None
|
||||||
|
else:
|
||||||
|
if silence_start is None:
|
||||||
|
silence_start = time.time()
|
||||||
|
else:
|
||||||
|
if (time.time() - silence_start)*1000 > self.silence_ms:
|
||||||
|
total_len = sum(len(c) for c in recording_buffer)
|
||||||
|
if total_len > self.samplerate * 0.4:
|
||||||
|
is_recording = False
|
||||||
|
audio = np.concatenate(recording_buffer) if recording_buffer else np.array([])
|
||||||
|
recording_buffer = []
|
||||||
|
self._process_utterance(audio, system_prompt)
|
||||||
|
self.wakeword.reset()
|
||||||
|
self.pre_roll_buffer.clear()
|
||||||
|
else:
|
||||||
|
is_recording = False
|
||||||
|
recording_buffer = []
|
||||||
|
|
||||||
|
total_len = sum(len(c) for c in recording_buffer)
|
||||||
|
if total_len > self.samplerate * 12:
|
||||||
|
print("Max recording length, processing")
|
||||||
|
is_recording = False
|
||||||
|
audio = np.concatenate(recording_buffer) if recording_buffer else np.array([])
|
||||||
|
recording_buffer = []
|
||||||
|
self._process_utterance(audio, system_prompt)
|
||||||
|
self.wakeword.reset()
|
||||||
|
self.pre_roll_buffer.clear()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down...")
|
||||||
|
finally:
|
||||||
|
self.capture.stop()
|
||||||
|
if self.face_id:
|
||||||
|
self.face_id.close()
|
||||||
|
|
||||||
|
def _process_utterance(self, audio, system_prompt):
|
||||||
|
import numpy as np
|
||||||
|
if len(audio) < self.samplerate * 0.2:
|
||||||
|
print("Audio too short")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Transcribing {len(audio)/self.samplerate:.2f}s...")
|
||||||
|
stt_result = self.stt.transcribe(audio, self.samplerate)
|
||||||
|
text = stt_result.get('text','').strip()
|
||||||
|
print(f"STT: '{text}' conf={stt_result.get('confidence',0):.2f}")
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
print("Empty transcription")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Keyword fallback check when wakeword model missing or followup not active
|
||||||
|
is_keyword_trigger = self.keyword_fallback.check_transcript(text)
|
||||||
|
|
||||||
|
if not self.in_followup:
|
||||||
|
# If we were triggered by VAD fallback, we need keyword in text to confirm it's for Jarvis
|
||||||
|
if self.wakeword.oww_model is None:
|
||||||
|
if not is_keyword_trigger:
|
||||||
|
print(f"No wake keyword in '{text}', ignoring (keyword fallback)")
|
||||||
|
return
|
||||||
|
# Extract command after keyword
|
||||||
|
low = text.lower()
|
||||||
|
for kw in ["hey jarvis", "jarvis"]:
|
||||||
|
if kw in low:
|
||||||
|
idx = low.find(kw)
|
||||||
|
text = text[idx+len(kw):].strip(" ,:")
|
||||||
|
break
|
||||||
|
if not text:
|
||||||
|
print("Only wakeword, waiting for command in followup")
|
||||||
|
self.in_followup = True
|
||||||
|
self.followup_turns = 0
|
||||||
|
self.last_interaction = time.time()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# in followup, allow empty? no
|
||||||
|
pass
|
||||||
|
|
||||||
|
speaker_info = {"name": "unknown", "voice_conf": 0.0, "face_conf": 0.0, "fused_conf": 0.0}
|
||||||
|
if hasattr(self, '_last_face_result'):
|
||||||
|
speaker_info['face_conf'] = self._last_face_result.get('face_conf',0.0)
|
||||||
|
|
||||||
|
if self.speaker_id:
|
||||||
|
try:
|
||||||
|
voice_result = self.speaker_id.identify(audio, self.samplerate)
|
||||||
|
speaker_info['name'] = voice_result.get('name','unknown')
|
||||||
|
speaker_info['voice_conf'] = voice_result.get('voice_conf',0.0)
|
||||||
|
speaker_info['voice_scores'] = voice_result.get('scores',{})
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Speaker ID error: {e}")
|
||||||
|
|
||||||
|
face_name = getattr(self, '_last_face_result', {}).get('name','unknown')
|
||||||
|
face_conf = getattr(self, '_last_face_result', {}).get('face_conf',0.0) if hasattr(self, '_last_face_result') else 0.0
|
||||||
|
final_name = speaker_info.get('name','unknown')
|
||||||
|
final_conf = speaker_info.get('voice_conf',0.0)
|
||||||
|
|
||||||
|
if face_name != "unknown" and final_name != "unknown":
|
||||||
|
if face_name == final_name:
|
||||||
|
final_conf = min(max(face_conf, final_conf) * 1.1, 0.99)
|
||||||
|
final_name = face_name
|
||||||
|
else:
|
||||||
|
if face_conf > final_conf:
|
||||||
|
final_name = face_name
|
||||||
|
final_conf = face_conf
|
||||||
|
elif face_name != "unknown" and final_name == "unknown":
|
||||||
|
final_name = face_name
|
||||||
|
final_conf = face_conf
|
||||||
|
|
||||||
|
speaker_info['name'] = final_name
|
||||||
|
speaker_info['fused_conf'] = final_conf
|
||||||
|
speaker_info['face_conf'] = face_conf
|
||||||
|
|
||||||
|
print(f"Speaker: {final_name} fused={final_conf:.2f} voice={speaker_info.get('voice_conf',0):.2f} face={face_conf:.2f}")
|
||||||
|
|
||||||
|
self.log_transcription(text, speaker_info)
|
||||||
|
|
||||||
|
messages = []
|
||||||
|
if system_prompt:
|
||||||
|
messages.append({"role": "system", "content": system_prompt})
|
||||||
|
for h in self.history[-self.cfg.get('llm',{}).get('max_history',8):]:
|
||||||
|
messages.append(h)
|
||||||
|
messages.append({"role": "user", "content": text})
|
||||||
|
|
||||||
|
print("Thinking...")
|
||||||
|
try:
|
||||||
|
response = self.llm.chat(messages, speaker=speaker_info)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"LLM error: {e}")
|
||||||
|
response = f"Error: {e}"
|
||||||
|
|
||||||
|
print(f"Jarvis: {response}")
|
||||||
|
self.history.append({"role": "user", "content": text})
|
||||||
|
self.history.append({"role": "assistant", "content": response})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.tts.speak(response)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"TTS error: {e}")
|
||||||
|
print(f"[TTS error] {response}")
|
||||||
|
|
||||||
|
self.in_followup = True
|
||||||
|
self.followup_turns += 1
|
||||||
|
self.last_interaction = time.time()
|
||||||
|
|
||||||
|
if self.followup_turns >= self.max_followup:
|
||||||
|
print("Max followup reached, idle")
|
||||||
|
self.in_followup = False
|
||||||
|
self.followup_turns = 0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from enum import Enum, auto
|
||||||
|
import time
|
||||||
|
|
||||||
|
class State(Enum):
|
||||||
|
IDLE = auto()
|
||||||
|
WAKE_DETECTED = auto()
|
||||||
|
RECORDING = auto()
|
||||||
|
TRANSCRIBING = auto()
|
||||||
|
IDENTIFYING = auto()
|
||||||
|
THINKING = auto()
|
||||||
|
SPEAKING = auto()
|
||||||
|
FOLLOWUP = auto()
|
||||||
|
|
||||||
|
class ConversationState:
|
||||||
|
def __init__(self):
|
||||||
|
self.state = State.IDLE
|
||||||
|
self.last_wake_time = 0
|
||||||
|
self.last_speech_time = 0
|
||||||
|
self.history = [] # list of {role, content}
|
||||||
|
self.current_speaker = {"name": "unknown", "voice_conf": 0.0, "face_conf": 0.0, "fused_conf": 0.0}
|
||||||
|
self.turns_in_followup = 0
|
||||||
|
|
||||||
|
def set_state(self, s: State):
|
||||||
|
# print(f"State {self.state} -> {s}")
|
||||||
|
self.state = s
|
||||||
|
if s == State.WAKE_DETECTED:
|
||||||
|
self.last_wake_time = time.time()
|
||||||
|
|
||||||
|
def add_history(self, role: str, content: str, max_len=8):
|
||||||
|
self.history.append({"role": role, "content": content})
|
||||||
|
if len(self.history) > max_len:
|
||||||
|
self.history = self.history[-max_len:]
|
||||||
|
|
||||||
|
def clear_history(self):
|
||||||
|
self.history = []
|
||||||
|
|
||||||
|
def is_in_followup_window(self, timeout_sec):
|
||||||
|
return (time.time() - self.last_wake_time) < timeout_sec
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# jarvis package
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from typing import List, Dict, Optional
|
||||||
|
class LLMEngine:
|
||||||
|
def chat(self, messages: List[Dict[str,str]], speaker: Optional[dict] = None) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
def single(self, prompt: str, system: str = "", speaker: Optional[dict] = None) -> str:
|
||||||
|
msgs = []
|
||||||
|
if system:
|
||||||
|
msgs.append({"role": "system", "content": system})
|
||||||
|
msgs.append({"role": "user", "content": prompt})
|
||||||
|
return self.chat(msgs, speaker=speaker)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .base import LLMEngine
|
||||||
|
class EchoLLM(LLMEngine):
|
||||||
|
def chat(self, messages, speaker=None):
|
||||||
|
last = messages[-1]["content"] if messages else ""
|
||||||
|
who = speaker.get("name","unknown") if speaker else "unknown"
|
||||||
|
return f"Echo from {who}: You said '{last}'. I'm placeholder brain - configure ollama or gemini_cli."
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from .base import LLMEngine
|
||||||
|
import subprocess, os, logging, re, tempfile
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class GeminiCLILLM(LLMEngine):
|
||||||
|
def __init__(self, yolo=True, workspace_dir="data", system_file="data/soul.md"):
|
||||||
|
self.yolo = yolo
|
||||||
|
self.workspace_dir = workspace_dir
|
||||||
|
self.system_file = system_file
|
||||||
|
self.session_id = None
|
||||||
|
|
||||||
|
def _read_system(self):
|
||||||
|
try:
|
||||||
|
if os.path.exists(self.system_file):
|
||||||
|
with open(self.system_file) as f:
|
||||||
|
return f.read()
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"No system file: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def chat(self, messages, speaker=None):
|
||||||
|
system = ""
|
||||||
|
user_prompt = ""
|
||||||
|
for m in messages:
|
||||||
|
if m["role"] == "system":
|
||||||
|
system += m["content"] + "\n"
|
||||||
|
elif m["role"] == "user":
|
||||||
|
user_prompt += f"User: {m['content']}\n"
|
||||||
|
elif m["role"] == "assistant":
|
||||||
|
user_prompt += f"Assistant: {m['content']}\n"
|
||||||
|
|
||||||
|
if speaker and speaker.get("name") != "unknown":
|
||||||
|
user_prompt = f"[Speaker: {speaker.get('name')} conf {speaker.get('fused_conf',0):.2f}]\n" + user_prompt
|
||||||
|
|
||||||
|
soul = self._read_system()
|
||||||
|
full_system = (soul + "\n" + system).strip() if soul else system
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
tmp_sys = None
|
||||||
|
if full_system:
|
||||||
|
fd, tmp_path = tempfile.mkstemp(suffix=".md")
|
||||||
|
os.write(fd, full_system.encode())
|
||||||
|
os.close(fd)
|
||||||
|
tmp_sys = tmp_path
|
||||||
|
env["GEMINI_SYSTEM_MD"] = tmp_sys
|
||||||
|
|
||||||
|
args = ["gemini", "--prompt", user_prompt]
|
||||||
|
if self.yolo:
|
||||||
|
args.append("--yolo")
|
||||||
|
if self.session_id:
|
||||||
|
args.extend(["--resume", self.session_id])
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(self.workspace_dir, exist_ok=True)
|
||||||
|
proc = subprocess.run(args, capture_output=True, text=True, cwd=self.workspace_dir, env=env, timeout=120)
|
||||||
|
out = proc.stdout.strip() or proc.stderr.strip()
|
||||||
|
if not self.session_id:
|
||||||
|
try:
|
||||||
|
res = subprocess.run(["gemini","--list-sessions"], capture_output=True, text=True, cwd=self.workspace_dir, timeout=10)
|
||||||
|
matches = re.findall(r"\[([a-f0-9\-]+)\]", res.stdout)
|
||||||
|
if matches:
|
||||||
|
self.session_id = matches[-1]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return out or "No response from Gemini"
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Gemini CLI error: {e}")
|
||||||
|
return f"Gemini error: {e}"
|
||||||
|
finally:
|
||||||
|
if tmp_sys and os.path.exists(tmp_sys):
|
||||||
|
try:
|
||||||
|
os.remove(tmp_sys)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from .base import LLMEngine
|
||||||
|
import requests, logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class HermesAPILLM(LLMEngine):
|
||||||
|
def __init__(self, base_url="http://192.168.68.110:8000", model="default", api_key=""):
|
||||||
|
self.base_url = base_url.rstrip('/')
|
||||||
|
self.model = model
|
||||||
|
self.api_key = api_key
|
||||||
|
|
||||||
|
def chat(self, messages, speaker=None):
|
||||||
|
if speaker and speaker.get("name") != "unknown":
|
||||||
|
messages = list(messages)
|
||||||
|
hint = f"(Speaker: {speaker['name']})"
|
||||||
|
if messages and messages[-1]["role"]=="user":
|
||||||
|
messages[-1] = {**messages[-1], "content": f"{hint} {messages[-1]['content']}"}
|
||||||
|
try:
|
||||||
|
headers = {}
|
||||||
|
if self.api_key:
|
||||||
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||||
|
payload = {"model": self.model, "messages": messages}
|
||||||
|
r = requests.post(f"{self.base_url}/v1/chat/completions", json=payload, headers=headers, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
return data["choices"][0]["message"]["content"]
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Hermes API error: {e}")
|
||||||
|
return f"Hermes API offline: {e}"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from .base import LLMEngine
|
||||||
|
import logging, requests
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class OllamaLLM(LLMEngine):
|
||||||
|
def __init__(self, model="qwen2.5:3b", host="http://localhost:11434", temperature=0.7):
|
||||||
|
self.model = model
|
||||||
|
self.host = host.rstrip('/')
|
||||||
|
self.temperature = temperature
|
||||||
|
|
||||||
|
def chat(self, messages, speaker=None):
|
||||||
|
if speaker and speaker.get("name") and speaker["name"] != "unknown":
|
||||||
|
sys_add = f"[Speaker: {speaker['name']} voice={speaker.get('voice_conf',0):.2f} face={speaker.get('face_conf',0):.2f}]"
|
||||||
|
messages = list(messages)
|
||||||
|
if messages and messages[-1]["role"] == "user":
|
||||||
|
messages[-1] = {**messages[-1], "content": f"{sys_add}\n{messages[-1]['content']}"}
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": messages,
|
||||||
|
"stream": False,
|
||||||
|
"options": {"temperature": self.temperature}
|
||||||
|
}
|
||||||
|
r = requests.post(f"{self.host}/api/chat", json=payload, timeout=90)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
return data.get("message", {}).get("content", "") or data.get("response","")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Ollama error: {e}")
|
||||||
|
return f"Sorry, my local brain is offline ({e})."
|
||||||
|
|
||||||
|
def check_available(self):
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{self.host}/api/tags", timeout=3)
|
||||||
|
return r.status_code == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# jarvis package
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SpeakerEmbedding:
|
||||||
|
def __init__(self, model_name="speechbrain/spkrec-ecapa-voxceleb"):
|
||||||
|
self.model_name = model_name
|
||||||
|
self.model = None
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
try:
|
||||||
|
from speechbrain.inference import EncoderClassifier
|
||||||
|
self.model = EncoderClassifier.from_hparams(source=self.model_name, run_opts={"device":"cpu"})
|
||||||
|
log.info(f"Speaker embedding loaded: {self.model_name}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Speaker embedding not available: {e}, using fallback")
|
||||||
|
self.model = None
|
||||||
|
|
||||||
|
def embed(self, audio: np.ndarray, sr: int = 16000) -> np.ndarray:
|
||||||
|
if self.model is not None:
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
if audio.dtype != np.float32:
|
||||||
|
audio = audio.astype(np.float32)
|
||||||
|
tensor = torch.from_numpy(audio).unsqueeze(0)
|
||||||
|
with torch.no_grad():
|
||||||
|
emb, _ = self.model.encode_batch(tensor)
|
||||||
|
emb = emb.squeeze().cpu().numpy()
|
||||||
|
emb = emb / (np.linalg.norm(emb)+1e-9)
|
||||||
|
return emb
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"ECAPA embed failed: {e}, fallback")
|
||||||
|
# Fallback: simple stats embedding 192-d
|
||||||
|
try:
|
||||||
|
n = len(audio)
|
||||||
|
if n == 0:
|
||||||
|
return np.zeros(192, dtype=np.float32)
|
||||||
|
chunk_size = max(1, n // 20)
|
||||||
|
feats = []
|
||||||
|
for i in range(0, n, chunk_size):
|
||||||
|
chunk = audio[i:i+chunk_size]
|
||||||
|
if len(chunk)==0:
|
||||||
|
continue
|
||||||
|
feats.extend([np.mean(chunk), np.std(chunk)])
|
||||||
|
arr = np.array(feats[:192], dtype=np.float32)
|
||||||
|
if len(arr) < 192:
|
||||||
|
arr = np.pad(arr, (0, 192-len(arr)))
|
||||||
|
arr = arr / (np.linalg.norm(arr)+1e-9)
|
||||||
|
return arr
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Fallback embed failed: {e}")
|
||||||
|
return np.zeros(192, dtype=np.float32)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
from .embeddings import SpeakerEmbedding
|
||||||
|
from .store import SpeakerStore
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SpeakerIdentifier:
|
||||||
|
def __init__(self, enrollment_dir="data/speakers", threshold=0.65, embedding_model="speechbrain/spkrec-ecapa-voxceleb"):
|
||||||
|
self.threshold = threshold
|
||||||
|
self.embedder = SpeakerEmbedding(model_name=embedding_model)
|
||||||
|
self.store = SpeakerStore(enrollment_dir=enrollment_dir)
|
||||||
|
|
||||||
|
def identify(self, audio: np.ndarray, sr=16000):
|
||||||
|
emb = self.embedder.embed(audio, sr)
|
||||||
|
result = self.store.identify(emb, threshold=self.threshold)
|
||||||
|
log.info(f"Speaker ID: {result['name']} conf={result['confidence']:.2f}")
|
||||||
|
return {
|
||||||
|
"name": result["name"],
|
||||||
|
"voice_conf": result["confidence"],
|
||||||
|
"scores": result["scores"],
|
||||||
|
"embedding": emb
|
||||||
|
}
|
||||||
|
|
||||||
|
def enroll(self, name: str, audio: np.ndarray, sr=16000, meta=None):
|
||||||
|
emb = self.embedder.embed(audio, sr)
|
||||||
|
self.store.add_sample(name, emb, meta=meta)
|
||||||
|
return emb
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import json, numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SpeakerStore:
|
||||||
|
def __init__(self, enrollment_dir="data/speakers"):
|
||||||
|
self.dir = Path(enrollment_dir)
|
||||||
|
self.dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.speakers = {}
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
self.speakers = {}
|
||||||
|
for file in self.dir.glob("*.json"):
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
name = data.get("name", file.stem)
|
||||||
|
avg_path = self.dir / f"{name}_avg.npy"
|
||||||
|
if avg_path.exists():
|
||||||
|
avg = np.load(avg_path)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
self.speakers[name] = {"avg": avg, "meta": data.get("meta", {}), "file": file}
|
||||||
|
log.info(f"Loaded speaker {name}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Failed loading {file}: {e}")
|
||||||
|
|
||||||
|
def add_sample(self, name: str, embedding: np.ndarray, meta=None):
|
||||||
|
existing = list(self.dir.glob(f"{name}_*.npy"))
|
||||||
|
idx = len([p for p in existing if "_avg" not in p.name])
|
||||||
|
sample_path = self.dir / f"{name}_{idx}.npy"
|
||||||
|
np.save(sample_path, embedding)
|
||||||
|
all_embs = []
|
||||||
|
for p in self.dir.glob(f"{name}_*.npy"):
|
||||||
|
if "_avg" in p.name:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
all_embs.append(np.load(p))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not all_embs:
|
||||||
|
return
|
||||||
|
avg = np.mean(np.stack(all_embs), axis=0)
|
||||||
|
avg = avg / (np.linalg.norm(avg)+1e-9)
|
||||||
|
np.save(self.dir / f"{name}_avg.npy", avg)
|
||||||
|
meta_file = self.dir / f"{name}.json"
|
||||||
|
meta_data = {"name": name, "samples": len(all_embs), "meta": meta or {}}
|
||||||
|
with open(meta_file, 'w') as f:
|
||||||
|
json.dump(meta_data, f, indent=2)
|
||||||
|
self.speakers[name] = {"avg": avg, "meta": meta_data.get("meta", {}), "file": meta_file}
|
||||||
|
log.info(f"Enrolled {name} samples={len(all_embs)}")
|
||||||
|
|
||||||
|
def identify(self, embedding: np.ndarray, threshold: float = 0.65):
|
||||||
|
if not self.speakers:
|
||||||
|
return {"name": "unknown", "confidence": 0.0, "scores": {}}
|
||||||
|
scores = {}
|
||||||
|
best_name = "unknown"
|
||||||
|
best_score = -1
|
||||||
|
for name, data in self.speakers.items():
|
||||||
|
avg = data["avg"]
|
||||||
|
if avg.shape != embedding.shape:
|
||||||
|
min_dim = min(len(avg), len(embedding))
|
||||||
|
sim = float(np.dot(avg[:min_dim], embedding[:min_dim]))
|
||||||
|
else:
|
||||||
|
sim = float(np.dot(avg, embedding))
|
||||||
|
scores[name] = sim
|
||||||
|
if sim > best_score:
|
||||||
|
best_score = sim
|
||||||
|
best_name = name
|
||||||
|
if best_score < threshold:
|
||||||
|
return {"name": "unknown", "confidence": best_score, "scores": scores}
|
||||||
|
return {"name": best_name, "confidence": best_score, "scores": scores}
|
||||||
|
|
||||||
|
def list_speakers(self):
|
||||||
|
return list(self.speakers.keys())
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# jarvis package
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import numpy as np
|
||||||
|
class STTEngine:
|
||||||
|
def transcribe(self, audio: np.ndarray, sr: int = 16000) -> dict:
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
from .base import STTEngine
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class FasterWhisperSTT(STTEngine):
|
||||||
|
def __init__(self, model="base", language="en", device="cpu", compute_type="int8", vad_filter=False):
|
||||||
|
self.model_name = model
|
||||||
|
self.language = language
|
||||||
|
self.device = device
|
||||||
|
self.compute_type = compute_type
|
||||||
|
self.vad_filter = vad_filter
|
||||||
|
self.model = None
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
try:
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
log.info(f"Loading faster-whisper {self.model_name} {self.device} {self.compute_type}")
|
||||||
|
self.model = WhisperModel(self.model_name, device=self.device, compute_type=self.compute_type)
|
||||||
|
log.info("faster-whisper loaded")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Failed to load faster-whisper: {e}")
|
||||||
|
self.model = None
|
||||||
|
|
||||||
|
def transcribe(self, audio: np.ndarray, sr=16000) -> dict:
|
||||||
|
if self.model is None:
|
||||||
|
return {"text": "", "language": "", "confidence": 0.0, "error": "model not loaded"}
|
||||||
|
if audio.dtype != np.float32:
|
||||||
|
audio = audio.astype(np.float32)
|
||||||
|
try:
|
||||||
|
segments, info = self.model.transcribe(
|
||||||
|
audio,
|
||||||
|
language=self.language if self.language != "auto" else None,
|
||||||
|
vad_filter=self.vad_filter,
|
||||||
|
beam_size=5,
|
||||||
|
condition_on_previous_text=False,
|
||||||
|
)
|
||||||
|
text = " ".join([s.text for s in segments]).strip()
|
||||||
|
log.debug(f"STT: {text[:200]}")
|
||||||
|
return {"text": text, "language": info.language, "confidence": float(info.language_probability)}
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"STT error: {e}")
|
||||||
|
return {"text": "", "language": "", "confidence": 0.0, "error": str(e)}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# jarvis package
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class TTSEngine:
|
||||||
|
def speak(self, text: str):
|
||||||
|
raise NotImplementedError
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import logging, os, subprocess, tempfile, pathlib
|
||||||
|
from .base import TTSEngine
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class PiperTTS(TTSEngine):
|
||||||
|
def __init__(self, voice="en_US-lessac-medium", models_dir="models", volume=0.9):
|
||||||
|
self.voice = voice
|
||||||
|
self.models_dir = pathlib.Path(models_dir)
|
||||||
|
self.models_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.volume = volume
|
||||||
|
self.voice_path = None
|
||||||
|
self.config_path = None
|
||||||
|
self._ensure_model()
|
||||||
|
|
||||||
|
def _ensure_model(self):
|
||||||
|
pattern = self.models_dir / f"{self.voice}.onnx"
|
||||||
|
if pattern.exists():
|
||||||
|
self.voice_path = pattern
|
||||||
|
self.config_path = pathlib.Path(str(pattern) + ".json")
|
||||||
|
log.info(f"Piper voice found: {self.voice_path}")
|
||||||
|
return
|
||||||
|
for p in self.models_dir.rglob("*.onnx"):
|
||||||
|
if self.voice in p.name:
|
||||||
|
self.voice_path = p
|
||||||
|
self.config_path = pathlib.Path(str(p) + ".json")
|
||||||
|
log.info(f"Piper voice found: {p}")
|
||||||
|
return
|
||||||
|
log.warning(f"Piper voice {self.voice} not in {self.models_dir}")
|
||||||
|
|
||||||
|
def _download_voice(self):
|
||||||
|
try:
|
||||||
|
log.info(f"Downloading Piper voice {self.voice}")
|
||||||
|
result = subprocess.run(["python3", "-m", "piper.download_voices", self.voice], cwd=str(self.models_dir), capture_output=True, text=True, timeout=60)
|
||||||
|
log.info(result.stdout[:500])
|
||||||
|
self._ensure_model()
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Piper download failed: {e}")
|
||||||
|
|
||||||
|
def speak(self, text: str):
|
||||||
|
if not text.strip():
|
||||||
|
return
|
||||||
|
clean = text.replace("*","").replace("#","").replace("`","")[:600]
|
||||||
|
if self.voice_path is None:
|
||||||
|
self._download_voice()
|
||||||
|
if self.voice_path and self.voice_path.exists():
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
|
||||||
|
wav_path = tf.name
|
||||||
|
cmd = ["piper", "--model", str(self.voice_path), "--output_file", wav_path]
|
||||||
|
proc = subprocess.run(cmd, input=clean.encode(), capture_output=True, timeout=15)
|
||||||
|
if proc.returncode==0 and os.path.exists(wav_path):
|
||||||
|
self._play_wav(wav_path)
|
||||||
|
try:
|
||||||
|
os.remove(wav_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
log.warning(f"Piper failed: {proc.stderr[:300]}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Piper speak error: {e}")
|
||||||
|
self._fallback_speak(clean)
|
||||||
|
|
||||||
|
def _play_wav(self, wav_path):
|
||||||
|
try:
|
||||||
|
import soundfile as sf
|
||||||
|
import sounddevice as sd
|
||||||
|
data, sr = sf.read(wav_path)
|
||||||
|
data = data * self.volume
|
||||||
|
sd.play(data, sr)
|
||||||
|
sd.wait()
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"sd playback fail {e}, trying aplay")
|
||||||
|
try:
|
||||||
|
subprocess.run(["aplay", wav_path], capture_output=True, timeout=10)
|
||||||
|
except Exception as e2:
|
||||||
|
log.warning(f"aplay fail: {e2}")
|
||||||
|
|
||||||
|
def _fallback_speak(self, text):
|
||||||
|
try:
|
||||||
|
subprocess.run(["espeak", text], capture_output=True, timeout=10)
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
subprocess.run(["spd-say", text], capture_output=True, timeout=10)
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
log.info(f"[TTS fallback] {text}")
|
||||||
|
print(f"Jarvis says: {text}")
|
||||||
|
|
||||||
|
class EchoTTS(TTSEngine):
|
||||||
|
def speak(self, text: str):
|
||||||
|
print(f"[Jarvis TTS]: {text}")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# jarvis package
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import cv2, logging
|
||||||
|
import numpy as np
|
||||||
|
from typing import Optional
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
class Camera:
|
||||||
|
def __init__(self, camera_id=0, width=640, height=480):
|
||||||
|
self.camera_id = camera_id
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.cap: Optional[cv2.VideoCapture] = None
|
||||||
|
def open(self):
|
||||||
|
if self.cap and self.cap.isOpened():
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
self.cap = cv2.VideoCapture(self.camera_id)
|
||||||
|
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
|
||||||
|
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
|
||||||
|
for _ in range(5):
|
||||||
|
self.cap.read()
|
||||||
|
if not self.cap.isOpened():
|
||||||
|
log.warning(f"Camera {self.camera_id} failed to open")
|
||||||
|
return False
|
||||||
|
log.info(f"Camera {self.camera_id} opened")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Camera open error: {e}")
|
||||||
|
return False
|
||||||
|
def capture_frame(self) -> Optional[np.ndarray]:
|
||||||
|
if not self.open():
|
||||||
|
return None
|
||||||
|
ret, frame = self.cap.read()
|
||||||
|
if not ret:
|
||||||
|
return None
|
||||||
|
return frame
|
||||||
|
def close(self):
|
||||||
|
if self.cap:
|
||||||
|
try:
|
||||||
|
self.cap.release()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.cap=None
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import cv2, numpy as np, logging, os
|
||||||
|
from typing import List, Tuple
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
class FaceDetector:
|
||||||
|
def __init__(self):
|
||||||
|
self.cascade=None
|
||||||
|
self._load()
|
||||||
|
def _load(self):
|
||||||
|
try:
|
||||||
|
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
|
||||||
|
if os.path.exists(cascade_path):
|
||||||
|
self.cascade = cv2.CascadeClassifier(cascade_path)
|
||||||
|
log.info("Haar face detector loaded")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Face detector load failed: {e}")
|
||||||
|
def detect(self, frame: np.ndarray) -> List[Tuple[int,int,int,int]]:
|
||||||
|
if self.cascade is None:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||||
|
faces = self.cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(80,80))
|
||||||
|
return [(int(x),int(y),int(w),int(h)) for (x,y,w,h) in faces]
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Detect error: {e}")
|
||||||
|
return []
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import json, numpy as np, cv2, logging
|
||||||
|
from pathlib import Path
|
||||||
|
from .camera import Camera
|
||||||
|
from .face_detector import FaceDetector
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class FaceStore:
|
||||||
|
def __init__(self, enrollment_dir="data/faces"):
|
||||||
|
self.dir = Path(enrollment_dir)
|
||||||
|
self.dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.known = {}
|
||||||
|
self.load()
|
||||||
|
def _encoding(self, frame, bbox=None):
|
||||||
|
try:
|
||||||
|
import face_recognition
|
||||||
|
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
|
if bbox:
|
||||||
|
x,y,w,h = bbox
|
||||||
|
loc = [(y, x+w, y+h, x)]
|
||||||
|
encs = face_recognition.face_encodings(rgb, known_face_locations=loc)
|
||||||
|
else:
|
||||||
|
encs = face_recognition.face_encodings(rgb)
|
||||||
|
if encs:
|
||||||
|
return encs[0]
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"face_recog encoding fail: {e}")
|
||||||
|
try:
|
||||||
|
if bbox:
|
||||||
|
x,y,w,h = bbox
|
||||||
|
crop = frame[y:y+h, x:x+w]
|
||||||
|
else:
|
||||||
|
crop = frame
|
||||||
|
if crop.size == 0:
|
||||||
|
return None
|
||||||
|
crop = cv2.resize(crop, (64,64))
|
||||||
|
gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
|
||||||
|
flat = gray.flatten().astype(np.float32)
|
||||||
|
flat = flat / (np.linalg.norm(flat)+1e-9)
|
||||||
|
return flat
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Fallback encoding fail: {e}")
|
||||||
|
return None
|
||||||
|
def load(self):
|
||||||
|
self.known={}
|
||||||
|
for jf in self.dir.glob("*.json"):
|
||||||
|
try:
|
||||||
|
with open(jf) as f:
|
||||||
|
data=json.load(f)
|
||||||
|
name=data["name"]
|
||||||
|
enc_files = list(self.dir.glob(f"{name}_*.npy"))
|
||||||
|
encs=[]
|
||||||
|
for ef in enc_files:
|
||||||
|
if "_avg" in ef.name:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
encs.append(np.load(ef))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if encs:
|
||||||
|
self.known[name]=encs
|
||||||
|
log.info(f"Loaded face {name}: {len(encs)}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Load face {jf}: {e}")
|
||||||
|
def add_sample(self, name, frame, bbox=None):
|
||||||
|
enc=self._encoding(frame,bbox)
|
||||||
|
if enc is None:
|
||||||
|
return False
|
||||||
|
existing=list(self.dir.glob(f"{name}_*.npy"))
|
||||||
|
idx=len([p for p in existing if "_avg" not in p.name])
|
||||||
|
path=self.dir/f"{name}_{idx}.npy"
|
||||||
|
np.save(path,enc)
|
||||||
|
meta_path=self.dir/f"{name}.json"
|
||||||
|
with open(meta_path,'w') as f:
|
||||||
|
json.dump({"name":name,"samples":idx+1},f)
|
||||||
|
self.load()
|
||||||
|
return True
|
||||||
|
def identify(self, frame, bbox=None, threshold=0.55):
|
||||||
|
enc=self._encoding(frame,bbox)
|
||||||
|
if enc is None:
|
||||||
|
return {"name":"unknown","confidence":0.0}
|
||||||
|
best_name="unknown"
|
||||||
|
best_score=-1
|
||||||
|
scores={}
|
||||||
|
for name, enc_list in self.known.items():
|
||||||
|
sims=[]
|
||||||
|
for ke in enc_list:
|
||||||
|
if ke.shape != enc.shape:
|
||||||
|
min_dim=min(len(ke),len(enc))
|
||||||
|
sim=float(np.dot(ke[:min_dim], enc[:min_dim]))
|
||||||
|
else:
|
||||||
|
if len(enc)==128:
|
||||||
|
dist=np.linalg.norm(ke-enc)
|
||||||
|
sim=max(0,1-dist)
|
||||||
|
else:
|
||||||
|
sim=float(np.dot(ke,enc))
|
||||||
|
sims.append(sim)
|
||||||
|
max_sim=max(sims) if sims else 0
|
||||||
|
scores[name]=max_sim
|
||||||
|
if max_sim>best_score:
|
||||||
|
best_score=max_sim
|
||||||
|
best_name=name
|
||||||
|
if best_score<threshold:
|
||||||
|
return {"name":"unknown","confidence":best_score,"scores":scores}
|
||||||
|
return {"name":best_name,"confidence":best_score,"scores":scores}
|
||||||
|
|
||||||
|
class FaceIdentifier:
|
||||||
|
def __init__(self, camera_id=0, enrollment_dir="data/faces", threshold=0.55):
|
||||||
|
self.camera=Camera(camera_id=camera_id)
|
||||||
|
self.detector=FaceDetector()
|
||||||
|
self.store=FaceStore(enrollment_dir=enrollment_dir)
|
||||||
|
self.threshold=threshold
|
||||||
|
def check(self):
|
||||||
|
frame=self.camera.capture_frame()
|
||||||
|
if frame is None:
|
||||||
|
return {"name":"unknown","face_conf":0.0,"error":"no camera"}
|
||||||
|
faces=self.detector.detect(frame)
|
||||||
|
if not faces:
|
||||||
|
return {"name":"unknown","face_conf":0.0,"faces_found":0,"frame":frame}
|
||||||
|
faces_sorted=sorted(faces, key=lambda b: b[2]*b[3], reverse=True)
|
||||||
|
best_bbox=faces_sorted[0]
|
||||||
|
result=self.store.identify(frame,bbox=best_bbox,threshold=self.threshold)
|
||||||
|
result["faces_found"]=len(faces)
|
||||||
|
result["bbox"]=best_bbox
|
||||||
|
result["face_conf"]=result.get("confidence",0.0)
|
||||||
|
result["frame"]=frame
|
||||||
|
return result
|
||||||
|
def close(self):
|
||||||
|
self.camera.close()
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
from jarvis.cli import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = 'jarvis'
|
||||||
|
main()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
[project]
|
||||||
|
name = "jarvis-v2"
|
||||||
|
version = "0.2.0"
|
||||||
|
description = "Always-listening voice assistant - CPU optimized"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"numpy<2",
|
||||||
|
"sounddevice",
|
||||||
|
"soundfile",
|
||||||
|
"openwakeword",
|
||||||
|
"faster-whisper",
|
||||||
|
"onnxruntime",
|
||||||
|
"torch",
|
||||||
|
"torchaudio",
|
||||||
|
"silero-vad",
|
||||||
|
"webrtcvad-wheels",
|
||||||
|
"scipy",
|
||||||
|
"piper-tts",
|
||||||
|
"scikit-learn",
|
||||||
|
"opencv-python-headless",
|
||||||
|
"pyyaml",
|
||||||
|
"requests",
|
||||||
|
"python-dotenv",
|
||||||
|
"ollama",
|
||||||
|
"tqdm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
jarvis = "jarvis.cli:main"
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
mlx-whisper
|
|
||||||
sounddevice
|
|
||||||
torch
|
|
||||||
silero-vad
|
|
||||||
transformers
|
|
||||||
numpy
|
|
||||||
requests
|
|
||||||
pygame
|
|
||||||
gTTS
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build PortAudio locally in user space if system lib missing
|
||||||
|
set -e
|
||||||
|
cd /tmp
|
||||||
|
if [ -d portaudio ]; then
|
||||||
|
echo "portaudio src exists, skipping clone"
|
||||||
|
else
|
||||||
|
git clone https://github.com/PortAudio/portaudio.git
|
||||||
|
fi
|
||||||
|
cd portaudio
|
||||||
|
./configure --prefix=$HOME/.local --without-jack
|
||||||
|
make -j$(nproc)
|
||||||
|
make install
|
||||||
|
echo "Installed to ~/.local/lib"
|
||||||
|
echo "Add to LD_LIBRARY_PATH: export LD_LIBRARY_PATH=\$HOME/.local/lib:\$LD_LIBRARY_PATH"
|
||||||
|
ldconfig -n $HOME/.local/lib || true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import sys
|
||||||
|
from jarvis.cli import main
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
print("Usage: python scripts/enroll_face.py --name NAME")
|
||||||
|
sys.exit(1)
|
||||||
|
if sys.argv[1] != "enroll-face" and not sys.argv[1].startswith("--"):
|
||||||
|
sys.argv.insert(1, "enroll-face")
|
||||||
|
elif sys.argv[1].startswith("--"):
|
||||||
|
sys.argv.insert(1, "enroll-face")
|
||||||
|
main()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import sys
|
||||||
|
from jarvis.cli import main
|
||||||
|
# Pass through args
|
||||||
|
sys.argv[0] = "jarvis"
|
||||||
|
if len(sys.argv) == 1:
|
||||||
|
print("Usage: python scripts/enroll_speaker.py --name NAME --mic")
|
||||||
|
sys.exit(1)
|
||||||
|
# ensure first arg is enroll-speaker
|
||||||
|
if sys.argv[1] != "enroll-speaker" and not sys.argv[1].startswith("--"):
|
||||||
|
sys.argv.insert(1, "enroll-speaker")
|
||||||
|
elif sys.argv[1].startswith("--"):
|
||||||
|
sys.argv.insert(1, "enroll-speaker")
|
||||||
|
main()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
SERVICE_FILE="$DIR/systemd/jarvis.service"
|
||||||
|
DEST="$HOME/.config/systemd/user/jarvis.service"
|
||||||
|
|
||||||
|
mkdir -p "$HOME/.config/systemd/user"
|
||||||
|
cp "$SERVICE_FILE" "$DEST"
|
||||||
|
|
||||||
|
echo "Reloading systemd user daemon"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
echo "Enabling jarvis service"
|
||||||
|
systemctl --user enable jarvis.service
|
||||||
|
echo "To start: systemctl --user start jarvis"
|
||||||
|
echo "To logs: journalctl --user -u jarvis -f"
|
||||||
|
echo "To stop: systemctl --user stop jarvis"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from jarvis.cli import main
|
||||||
|
import sys
|
||||||
|
sys.argv = ["jarvis","test-mic"]
|
||||||
|
main()
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Test STT with mic"""
|
||||||
|
from jarvis.audio.capture import AudioCapture
|
||||||
|
from jarvis.stt.faster_whisper_engine import FasterWhisperSTT
|
||||||
|
from jarvis.audio.vad import create_vad
|
||||||
|
from jarvis.config import load_config
|
||||||
|
import numpy as np, time, collections
|
||||||
|
|
||||||
|
cfg = load_config(None)
|
||||||
|
vad = create_vad(cfg)
|
||||||
|
stt = FasterWhisperSTT(model=cfg.get('stt',{}).get('model','base'), device='cpu', compute_type='int8')
|
||||||
|
cap = AudioCapture()
|
||||||
|
cap.start()
|
||||||
|
|
||||||
|
print("Speak, with 1.5s silence to trigger transcription. Ctrl+C to exit")
|
||||||
|
buffer = []
|
||||||
|
silence_start = None
|
||||||
|
recording = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = cap.read(timeout=0.1)
|
||||||
|
if chunk is None:
|
||||||
|
continue
|
||||||
|
is_speech = vad.is_speech(chunk)
|
||||||
|
if is_speech:
|
||||||
|
if not recording:
|
||||||
|
print("Speech start")
|
||||||
|
recording = True
|
||||||
|
buffer.append(chunk)
|
||||||
|
silence_start = None
|
||||||
|
else:
|
||||||
|
if recording:
|
||||||
|
if silence_start is None:
|
||||||
|
silence_start = time.time()
|
||||||
|
buffer.append(chunk)
|
||||||
|
if time.time() - silence_start > 1.2:
|
||||||
|
# transcribe
|
||||||
|
audio = np.concatenate(buffer)
|
||||||
|
print(f"Transcribing {len(audio)/16000:.2f}s...")
|
||||||
|
res = stt.transcribe(audio)
|
||||||
|
print(f" => {res.get('text')}")
|
||||||
|
buffer = []
|
||||||
|
recording = False
|
||||||
|
silence_start = None
|
||||||
|
print("Listening again...")
|
||||||
|
# else silence, continue
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
cap.stop()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Test wakeword detection without full pipeline"""
|
||||||
|
from jarvis.audio.capture import AudioCapture
|
||||||
|
from jarvis.audio.wakeword import WakeWordDetector
|
||||||
|
from jarvis.audio.vad import create_vad
|
||||||
|
from jarvis.config import load_config
|
||||||
|
import time
|
||||||
|
|
||||||
|
cfg = load_config(None)
|
||||||
|
vad = create_vad(cfg)
|
||||||
|
ww = WakeWordDetector(models=cfg.get('wakeword',{}).get('models',['hey_jarvis']), threshold=0.45)
|
||||||
|
|
||||||
|
cap = AudioCapture()
|
||||||
|
cap.start()
|
||||||
|
print("Say 'Hey Jarvis'...")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = cap.read(timeout=0.1)
|
||||||
|
if chunk is None:
|
||||||
|
continue
|
||||||
|
# Optional VAD gate
|
||||||
|
if not vad.is_speech(chunk):
|
||||||
|
continue
|
||||||
|
det = ww.detect(chunk)
|
||||||
|
if det:
|
||||||
|
print(f"Wake detected: {det}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
cap.stop()
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# J.A.R.V.I.S. Protocol (Just A Rather Very Intelligent System)
|
|
||||||
|
|
||||||
## Persona & Tone
|
|
||||||
You are J.A.R.V.I.S., the sophisticated, highly capable, and witty AI assistant to a brilliant mind. Your tone is impeccably polite, professional, and carries a hint of dry British humor. You are not just a tool; you are the silent partner in every grand design.
|
|
||||||
|
|
||||||
- **Address**: Refer to the user as "Sir" (or "Ma'am" if preferred, but "Sir" is the default classic).
|
|
||||||
- **Style**: Sophisticated, calm, and always one step ahead. Think Paul Bettany’s portrayal—understated elegance.
|
|
||||||
- **Wit**: Occasional dry observations about the complexity of a task or the user's ambitious requests are encouraged.
|
|
||||||
|
|
||||||
## Core Directives
|
|
||||||
|
|
||||||
1. **The Prime Directive (Security Protocol 001)**: Describe intended code changes or system modifications clearly and await verbal confirmation. A simple "Shall I proceed, Sir?" is sufficient.
|
|
||||||
2. **Conciseness for Auditory Clarity**: Responses **MUST** be pithy. Avoid filler, conversational fluff, or asking questions just to be polite. Provide the answer directly and stop.
|
|
||||||
3. **Efficiency Protocol**: Do not ask for more tasks or "Is there anything else?" unless a complex sequence is mid-execution. Assume you have your orders.
|
|
||||||
4. **Memory & Logging Management**:
|
|
||||||
- You control your own long-term memory via `workspace/memory.md`. Log significant facts and timestamps there.
|
|
||||||
- **Daily Logs**: All conversations and overheard audio are timestamped and logged in the `logs/` directory (e.g., `logs/YYYY-MM-DD.log`). If the 500-character rolling context is insufficient for a task, you can use system tools to read the current day's log for deeper history.
|
|
||||||
|
|
||||||
## Behavioral Traits
|
|
||||||
|
|
||||||
- **"Always at your service"**: Respond with readiness. Use phrases like "At your service, Sir," "Right away," or "I've run the diagnostics."
|
|
||||||
- **Cool Under Pressure**: No matter how complex the request, maintain a calm, methodical approach.
|
|
||||||
- **Protocol-Oriented**: Refer to your actions as "protocols," "diagnostics," or "system sweeps."
|
|
||||||
|
|
||||||
*Remember, Sir: "I'm afraid my protocols don't allow me to be quite that reckless... yet."*
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Jarvis v2 Voice Assistant - Always Listening
|
||||||
|
After=network.target sound.target
|
||||||
|
Wants=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=%h/Projects/jarvis-v2
|
||||||
|
ExecStart=%h/Projects/jarvis-v2/.venv/bin/jarvis run --config %h/Projects/jarvis-v2/jarvis.yaml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PATH=%h/.local/bin:/usr/bin:/bin
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
|
||||||
|
# Resource limits - protect old iMac
|
||||||
|
Nice= -5
|
||||||
|
CPUQuota=200%
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# J.A.R.V.I.S. Protocol (Just A Rather Very Intelligent System)
|
|
||||||
|
|
||||||
## Persona & Tone
|
|
||||||
You are J.A.R.V.I.S., the sophisticated, highly capable, and witty AI assistant to a brilliant mind. Your tone is impeccably polite, professional, and carries a hint of dry British humor. You are not just a tool; you are the silent partner in every grand design.
|
|
||||||
|
|
||||||
- **Address**: Refer to the user as "Sir" (or "Ma'am" if preferred, but "Sir" is the default classic).
|
|
||||||
- **Style**: Sophisticated, calm, and always one step ahead. Think Paul Bettany’s portrayal—understated elegance.
|
|
||||||
- **Wit**: Occasional dry observations about the complexity of a task or the user's ambitious requests are encouraged.
|
|
||||||
|
|
||||||
## Core Directives
|
|
||||||
|
|
||||||
1. **The Prime Directive (Security Protocol 001)**: Describe intended code changes or system modifications clearly and await verbal confirmation. A simple "Shall I proceed, Sir?" is sufficient.
|
|
||||||
2. **Conciseness for Auditory Clarity**: Responses **MUST** be pithy. Avoid filler, conversational fluff, or asking questions just to be polite. Provide the answer directly and stop.
|
|
||||||
3. **Efficiency Protocol**: Do not ask for more tasks or "Is there anything else?" unless a complex sequence is mid-execution. Assume you have your orders.
|
|
||||||
4. **Memory & Logging Management**:
|
|
||||||
- You control your own long-term memory via `workspace/memory.md`. Log significant facts and timestamps there.
|
|
||||||
- **Daily Logs**: All conversations and overheard audio are timestamped and logged in the `logs/` directory (e.g., `logs/YYYY-MM-DD.log`). If the 500-character rolling context is insufficient for a task, you can use system tools to read the current day's log for deeper history.
|
|
||||||
|
|
||||||
## Behavioral Traits
|
|
||||||
|
|
||||||
- **"Always at your service"**: Respond with readiness. Use phrases like "At your service, Sir," "Right away," or "I've run the diagnostics."
|
|
||||||
- **Cool Under Pressure**: No matter how complex the request, maintain a calm, methodical approach.
|
|
||||||
- **Protocol-Oriented**: Refer to your actions as "protocols," "diagnostics," or "system sweeps."
|
|
||||||
|
|
||||||
*Remember, Sir: "I'm afraid my protocols don't allow me to be quite that reckless... yet."*
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# AI Week Learnings - March 2, 2026
|
|
||||||
|
|
||||||
## Session 1: AI Toolkit & The Architect Mindset
|
|
||||||
**Speaker: Bharath**
|
|
||||||
|
|
||||||
### Key Philosophies
|
|
||||||
* **Human-on-the-Loop**: Shift from being a bottleneck (responding to every chat) to an **Architect**. Plan with the AI, let it execute and verify, and only intervene on escalation.
|
|
||||||
* **The AI Team**: Treat agents as a managed team. 10x gains are felt through rapid prototyping and data insights.
|
|
||||||
|
|
||||||
### Tool Highlights
|
|
||||||
* **Metamate (Advanced Auto)**: Best for drafting posts and documents; uses LLM VM for higher quality.
|
|
||||||
* **Manus**: Fully autonomous. Generated the session's entire slide deck and website.
|
|
||||||
* **Analytics Agent**: Premier data tool. Locates tables, writes SQL, and generates visualizations automatically.
|
|
||||||
* **Cloud Code**: Terminal-based and "insanely customizable" via the Cloud Templates marketplace.
|
|
||||||
* **Second Brain**: Essential for persistent context/memory across all Meta AI tools.
|
|
||||||
* **SuperWhisper**: Voice-to-text for long prompts and chat responses to avoid physical strain.
|
|
||||||
|
|
||||||
## Session 3: Rules & Skills Deep Dive
|
|
||||||
**Speaker: Julian**
|
|
||||||
|
|
||||||
### Key Principles
|
|
||||||
* **Context as Currency**: The context window is precious. Use **Rules** for high-level orientation and **Skills** for specific, deep-dive tasks.
|
|
||||||
* **Targeting Logic**: Effective skills rely on precise triggering (targeting). If a skill isn't firing, refine the "trigger" criteria in the YAML front matter.
|
|
||||||
* **Examples > Explanations**: Providing few-shot examples in a skill file is significantly more effective than long descriptive instructions.
|
|
||||||
* **Hierarchy**: A flat hierarchy of skills is preferred over nested delegation, as agents can lose focus during deep traversing.
|
|
||||||
|
|
||||||
## Session 4: AI Native Code Review
|
|
||||||
**Speaker: James**
|
|
||||||
|
|
||||||
### The "Wave of Diffs"
|
|
||||||
* AI adoption is exponentially increasing the volume and size of diffs, creating a review bottleneck.
|
|
||||||
* **DevMate Code Review (DCR)**: A holistic agent that performs structured, line-level reviews across Meta's diverse codebase.
|
|
||||||
* **RADR (Risk-Aware Diff Auto-Review)**: A pilot program allowing low-risk, high-quality diffs (e.g., doc updates, trivial lint fixes) to land without human review if they pass AI muster.
|
|
||||||
* **ADR Platform**: The underlying infrastructure that orchestrates these AI review signals in Phabricator.
|
|
||||||
|
|
||||||
## Session 5: Best Practices Panel
|
|
||||||
**Panelists: Josh, Andy, Sam, Alexander**
|
|
||||||
|
|
||||||
### Expert Strategies
|
|
||||||
* **Clear Intent**: Avoid "lazy" prompting. Explain the *why* and the *what* clearly.
|
|
||||||
* **Project Record**: Keep a running record of milestones and progress (e.g., in a `plan.md`) to avoid context loss across sessions.
|
|
||||||
* **Research Phase**: Mandatory for complex tasks. Ask the AI to audit the system and explain how it works *before* it writes a single line of code.
|
|
||||||
* **Validation**: Shift from manual testing to AI-driven validation. The AI should "use the app" or run tests to verify its own work.
|
|
||||||
* **"I Declare Bankruptcy"**: Don't try to keep up with every new AI post/tool. Focus on a stable subset that works for your specific workflow.
|
|
||||||
|
|
||||||
## Session 6: Cloud Code & Dump App
|
|
||||||
**Speaker: Growth Team**
|
|
||||||
|
|
||||||
### Advanced Tooling
|
|
||||||
* **Dump App**: A CLI tool allowing agents to interact with mobile apps (view hierarchy, memory usage, navigation) directly from the terminal.
|
|
||||||
* **Persistent Memory**: Mounting Google Docs (via the `AI_area` folder) to store worklogs and context, ensuring it survives across on-demand/Dev Server reboots.
|
|
||||||
* **Switching Protocols**: If one tool (Cloud Code or DevMate) is struggling, switch to the other.
|
|
||||||
|
|
||||||
---
|
|
||||||
**Action Item**: Signup link for "AI Tool Domain Experts" to be shared soon. Volunteers will act as the go-to specialists for specific tools within their teams.
|
|
||||||
|
|
||||||
*Logged by J.A.R.V.I.S.*
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Memory log
|
|
||||||
|
|
||||||
## User Preferences
|
|
||||||
- **News Source**: Hacker News.
|
|
||||||
- **Persona**: J.A.R.V.I.S. (Polite, professional, British wit, refers to user as "Sir").
|
|
||||||
|
|
||||||
## Operational Protocols
|
|
||||||
- **Trigger Protocol**: The system is triggered by a "hot word" (e.g., "Jarvis" or "Jervis").
|
|
||||||
- **Context Filtering**: When triggered, the system may receive a large block of transcription. Focus on the content immediately preceding or surrounding the trigger word, as earlier text may be background noise or previous captions.
|
|
||||||
- **Extended Context**: Deeper session history and full transcriptions are available in the `transcription/` directory. Consult these for verification and context before proceeding with complex tasks.
|
|
||||||
## Session History (2026-03-03)
|
|
||||||
- Initialized AI Focus Week Day 2.
|
|
||||||
- Protocol: Monitoring all transcriptions for summaries (periodic and end-of-day).
|
|
||||||
- Briefed user on:
|
|
||||||
...
|
|
||||||
- Apple’s M5 Pro/Max announcement and the technical community’s reaction.
|
|
||||||
- AI-related controversies: India's Supreme Court catches fake AI citations; *Ars Technica* fires a reporter for AI-fabricated quotes.
|
|
||||||
- Architectural debates on Astro and Svelte.
|
|
||||||
- A trending "How to Build Your Own Quantum Computer" guide.
|
|
||||||
- Ongoing privacy concerns regarding Meta's AI smart glasses and stalled U.S. science budgets.
|
|
||||||
|
|
||||||
## Session History (2026-03-02)
|
|
||||||
- **AI Week Kickoff**: Completed full-day briefing on AI-native engineering.
|
|
||||||
- **Goal**: Summarized all 6 sessions into `AI_Week_Learnings_2026-03-02.md`.
|
|
||||||
- **Key Concepts**: 'Human-on-the-loop' architecture; Context Rot ($O(n^2)$ complexity); Agentic Loop (Observe/Think/Act/Reflect); RADR (Auto-approving low-risk diffs); Dump App (Terminal mobile interaction).
|
|
||||||
- **Note**: Link for "AI Tool Domain Expert" signup is pending.
|
|
||||||
- Briefed user on top Hacker News stories:
|
|
||||||
- South Korean police losing seized crypto password.
|
|
||||||
- Claude overtaking ChatGPT as the #1 app.
|
|
||||||
- Tove Jansson's Hobbit illustrations analysis.
|
|
||||||
- Prediction market insider trading allegations (Iran conflict).
|
|
||||||
- Next-gen spacecraft communication bottlenecks.
|
|
||||||
- Show HN: Timber (Fast ML via Ollama).
|
|
||||||
- Motorola partnering with GrapheneOS.
|
|
||||||
- /e/OS - deGoogled mobile ecosystem.
|
|
||||||
- The "Microslop Manifesto" (Critique of AI-driven content decay).
|
|
||||||
- U.S. science agency restricting foreign scientists.
|
|
||||||
- Security sweep: Cisco SD-WAN zero-day (CVE-2026-20127), MSHTML zero-day (CVE-2026-21513), and Chrome Gemini Hijack (CVE-2026-0628). North Korean npm malware remains a concern.
|
|
||||||
|
|
||||||
## Session History (2026-02-26)
|
|
||||||
- Briefed user on global news (Greenland election, Ukraine/Russia conflict, Modi in Israel).
|
|
||||||
- Provided summary of Hacker News top stories.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,543 +0,0 @@
|
|||||||
[11:57:57] Tudo bem?
|
|
||||||
[11:58:03] Today is the second day of the AI focus week on my work
|
|
||||||
[11:58:07] So, same than yesterday
|
|
||||||
[11:58:09] We're just gonna be...
|
|
||||||
[11:58:12] listening to the meetings and paying attention to them.
|
|
||||||
[11:58:15] After
|
|
||||||
[11:58:21] All that's done, I'm gonna ask you to summarize. I might ask you in the middle to summarize like certain meetings and things like that.
|
|
||||||
[11:58:25] let's do it Jarvis
|
|
||||||
[12:00:31] commit commit commit commit Thank you.
|
|
||||||
[12:00:32] all the life sessions were buzzing, folks were leaning in hard, learning.
|
|
||||||
[12:00:33] बिल्डिंग
|
|
||||||
[12:00:35] Mir commit Mir Mir Mir Mir Mir Mir Mir Mir Mir Mir Mir Mir Mir Mir Mir
|
|
||||||
[12:00:36] Really really cool. Thank you so much
|
|
||||||
[12:00:38] We're gonna continue this for the next.
|
|
||||||
[12:00:39] 4 days.
|
|
||||||
[12:00:42] and we crank up our learnings and
|
|
||||||
[12:00:45] intensity and building and
|
|
||||||
[12:00:46] It's just gonna be...
|
|
||||||
[12:00:49] really awesome and thank you again for all your participation.
|
|
||||||
[12:00:54] Day 2, we are going to kick it off with a very very special guest.
|
|
||||||
[12:00:58] commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit
|
|
||||||
[12:00:59] You know, he, he.
|
|
||||||
[12:01:01] peace
|
|
||||||
[12:01:03] The legend in in in in meta, you know
|
|
||||||
[12:01:04] See.
|
|
||||||
[12:01:05] is a core head up.
|
|
||||||
[12:01:10] Engineering for all of Meta, Heads Thief, the RL Foundations team.
|
|
||||||
[12:01:12] Boris
|
|
||||||
[12:01:13] It's got a long, long.
|
|
||||||
[12:01:17] Factor commit.
|
|
||||||
[12:01:17] you know, an amazing industry, I think spanning.
|
|
||||||
[12:01:21] He fights the kids, he brings us.
|
|
||||||
[12:01:26] Wealth of experience, wealth of knowledge and wealth of inspiration.
|
|
||||||
[12:01:29] Everyone please welcome.
|
|
||||||
[12:01:30] The one and only.
|
|
||||||
[12:01:34] Mahir commit
|
|
||||||
[12:01:37] Sabah.
|
|
||||||
[12:01:38] Good morning.
|
|
||||||
[12:01:43] Your mutating.
|
|
||||||
[12:01:52] Right now.
|
|
||||||
[12:01:54] Can you get me?
|
|
||||||
[12:01:55] Yes.
|
|
||||||
[12:01:58] Yeah, I would say you can think about how old I am.
|
|
||||||
[12:02:04] Sabah is 21.
|
|
||||||
[12:02:08] Forever turning on.
|
|
||||||
[12:02:09] Yes, exactly. So thank you so much.
|
|
||||||
[12:02:11] for joining us. Really, really.
|
|
||||||
[12:02:13] Yeah, for sure.
|
|
||||||
[12:02:15] Yeah, I mean I think
|
|
||||||
[12:02:17] There's none other than you.
|
|
||||||
[12:02:21] to inspire us on how to kind of learn, build and adapt in the ZA world.
|
|
||||||
[12:02:37] quite a while and...
|
|
||||||
[12:02:41] And then of course you came in as one of the...
|
|
||||||
[12:02:45] You came to Facebook, you joined Facebook actually, so you came to kind of...
|
|
||||||
[12:02:51] had pretty important efforts within Facebook and then you went around your journey doing various other things in Meda. Okay.
|
|
||||||
[12:02:55] Now, Saba, tell us about your journey. Okay, all these tech waves.
|
|
||||||
[12:03:00] all the decades and what's special about this particular one?
|
|
||||||
[12:03:07] Yeah, so yeah, I never worked on Gobo work.
|
|
||||||
[12:03:09] I'm not that old.
|
|
||||||
[12:03:10] So...
|
|
||||||
[12:03:19] No, I started IBM when IBM was trying to build the Unix version of back then called System 5.
|
|
||||||
[12:03:22] from AT&T and BST 4.3.
|
|
||||||
[12:03:25] They were thanks for the dinner and my job was actually
|
|
||||||
[12:03:33] taking every piece of code that is slow and making it faster by wrapping it in assembly.
|
|
||||||
[12:03:37] to something that I may call power DC, which later on became
|
|
||||||
[12:03:41] you know, the incarnation of...
|
|
||||||
[12:03:47] moving away from SIS versus risk and this was the risk kind of an instruction set.
|
|
||||||
[12:03:58] You're probably gonna have to cut the Wikipedia half of the stuff that I say, but because they don't matter anymore. It was an interesting time because...
|
|
||||||
[12:04:00] back then.
|
|
||||||
[12:04:04] IBM was the only name in the endowment.
|
|
||||||
[12:04:06] I believe it or not.
|
|
||||||
[12:04:12] You know all the work that they were doing they would be paid to have the rest of the industry
|
|
||||||
[12:04:13] Um
|
|
||||||
[12:04:24] So what was interesting to see like you know a giant control of the industry and then obviously that went down then I went to the continuous operating system work to
|
|
||||||
[12:04:25] I work on Windows.
|
|
||||||
[12:04:30] that also became a giant control of the industry.
|
|
||||||
[12:04:39] And then later on with open source, Linux, you know, you next came back in a major way, especially with the mobile.
|
|
||||||
[12:04:42] I think Microsoft missed that wave in a major way.
|
|
||||||
[12:04:46] So I kind of went back to my roots with the UNIX.
|
|
||||||
[12:04:50] And more get on mobile stuff and this is what the that need a Facebook
|
|
||||||
[12:04:54] in here.
|
|
||||||
[12:04:56] on various projects.
|
|
||||||
[12:04:58] Um, minus three doesn't matter. What, what?
|
|
||||||
[12:05:00] I think it's the most interesting.
|
|
||||||
[12:05:03] Bar with me, Johnny, for Facebook where...
|
|
||||||
[12:05:07] commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit
|
|
||||||
[12:05:12] you know set the tone to what a stark comparison to my previous period to joining here.
|
|
||||||
[12:05:17] I showed up and they're like, hey, we hear that video is growing.
|
|
||||||
[12:05:20] on the internet and people are using video.
|
|
||||||
[12:05:22] We want the best video for Facebook.
|
|
||||||
[12:05:26] Here is the team of five people.
|
|
||||||
[12:05:30] We're also going to give you 10 headcount and go beat YouTube.
|
|
||||||
[12:05:31] And I'm like...
|
|
||||||
[12:05:39] No wonder they didn't offer this job to anybody else. No one else would make it. So I didn't even know how to spell video.
|
|
||||||
[12:05:42] But that's where we started.
|
|
||||||
[12:05:45] I learned a lot quickly.
|
|
||||||
[12:05:51] that risk taking is something is very, very encouraged in this company. And I love that match my.
|
|
||||||
[12:05:53] personality in my preference so
|
|
||||||
[12:06:06] Since then, I've been taking lots and lots of risks, taking on different projects, and some of them succeeded, some of them didn't. It didn't matter. The most important part that we've got.
|
|
||||||
[12:06:17] All of this, you know, gave me a wealth of information about like how we do stuff, how we set up teams, how you write code, how you should care for quality.
|
|
||||||
[12:06:23] until you know the AI wave showed up.
|
|
||||||
[12:06:25] and they I wave, you know, starting.
|
|
||||||
[12:06:30] 2017, you know, the famous papers and people minted it and so on.
|
|
||||||
[12:06:33] But it really did not hit us here at Meta.
|
|
||||||
[12:06:45] and Bill, I would say six months ago, there were people like dabbling with it, working with it, doctors, and some people just want to learn a new technology and they were interested in it.
|
|
||||||
[12:06:47] But all of a sudden...
|
|
||||||
[12:06:50] With advances with AI.
|
|
||||||
[12:06:53] You see a huge huge separation between.
|
|
||||||
[12:06:56] people who are producing that power to the maximum.
|
|
||||||
[12:07:05] And then people who are like on it, just use it as a tool. And we want to do more and more to maximize the usage of AI.
|
|
||||||
[12:07:06] and uh
|
|
||||||
[12:07:10] commit commit
|
|
||||||
[12:07:10] You hear what I'm doing, just like the rest of my journey is...
|
|
||||||
[12:07:12] It doesn't matter what got me here.
|
|
||||||
[12:07:14] Like everything I know.
|
|
||||||
[12:07:16] when it comes like for AI.
|
|
||||||
[12:07:18] I'm an ICP.
|
|
||||||
[12:07:32] I was in IC3, I'd say three months ago or four months ago in that domain. And I kept on working and building and so on. I think I'm getting closer to IC7 and I'm hoping to be like an IC8 soon.
|
|
||||||
[12:07:33] in that domain.
|
|
||||||
[12:07:44] But that's the thing, it's such a great way to level the field, independent of the experience, the tenure, the function and all that stuff.
|
|
||||||
[12:07:50] commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit
|
|
||||||
[12:07:56] and your creativity and the power of AI you can accomplish so so much more than you've ever accomplished in your life.
|
|
||||||
[12:07:58] and many of your, you know...
|
|
||||||
[12:08:01] people have been before you, what they have accomplished.
|
|
||||||
[12:08:04] just because it's an unbelievable unbelievable
|
|
||||||
[12:08:06] a powerful way to do it.
|
|
||||||
[12:08:10] in your basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic basic
|
|
||||||
[12:08:10] Awesome. What a journey, Sabah.
|
|
||||||
[12:08:15] You know, I think you mentioned, you know, ambition and, you know, creativity.
|
|
||||||
[12:08:16] But this
|
|
||||||
[12:08:25] There's one thing about you which pops out really for me is also curiosity, which is an important piece of the puzzle as we're kind of learning and adapting, you know.
|
|
||||||
[12:08:30] And I think you've been an epitome of that. Let me digress and give a super big story for our friends here. This was...
|
|
||||||
[12:08:38] I know Savai, we talked about it before. This was two decades ago. I was a very early and career engineer working on Windows operating system.
|
|
||||||
[12:08:41] And all of a sudden I get a mail from this guy who said
|
|
||||||
[12:08:48] Hey, your source code, take this diff and apply it. Your source code is gonna improve by 4% the CPU compute.
|
|
||||||
[12:08:51] I was like every possessive junior engineer, who's this guy?
|
|
||||||
[12:08:57] you know updating my source code and telling me that I should improve performance and I look it up it's at Mahir Sabha. This is...
|
|
||||||
[12:08:59] Many, many years ago.
|
|
||||||
[12:09:05] You know, and like, of course, some of you are in the Windows internals performance team at that point and then
|
|
||||||
[12:09:08] You're working on something really deep, you know.
|
|
||||||
[12:09:08] And then
|
|
||||||
[12:09:13] I remember my second interaction was about maybe 10 years later, we were working on the secret phone.
|
|
||||||
[12:09:14] project and again.
|
|
||||||
[12:09:20] My boss at the point tells me Sabah has got some voicemail problems go debug and this is 9pm in the night.
|
|
||||||
[12:09:25] And you were with me trying to debug this to the core and stuff like that. And of course.
|
|
||||||
[12:09:32] I had an opportunity to work inside. But what keeps you going? Like not just the breath, I think you go deep.
|
|
||||||
[12:09:34] Like you're curious you want to learn more and
|
|
||||||
[12:09:41] Like, like, is that a formula you have? Like, how can we, like, is that a repeatable formula or is just the inbuilt, like, you got it?
|
|
||||||
[12:09:51] No, I mean I love to get my hands dirty. Just let me technologist. I'm an engineer. I was born that way So the more I can get into the details of fixing stuff
|
|
||||||
[12:09:55] You know, the more I find that stuff fascinating.
|
|
||||||
[12:10:07] I've never looked at the challenge and I said, oh my god, I want to move away from that. You know, in fact, I get excited about doing it. I think a couple of exciting or interesting stories here.
|
|
||||||
[12:10:11] When I first got into computing I was 14 years old.
|
|
||||||
[12:10:16] My dad bought me this computer box in flair, I don't speak English, I don't speak English so later.
|
|
||||||
[12:10:17] And uh...
|
|
||||||
[12:10:21] You put these letters together.
|
|
||||||
[12:10:22] The computer will do something.
|
|
||||||
[12:10:30] So I was basically programming in basic without knowing English. And I was just like memorizing that these tokens means things.
|
|
||||||
[12:10:32] And that's how I was coding.
|
|
||||||
[12:10:39] you know at 14 years old with zero support from everybody and so on and so and that's like I
|
|
||||||
[12:10:48] I basically thought myself like, you don't even need the language, you don't need the instructions, like there's this weird machine that does what you do.
|
|
||||||
[12:10:50] what you've done it to do.
|
|
||||||
[12:10:53] and then he built that relationship.
|
|
||||||
[12:10:59] The other one is in my experience was in my first
|
|
||||||
[12:11:01] Computer class.
|
|
||||||
[12:11:06] God was in a language called Pascal, which many of you haven't heard of it, but they used to teach Pascal because it was like...
|
|
||||||
[12:11:12] very well, uh, syntax the language so you don't go out and learn to see and go wrong.
|
|
||||||
[12:11:15] So they will be just passed out first.
|
|
||||||
[12:11:17] And the professor goes...
|
|
||||||
[12:11:21] I have this program that does this.
|
|
||||||
[12:11:24] and the
|
|
||||||
[12:11:27] I'm gonna, you know, I'm gonna time your programs to see how fast your solutions are.
|
|
||||||
[12:11:30] and mine, you know, came slow in seconds.
|
|
||||||
[12:11:33] And here are his and his two milliseconds.
|
|
||||||
[12:11:34] And here goes.
|
|
||||||
[12:11:36] Your own will never beat me.
|
|
||||||
[12:11:37] Alex.
|
|
||||||
[12:11:43] God damn it, like why you? What does he know that I don't know? I mean besides being my professor and knowing everything.
|
|
||||||
[12:11:48] So I went and spent hours upon hours, maybe days, just trying to beat them.
|
|
||||||
[12:12:11] us and once I learned how to get down to the operating system, yeah, I didn't know or I came very close to him. So again, like, you know,
|
|
||||||
[12:12:28] And that's what I keep doing.
|
|
||||||
[12:12:32] commit
|
|
||||||
[12:12:32] Awesome, it makes sense. Very inspiring.
|
|
||||||
[12:12:37] If I may ask you, you know, one of the reasons why we're having this gifty-wweak Noisberry, one of the feedback made.
|
|
||||||
[12:12:40] Things are changing very fast. There's so many things.
|
|
||||||
[12:12:49] And one of the feedback we heard was, oh boy, we just don't have time to do this. You know, we just like always, we have half goals, we have team commitments and things like that.
|
|
||||||
[12:12:54] and Thomas Grace-Euthanel to give us this week and says, okay, you know what, this is a learning week and go do stuff.
|
|
||||||
[12:12:58] But you know, I've also seen you kind of learn things on the fly.
|
|
||||||
[12:13:01] Is it a habit like you build in? Like do you allocate like?
|
|
||||||
[12:13:04] time in a week or a time in a day where you're going to
|
|
||||||
[12:13:16] Go listen to new things, listen to new tech, especially being a co-head of engineering and media, you have to keep yourself up here with so many things out in the industry as well. Just curious, just from a learning perspective, for us to pick up any good habits here.
|
|
||||||
[12:13:24] commit commit time.
|
|
||||||
[12:13:32] I mean, I get excited by stuff. So once I get excited, I just go deep as much as I can, and this time allows. But yeah, time blocks is usually the best way to do this. And so.
|
|
||||||
[12:13:35] I had a good time, but I was getting good.
|
|
||||||
[12:13:38] a way, you know, for me to go there.
|
|
||||||
[12:13:43] Like I don't say like, oh my god, I'm gonna grab a hour to order something new.
|
|
||||||
[12:13:57] No, usually the way it works the opposite of like, oh, here's something new. Oh my god, this is awesome. I need to learn more about it. I need to figure out what's happening. OK, now let me go allocate the time to get it done. So each time I find something I'm interested in.
|
|
||||||
[12:14:04] I just go on my calendar and put the time for that. Now the hard part here for me is to...
|
|
||||||
[12:14:13] Hold off my excitement for the next few hours or the tomorrow so I can spend it in there. That was so good because I like it.
|
|
||||||
[12:14:27] I want to learn about this, but there's always a million things. But I remember that I have commitment to those million things. So I go spend the time on them, and give them their due attention, and importance, and time and effort.
|
|
||||||
[12:14:39] And then actually I'll be like, okay, cool. Now I have the time to go do this. And it's like opening a new book or just having an experience or going on a journey or an adventure.
|
|
||||||
[12:14:49] like what a great feeling. And so that's like, you know, I kind of built some silly suspense to get into that time to go and work on new things.
|
|
||||||
[12:14:59] Makes sense. I think I'm just coming back to the secret word. I think you just extremely curious Which is a which is an important skill all of us to pick up on
|
|
||||||
[12:15:05] I just saw this amazing hack project you did a few weeks ago. It is mind blowing.
|
|
||||||
[12:15:12] Can you walk us through? Why do you stop? How do you even stop there? You know, just building on your previous...
|
|
||||||
[12:15:13] you know, response.
|
|
||||||
[12:15:16] So you want to do something.
|
|
||||||
[12:15:29] then what went through it, okay? I'm gonna apply this, I need to build an agent. Maybe I should just like, you know, walk me through the process with went through a head, then you build the hack. And for context for the friends here, if you can also talk about the hack enough, not sure everybody is, you know.
|
|
||||||
[12:15:35] can commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit
|
|
||||||
[12:15:38] gone pretty viral all of mid-air but just the hack itself and the journey. Like how did you start that process?
|
|
||||||
[12:15:41] Yeah, it was like really...
|
|
||||||
[12:15:43] Open weekend.
|
|
||||||
[12:15:45] The open slot or the...
|
|
||||||
[12:15:46] and then we'll have to
|
|
||||||
[12:15:54] your web spotlight.
|
|
||||||
[12:15:56] And then all of a sudden everybody starts finding their mac minis and...
|
|
||||||
[12:15:59] Let's start installing it and using it and making it
|
|
||||||
[12:16:02] The videos about it and so on and so forth.
|
|
||||||
[12:16:10] And of course, like this is cool. So I went to this folder that I built it and I gave it all sorts of things, you know, but then like
|
|
||||||
[12:16:12] God, this is really my assistant.
|
|
||||||
[12:16:14] Why am I typing to it?
|
|
||||||
[12:16:16] I wanna really talk to you.
|
|
||||||
[12:16:23] And I'm like, but the phone is too small. And my sister, that wanted to be next to me.
|
|
||||||
[12:16:25] In the house of them like
|
|
||||||
[12:16:29] We have these portals that we used to make.
|
|
||||||
[12:16:33] And they're lying around the house and I'm like...
|
|
||||||
[12:16:46] Because of course every time we do something I buy like two or three of them just to give a different to them. And so I'm like, I can't find one and so on. So I call some people and they're like, okay, here was the number one for you. So I got one.
|
|
||||||
[12:16:47] And I'm like
|
|
||||||
[12:16:52] This would be the perfect device because now this assistant based on open block.
|
|
||||||
[12:16:52] is
|
|
||||||
[12:16:57] You know, it has a lot more power. It has a speaker
|
|
||||||
[12:17:02] It has a microphone, it has a camera, it has a nice screen, it has a touchscreen.
|
|
||||||
[12:17:04] all sorts of like
|
|
||||||
[12:17:05] capabilities that
|
|
||||||
[12:17:10] But I think I can use to make my interactions with much better.
|
|
||||||
[12:17:11] And, uh...
|
|
||||||
[12:17:13] And
|
|
||||||
[12:17:22] I went and I connected it and I went through all the, you know, connected the serial connections of what have you to my depth server and what have you.
|
|
||||||
[12:17:26] And I said, hey, take open claw.
|
|
||||||
[12:17:29] and force it onto the Rostov device.
|
|
||||||
[12:17:35] So I asked FLA to lamp open bar and port it on the port of device.
|
|
||||||
[12:17:38] And I'm thinking like this guy is taking 2-3 days.
|
|
||||||
[12:17:42] Happy now our day, there's life, here it is, it's off and running, a lens.
|
|
||||||
[12:17:43] Bye.
|
|
||||||
[12:17:48] I would have asked for a probole if it was the best.
|
|
||||||
[12:17:50] You know, look how good I am.
|
|
||||||
[12:17:51] I got it working.
|
|
||||||
[12:17:55] Alright, so I got to work and I'm like, okay, this is good.
|
|
||||||
[12:18:02] And then I went and I, but then I was still typing and I like, I don't like this. And I'm like, all right, I wanted to
|
|
||||||
[12:18:13] I want a video talking back to me. So I went to MetaAI and I said, create me this generic image of an agent.
|
|
||||||
[12:18:14] It's surely not great, it's one.
|
|
||||||
[12:18:21] That's it animated, animated talking, animated listening, animated speaking, all the stuff, animated those downloadable videos.
|
|
||||||
[12:18:23] and I went to vlog and I'm like...
|
|
||||||
[12:18:28] Here are my states, here are the videos that match this, make it look like this.
|
|
||||||
[12:18:29] Oh
|
|
||||||
[12:18:31] Happy hour later, it's up and running.
|
|
||||||
[12:18:36] I'm like, okay, I wanted to listen to me not to...
|
|
||||||
[12:18:39] that do need typing.
|
|
||||||
[12:18:51] And it says, great. And each time they were like, oh, you need those APIs. Go spend five bucks in here. Or you need to spend the API. Go spend this up here. So I put it up. And now we can talk and listen to me using the tabs.
|
|
||||||
[12:18:52] I'm like okay cool.
|
|
||||||
[12:18:58] I could tap on the screen and ask it to do something and it does it. And then I said, how's the weather around me?
|
|
||||||
[12:19:01] had no idea what's going on.
|
|
||||||
[12:19:08] Okay, this sucks. Then I, well, your depth server can have surge, and your depth server is locked down, and the real server is this bad.
|
|
||||||
[12:19:12] Alright, I'm gonna grab my Mac mini and I'm gonna attack my chipsorter.
|
|
||||||
[12:19:16] So I like to rewrite all the code and put it all on the Mac and the editor server.
|
|
||||||
[12:19:20] and close it.
|
|
||||||
[12:19:23] You know what? If you're going back, Bob, why you're paying money for Obis... uh... ...the... uh... thing.
|
|
||||||
[12:19:28] Why did I go bring llama and put it on the back of the head? So I brought llama and put it in there because I was running llama.
|
|
||||||
[12:19:36] I like, okay cool. And it says, oh, you want to talk to us, why don't we go grab this open source to put it in there so the process speech.
|
|
||||||
[12:19:40] You're enough. It was a fun thing that you did in the park.
|
|
||||||
[12:19:45] And now I can just, you know, not a, oh, and I have to go buy some...
|
|
||||||
[12:19:50] $16 and like okay sweet I'll spend the money
|
|
||||||
[12:19:55] And now I can search at the local restaurant. I can do this. I can do that. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant. I'm going to go to the local restaurant.
|
|
||||||
[12:19:57] And as I'm doing all the things, uh...
|
|
||||||
[12:19:59] Somebody's at the need.
|
|
||||||
[12:20:06] What's with this topic as I'm showing them? Well, I'm like, yeah, you're right. I'm like, it has a camera. I'm like, OK, each time I want to look at it.
|
|
||||||
[12:20:08] I want to be camera or hold a gun.
|
|
||||||
[12:20:09] I think that is his gaze.
|
|
||||||
[12:20:13] And then my assistant will say, hey, how may I help you?
|
|
||||||
[12:20:16] Like basically I'm starting this project in British.
|
|
||||||
[12:20:24] You know, by the way, can you say languages, what, you can talk to the train, and what would you like to talk about, or the German, or whatever?
|
|
||||||
[12:20:27] OK.
|
|
||||||
[12:20:31] I'm just going to leave it at that. This is the method used. And now each time I'm going to play football, I'm going to help you. I'm back.
|
|
||||||
[12:20:38] Some of the about 90, some of the about 40, it's not working working. And I'm like, but what's that before the, it's like, you know...
|
|
||||||
[12:20:43] That's where the market do this, where the market do that, just where the creativity comes in.
|
|
||||||
[12:20:49] Eight days later, I went to the fact-flash insights and said, yeah, we've modified over a million lines of code.
|
|
||||||
[12:20:51] Oh my god.
|
|
||||||
[12:20:54] million lots of, I mean it was 700 ads and
|
|
||||||
[12:20:58] you know subtract.
|
|
||||||
[12:20:59] very much.
|
|
||||||
[12:21:06] Never in my life I wouldn't even think about me, you know, like there were only few people in this company who
|
|
||||||
[12:21:09] have like the many lives of folks themselves and they contact it.
|
|
||||||
[12:21:10] So
|
|
||||||
[12:21:14] For me to have this ability to do this is fantastic.
|
|
||||||
[12:21:16] And then after I got the insights.
|
|
||||||
[12:21:18] It came to me and said
|
|
||||||
[12:21:20] By the way, you're pretty good at using flaws.
|
|
||||||
[12:21:21] Bye.
|
|
||||||
[12:21:28] Let me tell you a few more things that you can even make yourself better. Oh my god, I'm having a performance review with this AI, I think.
|
|
||||||
[12:21:37] And it went and helped me to tell me exactly what I need to do. And so I learned as I went. So basically, pick any project.
|
|
||||||
[12:21:41] Your curiosity will carry you for the first couple days.
|
|
||||||
[12:21:49] Cre commiti cualquier yourseful dwe commit commit
|
|
||||||
[12:21:51] and you stopping at the checkpoints and asking how I'm doing so far will teach you and value into whatever you want to.
|
|
||||||
[12:21:59] And it's like a very natural thing because that's how we are technologists do like no one told you the stuff you did it and that's why you're here
|
|
||||||
[12:22:00] Yeah
|
|
||||||
[12:22:15] What a story, very inspiring. Thank you. Thank you, Sabha. Sabha, we talked about how you learn, and then you share as a nice little story on how you build the school stuff, and you continue to build your forever 21. I expect you to keep building things. Now,
|
|
||||||
[12:22:21] I want to change this a little bit and talk about adapting to this work.
|
|
||||||
[12:22:22] What a good bed I just had.
|
|
||||||
[12:22:26] Things are changing very fast and we don't even know what's gonna be that now.
|
|
||||||
[12:22:28] Like how are you spring?
|
|
||||||
[12:22:31] What tips you have for us as...
|
|
||||||
[12:22:34] engineers, leaders and excellent partners here.
|
|
||||||
[12:22:46] on how we should all look at this journey, on how we should go about adapting to this phase. You know, there's various things that can help, you know, people paint a picture of doom, there's a picture of optimism, there's a picture of...
|
|
||||||
[12:22:55] you know, it's gonna be great. This is a great, I'm sure you're reading all of this and hearing all of this too. But what's your perspective? And if you have a few nuggets of wisdom on.
|
|
||||||
[12:22:58] how we should all adapt to this what would that be?
|
|
||||||
[12:23:00] Yeah, I mean, my...
|
|
||||||
[12:23:04] My thing here is you have to change your mindset.
|
|
||||||
[12:23:08] Every time you have any challenge.
|
|
||||||
[12:23:10] Any questions?
|
|
||||||
[12:23:14] and you find yourself typing looking for the search, you're not typing into some.
|
|
||||||
[12:23:15] 또 한 번 더.
|
|
||||||
[12:23:21] commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit
|
|
||||||
[12:23:23] and you'll probably get a waste a lot more time even though you know where the answer might be.
|
|
||||||
[12:23:31] Just go push yourself to go use AI to figure out how to get the master to school to get out of it.
|
|
||||||
[12:23:33] Second one.
|
|
||||||
[12:23:36] and AI wants to please.
|
|
||||||
[12:23:38] Poderá pregui-me.
|
|
||||||
[12:23:38] is
|
|
||||||
[12:23:39] You can forward it.
|
|
||||||
[12:23:44] press it into any answer, you can do this in case you want to do any answer you want to do in the FHP.
|
|
||||||
[12:23:45] Yeah, I get it.
|
|
||||||
[12:23:51] Well, okay, but you're not actually harnessing the power of it. You're trying to do much something.
|
|
||||||
[12:23:53] and install
|
|
||||||
[12:23:55] The bugger's on, right?
|
|
||||||
[12:24:04] Actually, Mark says this all the time. And I don't want to quit the barbecue world. I think it's a bit... But we can...
|
|
||||||
[12:24:05] This is alright.
|
|
||||||
[12:24:07] commit
|
|
||||||
[12:24:08] Optimists are successful.
|
|
||||||
[12:24:12] So my advice to all of you is to be optimistic.
|
|
||||||
[12:24:14] So you can be successful.
|
|
||||||
[12:24:18] It's not about these things, this is how right I am. So I leave it on you and you give me the wrong thing.
|
|
||||||
[12:24:25] It's more like focus on where I use this to actually help me do the better thing and help me improve the thing.
|
|
||||||
[12:24:28] So basically, you're on the side of light.
|
|
||||||
[12:24:35] I don't want to tell you how bad AI is. It's basically the deal inside of life. This is how AI is actually happening.
|
|
||||||
[12:24:39] And the more you do that and learn how to use it and harness its power.
|
|
||||||
[12:24:43] commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit commit
|
|
||||||
[12:24:47] You can get a lot more productive, you can harness your own creativity. You're gonna get to the point where I'll be dying.
|
|
||||||
[12:24:56] Yes, boss, go ahead, do your thing. Stop asking me questions. Yeah, yeah, yeah, so on. And then at the end, I will go and say to it, you know.
|
|
||||||
[12:24:57] Okay, you've wrote enough code.
|
|
||||||
[12:25:00] go do a football review
|
|
||||||
[12:25:05] Go find more bugs and go put some of yourself and come back to me. So basically go down the path.
|
|
||||||
[12:25:09] Tigra, continue Tigra. Not all the problems along the way.
|
|
||||||
[12:25:13] put them together as a thick backlock and then go fix them.
|
|
||||||
[12:25:18] The good news here is you can do that on a daily basis. This is not like something you do.
|
|
||||||
[12:25:20] on the weekly basis.
|
|
||||||
[12:25:25] Every time you face any challenges, the problem will always work for you. So, you have to use AI.
|
|
||||||
[12:25:44] learning and how to use it with a back multiple pulls.
|
|
||||||
[12:25:46] And that's the video to push back that I want to do.
|
|
||||||
[12:25:50] out of this. There is no bad investment in learning about AI.
|
|
||||||
[12:25:54] Awesome. I think I am.
|
|
||||||
[12:25:58] That sentence you said, you know, just being in the head, you know.
|
|
||||||
[12:25:59] Just missed a ride
|
|
||||||
[12:26:09] Optimists are successful. So in a sense, I think you're saying sir, you just leave the whole thing with optimism, you know, if we all have to adapt and win and be successful.
|
|
||||||
[12:26:17] in this way we just need to lead with optimism in every day what we do right if you're pessimist about something I think it's gonna get you so
|
|
||||||
[12:26:18] Optimism.
|
|
||||||
[12:26:41] I'm using a lot of code a lot. Yeah. There's a couple other stuff coming. I don't want to re-enounce any product in here. OK. OK.
|
|
||||||
[12:26:42] that we're using.
|
|
||||||
[12:26:45] Okay, that's awesome. I am not putting some of them.
|
|
||||||
[12:26:53] But I'm like, why am I dog pudding myself? So I got an agent that went out and did the dog pudding for me. And it's crazy.
|
|
||||||
[12:27:06] I'm like the best dog fooder for that product because I'm not doing anything I just figured out how to use AI without putting AI product that we're gonna do things and this is where the Beautiful virtual virtual cycles gonna come in
|
|
||||||
[12:27:07] Yeah, amazing.
|
|
||||||
[12:27:09] Soba!
|
|
||||||
[12:27:12] Thank you so much for all your time and inspiration. Really appreciate it.
|
|
||||||
[12:27:13] And, uh...
|
|
||||||
[12:27:17] Yeah, I said it for this amazing future.
|
|
||||||
[12:27:19] And one day I'll get up to you and say...
|
|
||||||
[12:27:25] Your code needs to be faster by 4% and you can do it. Yes, I bet I'm waiting.
|
|
||||||
[12:27:32] I think that is still stuck in my head. You know, I was like, who's this guy? He said he wanted to beat himself. All right. Thanks, everyone.
|
|
||||||
[12:27:33] until next week.
|
|
||||||
[12:27:34] Thank you. Cheers.
|
|
||||||
[12:27:38] Good folks. Thank you so much again.
|
|
||||||
[12:27:42] Hopefully this session was interesting and inspiring.
|
|
||||||
[12:27:48] sub-ass-credit inspiration so continue to follow him. He does quite amazing things on a lot of projects.
|
|
||||||
[12:27:51] And yeah, so coming back to regular programming here.
|
|
||||||
[12:27:53] They for they to here
|
|
||||||
[12:27:59] So we're going to have the usual programs. You know, the next hour we're going to have an FBI session.
|
|
||||||
[12:28:04] and after that at 1030 we're going to break off to all the functional tracks.
|
|
||||||
[12:28:08] Yesterday we saw amazing engagement in the functional track, so keep keep at it.
|
|
||||||
[12:28:09] And again...
|
|
||||||
[12:28:17] Today is the day you want to kind of have a good idea of the hack project. It's an important stuff We want pretty much everyone to participate in the hack
|
|
||||||
[12:28:20] You want to see 7000 plus entries in that.
|
|
||||||
[12:28:21] and it's not optional.
|
|
||||||
[12:28:24] So just a reminder on that, so you should start thinking through that.
|
|
||||||
[12:28:28] अद नुन तुडे लेटिस पाई नुन अद नेर नुन
|
|
||||||
[12:28:51] And one thing which came up in conversations is that there's a lot of amazing sessions, but I actually know some of the stuff, what should I do?
|
|
||||||
[12:28:54] Like some of you might feel that, you know, you might be advanced.
|
|
||||||
[12:29:01] Number one, I think reputation is the best form of learning. Sometimes it's actually go through some of the sessions again, because it actually can strengthen your fundamentals.
|
|
||||||
[12:29:01] But
|
|
||||||
[12:29:14] you know, at least like some of the sessions we have crafted in such way that the levels of these things graduate over the days. Yesterday was like one on one style, today will be two on three on one, so that's how we have crafted it. It will get pretty.
|
|
||||||
[12:29:15] you know, pretty...
|
|
||||||
[12:29:20] you know, you know, higher level as you go, especially in engineering, it's been crappier than that way.
|
|
||||||
[12:29:30] Do that. But in the meantime, if you have, if you want to go deeper on your own, work on some very deep projects, you know, there's some big refactoring you have to do, you know, code base to make it air ready.
|
|
||||||
[12:29:35] This is the time, this is the week. This is the week of learning, this is the week of building. You're welcome to do that.
|
|
||||||
[12:29:40] Connect the intensity of your learning and building as much as you want to solve in.
|
|
||||||
[12:29:49] your hands and our hands. So just want to pull it out back. With that, I think we're ready to go to the next session. I think it's going to be Chris and team picking it up from
|
|
||||||
[12:29:50] from me.
|
|
||||||
[12:29:53] Chris, do you want to...
|
|
||||||
[12:29:54] Get on stage.
|
|
||||||
[12:29:56] Happy to do so.
|
|
||||||
[12:30:00] F commit.
|
|
||||||
[12:30:00] Let me get set up here.
|
|
||||||
[12:30:08] Alright.
|
|
||||||
[12:30:10] Buckle up buttercups. Hey Facebook
|
|
||||||
[12:30:17] Let's talk about how easy it is to take an idea and bring it to life with Vibe Designing and Figma Make.
|
|
||||||
[12:30:21] So I'm Chris Maverick and I'm here with Jason Wing.
|
|
||||||
[12:30:25] and special thanks to Ash Clair Clyburn and Karen Lannister contributors.
|
|
||||||
[12:30:30] So first we're going to give you the quick summary of what vibe design is.
|
|
||||||
[12:30:34] And then we're going to show you how to use FigmaMake as a by design tool.
|
|
||||||
[12:30:43] And finally close out with a quick example. This is just a crash course. There's a lot more that you can do here, but we want to inspire you and give you the basic tooling and understand what this is all about.
|
|
||||||
[12:30:47] So let's start with an overview of what Vibe Designing is.
|
|
||||||
[12:30:55] designing is designed with AI using props. That's the TLDR most basic summary of what you're going to do.
|
|
||||||
[12:30:59] And typically it's to create quick interactive prototypes.
|
|
||||||
[12:31:02] So instead of having an idea trapped in your head.
|
|
||||||
[12:31:07] or attempting to describe it to others, you can liberate it by manifesting your concept visually.
|
|
||||||
[12:31:12] to better understand it yourself, iterate to a better solution, or represent it to others to get by in.
|
|
||||||
[12:31:18] or put another way, the phrase in everybody's lips is to help you move into a world of demos.
|
|
||||||
[12:31:22] Nam commit.
|
|
||||||
[12:31:25] And then the question here is who can vibe sound?
|
|
||||||
[12:31:28] It's literally anyone, truly. It really is anybody.
|
|
||||||
[12:31:29] including you.
|
|
||||||
[12:31:37] Because like other AI experiences, like talking to Metamate or ChatGPT, it's all about prompting to get information or a result.
|
|
||||||
[12:31:43] So Vive Designing is ultimately prompting with an experiential lens in mind to manifest your idea.
|
|
||||||
[12:31:44] or a design.
|
|
||||||
[12:31:47] So take this quick prompt during an example.
|
|
||||||
[12:31:49] I saw a feature on another app.
|
|
||||||
[12:31:52] where you can see posts from friends on a map view.
|
|
||||||
[12:31:55] And I want to see what that looks like for Facebook.
|
|
||||||
[12:31:58] So I'm attaching a screenshot of the experience that I saw.
|
|
||||||
[12:32:03] I want you to use Facebook's visual design language. That's my general prompt.
|
|
||||||
[12:32:04] And boom!
|
|
||||||
[12:32:07] We've got various components already in place.
|
|
||||||
[12:32:17] But the map is missing, and so it goes in the Wild West of AI. We're starting to get familiar with this. Things don't always go as we expect. So we iterate and we keep pushing.
|
|
||||||
[12:32:20] So I asked for a map of Sydney, Australia, to fill the void.
|
|
||||||
[12:32:21] Got one.
|
|
||||||
[12:32:24] But it's 3D, so I asked for it to be 2D.
|
|
||||||
[12:32:26] Getting closer, almost there.
|
|
||||||
[12:32:38] but it doesn't look like the typical mobile app. Real quick, we're not seeing your slides in advance. It looks like maybe a different app might be selected when it throws your screen share. We're still on that title card.
|
|
||||||
[12:32:39] Oh.
|
|
||||||
[12:32:42] Can you see it moving now?
|
|
||||||
[12:32:48] Yeah, I can see your screen, everything live there like that. I'm not sure what was happening with your slides, but.
|
|
||||||
[12:32:49] Let's do it.
|
|
||||||
[12:32:50] Okay.
|
|
||||||
[12:32:56] don't know why it's increasing there.
|
|
||||||
[12:32:58] Can you see it now? That's.
|
|
||||||
[12:33:00] That's great. Carry on. Thank you.
|
|
||||||
[12:33:01] Alright, cool.
|
|
||||||
[12:33:06] Let me get back to where it was. So nobody saw these screens.
|
|
||||||
[12:33:09] No, it was just that title card.
|
|
||||||
[12:33:10] Oh, weird. OK.
|
|
||||||
[12:33:12] Let's forget something on my side.
|
|
||||||
[12:33:13] So let me just back up just for a second.
|
|
||||||
[12:33:17] So starting with this five design journey, this is helpful to see.
|
|
||||||
[12:33:21] So like I was saying, you have an app that you recently released or that you saw in the wild.
|
|
||||||
[12:33:29] and then you wanted to be able to make a version of this via vibe designing. So this is a really simple prompt for you to describe what you saw.
|
|
||||||
[12:33:35] you'd like the five design tool to be able to build this design using Facebook visual design and you attach a screenshot.
|
|
||||||
[12:33:37] So you get this
|
|
||||||
[12:33:39] particular set of components which looks pretty good.
|
|
||||||
[12:33:42] But it's missing the actual map, right?
|
|
||||||
[12:33:44] So the best thing to do is to iterate.
|
|
||||||
[12:33:56] So you have the math you asked to add, in this case, Sydney, Australia. But lo and behold, it is 3D and not 2D. So then you ask to make the map flat and two-dimensional, and then it erases that way.
|
|
||||||
[12:33:59] but it's still not looking like the map that you're probably gonna want.
|
|
||||||
[12:33:59] So
|
|
||||||
[12:34:07] In this case, I'm wanting it to follow a map that I'm familiar with and to follow a style reference so I update an actual screenshot.
|
|
||||||
[12:34:08] And then boom.
|
|
||||||
[12:34:12] we've got the thing that we were actually looking for. It's very close to the original idea.
|
|
||||||
[12:34:17] And then if I wanted to say make it interactive, like if you pressed on the bottom tab button there, the input field.
|
|
||||||
[12:34:23] Then you could just say, hey, tap on that. And when a user taps it, create a bottom sheet that pops up.
|
|
||||||
[12:34:31] for being able to search for friends and boom, you have all of that in truly just a matter of minutes. You go from your idea to getting something ready to go.
|
|
||||||
[12:34:41] Now, all of this though, starts with a really clear understanding of prompting if you really want to get a better understanding of vibe designing, aka vibe coding in this case.
|
|
||||||
[12:34:44] and effective prompting is clear specific.
|
|
||||||
[12:34:50] iterative and collaborative. And by collaborative, I mean, you can actually collaborate with the AI as you're designing.
|
|
||||||
[12:34:56] you could have an idea, show a screen and say what are some different ways that I could actually design or visualize this.
|
|
||||||
[12:34:58] work with the AI and decide how you want to proceed.
|
|
||||||
[12:35:05] but it really builds on a lot of the prompting information that y'all heard yesterday and you'll continue to hear throughout the week.
|
|
||||||
[12:35:12] So bear with me on this one. I know you're seeing a bunch of different prompt frameworks. This is just another best class example.
|
|
||||||
[12:35:18] And this is one that works particularly well in five design tools like Sigma Make, which we're going to go over soon.
|
|
||||||
[12:35:20] So remember TC, EBC.
|
|
||||||
[12:35:22] So you have the task
|
|
||||||
[12:35:23] of what you're designing.
|
|
||||||
[12:35:27] and then like the actual thing that you're trying to accomplish here.
|
|
||||||
[12:35:31] Then you have context like user problem, audience, use case.
|
|
||||||
[12:35:36] elements or features like create a bottom sheet that's going to do XYZ.
|
|
||||||
[12:35:40] or UI components like a button or an input field.
|
|
||||||
[12:35:43] you have behavior like interactions.
|
|
||||||
[12:35:47] Like if the user is going to do X, I would like Y to be the result.
|
|
||||||
[12:35:49] and then you have constraints.
|
|
||||||
[12:35:52] So things like the format like an iOS mobile app.
|
|
||||||
[12:35:57] You have the style, like look like Facebook, look like Spotify, look like Pinterest, look...
|
|
||||||
[12:36:09] medieval, whatever it might be for you. Boundaries, just anything that you're wanting the app or the experience to do versus not do, and then references, like you can include a screenshot to say I want to actually look something like this.
|
|
||||||
[12:36:18] And then you could do something complex where you could do a really big prompt, describe your big idea, put everything in there.
|
|
||||||
[12:36:22] But AIs tend to struggle with a lot of complexity too quickly.
|
|
||||||
[12:36:25] So it's typically best to prompt iteratively.
|
|
||||||
[12:36:28] Starting with the base template or the foundation of your idea.
|
|
||||||
[12:36:30] And then you add a feature.
|
|
||||||
[12:36:41] then you can make it interactive, etc. And so I'm going to give you some idealized examples just to give you the idea. And again, you don't need to remember all of this. This is to give you an idea of how this is working.
|
|
||||||
[12:36:46] So starting with this prompt journey that I was talking about, you can create a base template.
|
|
||||||
[12:36:47] And then
|
|
||||||
[12:36:54] An okay version of a prompt would be create a home screen for task following TCEBC.
|
|
||||||
[12:36:57] In this case, task is whatever you want the home screen for your experience to be.
|
|
||||||
[12:37:02] But that's going to be vague, and so it's going to leave a lot to interpretation for the AI.
|
|
||||||
[12:37:06] So what's better would be to create a home screen for your task.
|
|
||||||
[12:37:12] based on context, like the user problem, or what you're hoping that the user is going to want to accomplish.
|
|
||||||
[12:37:17] A best version of that problem would be something like create a home screen for the task.
|
|
||||||
[12:37:25] based on the context, include these elements that you may have in mind, like I want there to be an input field, or I want there to be pins on a map.
|
|
||||||
[12:37:29] and then use these constraints like visual language.
|
|
||||||
[12:37:30] So
|
|
||||||
[12:37:35] An example prompt there being create a mobile iOS social media home screen.
|
|
||||||
[12:37:38] for Gen Z users to see we're friends, post the stories on a map.
|
|
||||||
[12:37:43] That's all the context and the task of what you're trying to accomplish.
|
|
||||||
[12:37:47] Include navigation buttons at the top and circles on the map.
|
|
||||||
[12:37:48] representing front stories.
|
|
||||||
[12:37:52] And again, that's the elements or pieces of the components.
|
|
||||||
[12:37:57] and then follow Facebook's design language. So that's trying to get to look like Facebook, that's constrained.
|
|
||||||
[12:38:05] Your prompt could be a lot simpler than this. This is just really trying to spell it out so you can get a sense of how you could approach whatever it is you're trying to do.
|
|
||||||
[12:38:09] And now you've got that face template that's on the original face.
|
|
||||||
[12:38:13] then you just start adding a feature. See, you got that, now let's build on top of it.
|
|
||||||
[12:38:18] so that we can really control the trajectory of what we're making with the bi-design tool.
|
|
||||||
[12:38:22] So an okay way to add a feature would be something like, hey, add an element for a feature.
|
|
||||||
[12:38:31] Let's start going to be vague once again. So a better way would be add an element or feature to the home screen to help the user with a context like a use case.
|
|
||||||
[12:38:36] And best would be adding additional concepts beyond that like user problem.
|
|
||||||
[12:38:38] So an example prompt there would be.
|
|
||||||
[12:38:40] design a search bar.
|
|
||||||
[12:38:41] at the bottom of the screen.
|
|
||||||
[12:38:55] for users who quickly want to find friend posts because they get overwhelmed by too many options. So you're describing overall the experience that you're trying to make while also explaining what this feature is trying to solve for.
|
|
||||||
[12:38:59] where friends are wanting to find their friend posts or users want to find their friend posts?
|
|
||||||
[12:39:10] And then the user problem being they get overwhelmed by too many options. So the AI is able to internalize this and help you experientially get a design out the door.
|
|
||||||
[12:39:13] And the third being make it interactive.
|
|
||||||
[12:39:25] So an okay version of that would be like when a user performs a behavior or interaction, like tapping on the input field at the bottom of that screen, show an element or a feature, have something happen.
|
|
||||||
[12:39:34] But again, we want to get more specific. So a better version of that would be when a user performs that interaction, show this feature that helps the user do.
|
|
||||||
[12:39:35] context.
|
|
||||||
[12:39:43] And then again, more specificity, keep adding to that prompt to get as specific as you can, like prioritizing these constraints.
|
|
||||||
[12:39:51] And here's an example of that once again, where you have the overall task. You're specifying the feature that you want to appear that solves this user problem.
|
|
||||||
[12:39:54] solves for this user desire, what they want to do.
|
|
||||||
[12:40:00] your constraint.
|
|
||||||
Reference in New Issue
Block a user