Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions examples/linux-voice/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Linux PipeWire `/voice` Shim & Local STT/TTS Fallback

On modern Linux distributions using **PipeWire 1.0+** (Ubuntu 24.04+, Fedora 40+, Debian 12/13, Arch Linux, NVIDIA DGX OS), the Antigravity CLI `/voice` command invokes `pw-record` with the `--raw` flag. Because PipeWire's `pw-record` binary does not accept `--raw` (unlike PulseAudio's `parec`), `/voice` exits immediately or captures silent/low-gain audio on USB microphones.

This directory provides a zero-dependency Python 3 standard-library shim (`pw-record`) and installer (`install.sh`) that resolves Linux `/voice` capture end-to-end:

1. **Flag Normalization**: Transparently strips the unsupported `--raw` flag before invoking `/usr/bin/pw-record` when streaming raw PCM (`-`) to `agy`.
2. **Automatic Physical Microphone Selection**: Queries `pactl list short sources` to select and unmute active hardware USB/analog microphones while ignoring `.monitor` loopback sinks.
3. **Real-Time DC-Offset Removal & Automatic Gain Control (AGC)**: Uses Python's built-in `array` module to center 16kHz `s16le` PCM frames and normalize quiet microphone levels in real time (`8ms` startup overhead), preventing false `No speech detected` errors.
4. **Optional Offline Faster-Whisper Fallback**: Tees the captured PCM stream to `/tmp/agy_voice_last.raw` and optionally forwards it to a local Faster-Whisper UNIX domain socket (`/run/user/$UID/voice_dictate.sock`) if cloud transcription is unavailable.

## Installation

```bash
bash examples/linux-voice/install.sh
```

Ensure `~/.local/bin` precedes `/usr/bin` in your `PATH`:

```bash
export PATH="$HOME/.local/bin:$PATH"
```

### Optional Local STT/TTS Model Bootstrap (`Faster-Whisper` + `Piper` + `Kokoro`)

To keep the Git repository lightweight and free of binary model weights, `install.sh` includes an optional `--with-models` flag that creates an isolated Python virtual environment (`~/.local/share/antigravity-voice/venv`), installs [`requirements.txt`](requirements.txt) (`faster-whisper`, `piper-tts`, `kokoro-onnx`), and downloads the Piper ONNX voice model on demand:

```bash
bash examples/linux-voice/install.sh --with-models
ai-say "Antigravity local voice synthesis is active."
```
24 changes: 24 additions & 0 deletions examples/linux-voice/ai-say
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Portable CLI Text-to-Speech helper for Antigravity CLI (Piper TTS default, or --kokoro)
set -euo pipefail

VOICE_ENV="${AGY_VOICE_ENV:-$HOME/.local/share/antigravity-voice/venv}"
MODEL_DIR="${AGY_VOICE_MODELS:-$HOME/.local/share/antigravity-voice/models}"

ENGINE="piper"
if [ "${1:-}" = "--kokoro" ] || [ "${1:-}" = "-k" ]; then
ENGINE="kokoro"
shift
fi

TEXT="$*"
if [ -z "$TEXT" ]; then
exit 0
fi

if [ "$ENGINE" = "piper" ] && [ -x "$VOICE_ENV/bin/piper" ] && [ -f "$MODEL_DIR/en_GB-alan-medium.onnx" ]; then
printf "%s\n" "$TEXT" | "$VOICE_ENV/bin/piper" --model "$MODEL_DIR/en_GB-alan-medium.onnx" --output-raw 2>/dev/null | \
pw-play --rate 22050 --channels 1 --format s16 - 2>/dev/null || true
elif command -v spd-say >/dev/null 2>&1; then
spd-say "$TEXT"
fi
30 changes: 30 additions & 0 deletions examples/linux-voice/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
mkdir -p "$HOME/.local/bin" "$HOME/.gemini/antigravity-cli/bin"

install -m 0755 "$SCRIPT_DIR/pw-record" "$HOME/.local/bin/pw-record"
install -m 0755 "$SCRIPT_DIR/pw-record" "$HOME/.gemini/antigravity-cli/bin/pw-record"
install -m 0755 "$SCRIPT_DIR/ai-say" "$HOME/.local/bin/ai-say"

echo "Installed PipeWire /voice shim and ai-say wrapper to ~/.local/bin"

