Compare commits

1 Commits

55 changed files with 2162 additions and 6246 deletions
+11 -4
View File
@@ -1,7 +1,14 @@
venv/
__pycache__/
*.pyc
.DS_Store
.venv/
venv/
.env
data/*.log
data/transcriptions/
data/speakers/*.npy
data/faces/*.npy
models/
*.wav
*.mp3
logs/
.DS_Store
/tmp/
jarvis.yaml
+87
View File
@@ -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.
+57 -37
View File
@@ -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
1. **Install PortAudio** (macOS):
```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
## Architecture
```
Mic 16kHz -> VAD (silero) -> Wakeword (openWakeWord TFLite) -> STT (faster-whisper int8) -> SpeakerID (ECAPA) + FaceID (OpenCV) -> LLM (ollama/gemini_cli/hermes) -> TTS (piper) -> Speaker
```
1. Say "**Hey Jarvis**".
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>"`.
## Quick start on iMac
## 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.
- **SpeechRecognition**: Uses Google's Web Speech API for transcription.
- **Gemini CLI**: The brain that processes the commands and calls agent tools.
# for audio libs you may need (if sudo works, otherwise pip wheels should work)
# sudo apt install portaudio19-dev libasound2-dev espeak
# 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
+21
View File
@@ -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.
-250
View File
@@ -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()
+69
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
# jarvis package
+2
View File
@@ -0,0 +1,2 @@
# audio
from .capture import AudioCapture
+76
View File
@@ -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()
+130
View File
@@ -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
+52
View File
@@ -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)
+101
View File
@@ -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)
+101
View File
@@ -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
View File
@@ -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()
+28
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
# core
from .pipeline import JarvisPipeline
+369
View File
@@ -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
+38
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
# jarvis package
+10
View File
@@ -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)
+6
View File
@@ -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."
+75
View File
@@ -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
+29
View File
@@ -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}"
+38
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
# jarvis package
+54
View File
@@ -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)
+28
View File
@@ -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
+78
View File
@@ -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())
+1
View File
@@ -0,0 +1 @@
# jarvis package
+4
View File
@@ -0,0 +1,4 @@
import numpy as np
class STTEngine:
def transcribe(self, audio: np.ndarray, sr: int = 16000) -> dict:
raise NotImplementedError
+45
View File
@@ -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)}
+1
View File
@@ -0,0 +1 @@
# jarvis package
+3
View File
@@ -0,0 +1,3 @@
class TTSEngine:
def speak(self, text: str):
raise NotImplementedError
+95
View File
@@ -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}")
+1
View File
@@ -0,0 +1 @@
# jarvis package
+41
View File
@@ -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
+25
View File
@@ -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 []
+128
View File
@@ -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()
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env python3
import sys
from jarvis.cli import main
if __name__ == '__main__':
sys.argv[0] = 'jarvis'
main()
+30
View File
@@ -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"
-9
View File
@@ -1,9 +0,0 @@
mlx-whisper
sounddevice
torch
silero-vad
transformers
numpy
requests
pygame
gTTS
+16
View File
@@ -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
+10
View File
@@ -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()
+13
View File
@@ -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()
+16
View File
@@ -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"
+4
View File
@@ -0,0 +1,4 @@
from jarvis.cli import main
import sys
sys.argv = ["jarvis","test-mic"]
main()
+50
View File
@@ -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()
+29
View File
@@ -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()
-25
View File
@@ -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 Bettanys 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."*
+24
View File
@@ -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
-25
View File
@@ -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 Bettanys 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."*
-57
View File
@@ -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.*
-42
View File
@@ -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:
...
- Apples M5 Pro/Max announcement and the technical communitys 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
-543
View File
@@ -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.