if [ "${1:-}" = "--with-models" ]; then
VOICE_DIR="${AGY_VOICE_DIR:-$HOME/.local/share/antigravity-voice}"
mkdir -p "$VOICE_DIR/models"
echo "Bootstrapping isolated Python venv in $VOICE_DIR/venv..."
python3 -m venv "$VOICE_DIR/venv"
"$VOICE_DIR/venv/bin/pip" install --upgrade pip
"$VOICE_DIR/venv/bin/pip" install -r "$SCRIPT_DIR/requirements.txt"

PIPER_URL_BASE="https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_GB/alan/medium"
if [ ! -f "$VOICE_DIR/models/en_GB-alan-medium.onnx" ]; then
echo "Downloading Piper TTS voice model (en_GB-alan-medium.onnx)..."
curl -fL "$PIPER_URL_BASE/en_GB-alan-medium.onnx" -o "$VOICE_DIR/models/en_GB-alan-medium.onnx"
curl -fL "$PIPER_URL_BASE/en_GB-alan-medium.onnx.json" -o "$VOICE_DIR/models/en_GB-alan-medium.onnx.json"
fi
echo "Local STT/TTS environment ready in $VOICE_DIR"
else
echo "Tip: Run 'bash examples/linux-voice/install.sh --with-models' to auto-install Faster-Whisper, Piper TTS, and Kokoro into an isolated venv."
fi
195 changes: 195 additions & 0 deletions examples/linux-voice/pw-record
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""
PipeWire Recording Shim for Antigravity CLI (`/voice`) on Linux.
Uses pure Python standard library (`array`) for zero-dependency startup:
1. Strips unsupported `--raw` flag passed by Antigravity CLI (`agy`) on PipeWire systems.
2. Automatically selects and unmutes the primary non-monitor physical microphone input.
3. Performs real-time DC-offset removal and Automatic Gain Control (AGC) when streaming
raw 16kHz s16le PCM to `agy` stdout, preventing `No speech detected` on quiet USB mics.
4. Optionally forwards `/tmp/agy_voice_last.raw` to a local Faster-Whisper socket
(`/run/user/$UID/voice_dictate.sock`) if cloud `audiotranscribe` fails.
"""

import array
import math
import os
import signal
import socket
import subprocess
import sys
import time

REAL_PW_RECORD = os.environ.get("AGY_REAL_PW_RECORD", "/usr/bin/pw-record")
RAW_CAPTURE_PATH = os.environ.get("AGY_RAW_CAPTURE_PATH", f"/tmp/agy_voice_{os.getuid()}.raw")
SOCKET_PATH = os.environ.get("AGY_WHISPER_SOCKET", f"/run/user/{os.getuid()}/voice_dictate.sock")
CLI_LOG_PATH = os.path.expanduser("~/.gemini/antigravity-cli/cli.log")


def auto_select_mic() -> None:
"""Select and unmute the first physical non-monitor input source via pactl."""
try:
out = subprocess.check_output(
["pactl", "list", "short", "sources"],
text=True,
stderr=subprocess.DEVNULL,
)
sources = [
line.split("\t")[1]
for line in out.strip().splitlines()
if len(line.split("\t")) >= 2 and ".monitor" not in line.split("\t")[1]
]
if not sources:
return
best = sources[0]
for preferred in ["usb-", "input", "mic", "yeti", "audio"]:
matches = [s for s in sources if preferred in s.lower()]
if matches:
best = matches[0]
break
subprocess.run(["pactl", "set-default-source", best], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(["pactl", "set-source-mute", best, "0"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
pass


def trigger_local_whisper_fallback() -> None:
"""Optionally notify a local Faster-Whisper UNIX socket if cloud transcription fails."""
try:
if not os.path.exists(RAW_CAPTURE_PATH) or os.path.getsize(RAW_CAPTURE_PATH) < 11200:
return
if not os.path.exists(SOCKET_PATH):
return
if os.fork() != 0:
return

os.setsid()
time.sleep(0.45)

cloud_failed = os.environ.get("AGY_FORCE_LOCAL_WHISPER", "0") == "1"
try:
if os.path.exists(CLI_LOG_PATH):
with open(CLI_LOG_PATH, "rb") as lf:
lf.seek(max(0, os.path.getsize(CLI_LOG_PATH) - 4096))
tail = lf.read().decode("utf-8", errors="ignore")
if "audiotranscribe:" in tail or "finalizing the transcript failed" in tail:
cloud_failed = True
except Exception:
pass

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(5.0)
s.connect(SOCKET_PATH)
cmd = f"TRANSCRIBE_AND_TYPE:{RAW_CAPTURE_PATH}\n" if cloud_failed else f"TRANSCRIBE_RAW:{RAW_CAPTURE_PATH}\n"
s.sendall(cmd.encode("utf-8"))
s.close()
except Exception:
pass
finally:
if "s" in locals():
os._exit(0)


def main() -> None:
raw_mode = "--raw" in sys.argv
filtered_args = [a for a in sys.argv[1:] if a != "--raw"]

auto_select_mic()

if not raw_mode or "-" not in filtered_args:
os.execv(REAL_PW_RECORD, [REAL_PW_RECORD] + filtered_args)

proc = subprocess.Popen(
[REAL_PW_RECORD] + filtered_args,
stdout=subprocess.PIPE,
stderr=sys.stderr,
)

raw_file = None
try:
raw_file = open(RAW_CAPTURE_PATH, "wb")
except Exception:
pass

def handle_sig(signum, frame):
try:
proc.terminate()
except Exception:
pass

signal.signal(signal.SIGTERM, handle_sig)
signal.signal(signal.SIGINT, handle_sig)

running_gain = 4.0
chunk_size = 1280 # 640 samples = 40ms at 16kHz s16le

try:
while True:
data = proc.stdout.read(chunk_size) if proc.stdout else b""
if not data:
break

if len(data) % 2 == 0 and len(data) > 0:
samples = array.array("h")
samples.frombytes(data)
n = len(samples)
mean_val = int(sum(samples) / n)
peak = 0
sum_sq = 0.0
centered = [0] * n
for i, v in enumerate(samples):
c = v - mean_val
centered[i] = c
ac = abs(c)
if ac > peak:
peak = ac
sum_sq += c * c
rms = math.sqrt(sum_sq / n)

if rms > 15.0:
target_gain = min(22000.0 / max(peak, 1), 18.0)
running_gain = 0.8 * running_gain + 0.2 * target_gain
g = max(running_gain, 1.5)
out_arr = array.array(
"h",
(max(-32767, min(32767, int(c * g))) for c in centered),
)
else:
out_arr = array.array(
"h",
(max(-32767, min(32767, c)) for c in centered),
)
out_bytes = out_arr.tobytes()
else:
out_bytes = data

if raw_file is not None:
try:
raw_file.write(out_bytes)
raw_file.flush()
except Exception:
pass

try:
sys.stdout.buffer.write(out_bytes)
sys.stdout.buffer.flush()
except BrokenPipeError:
break
finally:
if raw_file is not None:
try:
raw_file.close()
except Exception:
pass
try:
proc.terminate()
proc.wait(timeout=0.3)
except Exception:
try:
proc.kill()
except Exception:
pass
trigger_local_whisper_fallback()


if __name__ == "__main__":
main()
5 changes: 5 additions & 0 deletions examples/linux-voice/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
faster-whisper>=1.1.0
piper-tts>=1.2.0
kokoro-onnx>=0.4.0
soundfile>=0.12.1
numpy>=1.26.0
7 changes: 7 additions & 0 deletions examples/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Community Skills Examples for Antigravity CLI

This directory contains portable, self-contained agent skills that can be installed into `~/.agents/skills/<skill-name>/`:

1. **[`local-llm-web-search`](local-llm-web-search/SKILL.md)**: Zero-API-key live web search, parallel page extraction, and local LLM synthesis via Ollama (`127.0.0.1:11434`) or any OpenAI-compatible local server.
2. **[`linux-usb-xhci-diagnostics`](linux-usb-xhci-diagnostics/SKILL.md)**: Kernel-register-first Linux USB-C, xHCI link lane (`portli`), USB 2.0 Single-TT vs. Multi-TT hub collision, and DisplayPort diagnostic workflow.
3. **[`headless-vector-pdf`](headless-vector-pdf/SKILL.md)**: Publication-grade Markdown-to-Print-CSS vector PDF and Graphviz SVG-to-PNG compilation using headless Chromium/Chrome without root privileges.
31 changes: 31 additions & 0 deletions examples/skills/headless-vector-pdf/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: headless-vector-pdf
description: Compile Markdown documents and Graphviz DOT diagrams into publication-grade vector PDFs and high-resolution PNGs using headless Chrome or Chromium without root packages.
---

# Headless Vector PDF & Diagram Compiler

Use this skill when generating printable technical reports, manuals, or architectural diagrams in environments without LaTeX or root package installation privileges.

## 1. Markdown / HTML to Vector PDF

Wrap the rendered HTML body in print-media CSS rules and invoke headless Chrome/Chromium:

```css
@page { size: letter; margin: 18mm 16mm 20mm 16mm; }
body { font-family: Georgia, serif; line-height: 1.65; color: #111; }
pre, code { font-family: monospace; white-space: pre-wrap; word-wrap: break-word; }
```

```bash
google-chrome --headless=new --disable-gpu --no-sandbox --print-to-pdf=output.pdf document.html
```

## 2. Graphviz DOT to High-Resolution PNG Fallback

If `dot -Tpng` fails due to missing Cairo/Pango plugins in user-space, output standard SVG first and rasterize via headless Chrome:

```bash
dot -Tsvg architecture.dot -o architecture.svg
google-chrome --headless=new --disable-gpu --no-sandbox --screenshot=architecture.png --window-size=1600,1000 architecture.svg
```
34 changes: 34 additions & 0 deletions examples/skills/linux-usb-xhci-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: linux-usb-xhci-diagnostics
description: Diagnose Linux USB-C ports, xHCI link error counts, Single-TT vs Multi-TT USB 2.0 hub collisions, and GPU DisplayPort clock states via sysfs and debugfs registers.
---

# Linux USB-C, xHCI & Display Register Diagnostics

Use this skill whenever diagnosing intermittent USB device disconnects (`error -71` / `-EPROTO`), USB hub resets (`clear tt 1`), or external display blanking on Linux workstations. Always inspect kernel hardware registers before speculating about power supplies or physical cables.

## 4-Stage Register Inspection Protocol

1. **Over-Current & Port Power Control (`sysfs`)**:
Check whether any physical root port has tripped hardware over-current protection or aggressive autosuspend:
```bash
grep -H . /sys/bus/usb/devices/*-port*/over_current_count
grep -H . /sys/bus/usb/devices/*-port*/power/control
grep -H . /sys/bus/usb/devices/*-port*/usb3_lpm_permit 2>/dev/null
cat /sys/module/usbcore/parameters/autosuspend
```

2. **xHCI Link Error Count & SuperSpeed Lane Negotiation (`debugfs`)**:
Inspect `/sys/kernel/debug/usb/xhci/*/ports/*/portli` to check `LEC` (Link Error Count) and `RLC`/`TLC` (active RX/TX lanes):
```bash
sudo grep -H . /sys/kernel/debug/usb/xhci/*/ports/*/portli
```

3. **USB 2.0 Transaction Translator (Single-TT vs. Multi-TT) Audit**:
Inspect `/sys/kernel/debug/usb/devices` for `Cls=09(hub)`:
- `Prot=01` indicates a **Single-TT** USB 2.0 hub (all ports share a single 12 Mbps USB 1.1 Transaction Translator).
- `Prot=02` indicates a **Multi-TT** USB 2.0 hub (dedicated Transaction Translator per port).
- Combining a 12 Mbps Full-Speed isochronous audio device (`#Iso>0`) and 1.5 Mbps Low-Speed HID keyboard/mouse on a `Prot=01` Single-TT hub overflows the periodic frame schedule (`clear tt 1 error -71`). Separate them onto different root ports or a `Prot=02` Multi-TT hub.

4. **Portable Dual-USB-C Monitor Back-Feed Safety**:
Never advise plugging an external USB-C PD wall brick into the secondary unlabelled USB-C port of a generic portable monitor while connected to a host computer unless reverse-current diode isolation is verified, as shared `VBUS` rails can back-feed voltage into the host Type-C controller.
23 changes: 23 additions & 0 deletions examples/skills/local-llm-web-search/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
name: local-llm-web-search
description: Augment local Ollama or OpenAI-compatible models (127.0.0.1:11434) with live web search and parallel article extraction without external search API keys.
---

# Local LLM Web Search Skill (`ollama-web`)

Use this skill when the user wants to run a local model via Ollama (`127.0.0.1:11434`) grounded with live, up-to-date web search results and full-text page extraction.

## Workflow

1. **Discover Local Models Dynamically**:
Query `http://127.0.0.1:11434/api/tags` to list installed Ollama models rather than hardcoding model tags.
2. **Execute Grounded Query**:
Run `ollama_web_cli.py` with the target query and optional `--model`:
```bash
python3 ~/.agents/skills/local-llm-web-search/ollama_web_cli.py "Latest Linux kernel 6.14 ARM64 features"
```
3. **Interactive Mode**:
Launch an interactive grounded REPL session:
```bash
python3 ~/.agents/skills/local-llm-web-search/ollama_web_cli.py -i
```
Loading
Loading