Skip to content
Merged
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
8 changes: 8 additions & 0 deletions examples/remote_liaison_demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
logs/
rbnx-boot/
rbnx-build/
__pycache__/
*.pyc
.venv/
skills/*/logs/
skills/*/rbnx-build/
119 changes: 119 additions & 0 deletions examples/remote_liaison_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Remote Liaison Demo

This example is a small reusable remote voice path for Liaison:

```text
macOS mic/speaker
-> SSH reverse tunnel
-> audio_macos_bridge
-> voiceprint/user access gate
-> speech ASR
-> Liaison + Pilot + Executor
-> demo skills
-> speech TTS
-> macOS speaker
```

Linux runs Atlas, Executor, Pilot, Liaison, voiceprint, speech, the Mac audio
bridge primitive, and three lightweight skills. macOS only owns the physical
microphone and speaker.

The three skills are intentionally small but real Robonix skills:

- `status_skill`: reports demo status.
- `notes_skill`: records a note into its package-local `rbnx-build/data`.
- `summary_skill`: summarizes the demo and recorded notes.

Do not commit real cloud keys or enrolled voiceprint data. Put credentials in
your shell or a local `.env` file outside git.

## Mac Audio

Mac terminal 1:

```bash
cd ~/robonix-scripts/mac_server
source .venv/bin/activate
python3 server_web.py --host 127.0.0.1 --port 60000
```

Mac terminal 2:

```bash
ssh -N -R 60101:127.0.0.1:60000 <linux-user>@<linux-host>
```

Keep both Mac terminals open. If SSH prints `remote port forwarding failed`,
pick another Linux port and update `robonix_manifest.yaml` plus
`scripts/check_mac_audio.py`.

## Linux Run

```bash
cd examples/remote_liaison_demo

./scripts/clean_demo_state.sh
python3 scripts/check_mac_audio.py

export VLM_BASE_URL="https://api.deepseek.com"
export VLM_API_KEY="<your-vlm-api-key>"
export VLM_MODEL="deepseek-v4-flash"

export TENCENT_ASR_APPID="<your-tencent-asr-appid>"
export TENCENTCLOUD_SECRET_ID="<your-tencent-secret-id>"
export TENCENTCLOUD_SECRET_KEY="<your-tencent-secret-key>"
export TENCENT_ASR_ENGINE="16k_zh_en"
export TENCENT_TTS_VOICE_TYPE="1001"
export TENCENT_TTS_REGION="ap-guangzhou"

export ROBONIX_LIAISON_ALLOWED_USERS="voice:<your-user-id>"
# Optional GPU pin, for example:
# export VOICEPRINT_DEVICE="cuda:0"

rbnx build
rbnx boot
```

In another Linux terminal:

```bash
cd examples/remote_liaison_demo
rbnx chat
```

Press `Ctrl+V` in `rbnx chat`, speak to the Mac microphone, and listen from the
Mac speaker.

Try:

```text
Check the current demo status.
Remember that this demo uses remote Liaison with Pilot calling three skills.
Summarize the current demo.
```

## Voiceprint Enrolment

Run the demo first so Atlas, `audio_macos_bridge`, and `voiceprint` are active.
Then register one allowed speaker:

```bash
cd ../..
python3 examples/webots/scripts/enroll_voiceprint.py \
--user-id <your-user-id> \
--user-name <your-display-name> \
--seconds 6
```

To replace an existing enrollment:

```bash
python3 examples/webots/scripts/delete_voiceprint.py --user-id <your-user-id>
python3 examples/webots/scripts/enroll_voiceprint.py \
--user-id <your-user-id> \
--user-name <your-display-name> \
--seconds 6
```

`ROBONIX_LIAISON_ALLOWED_USERS` should include the matching voice identity,
for example `voice:alice`.
63 changes: 63 additions & 0 deletions examples/remote_liaison_demo/robonix_manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
manifestVersion: 1
name: robonix-remote-liaison-demo

env:
LOG: "INFO"
ROBONIX_ATLAS: "127.0.0.1:50051"
VOICEPRINT_THRESHOLD: "0.35"
ROBONIX_LIAISON_ACCESS_ENABLED: "1"
ROBONIX_LIAISON_ALLOWED_USERS: "${ROBONIX_LIAISON_ALLOWED_USERS}"
ROBONIX_LIAISON_VOICE_THRESHOLD: "0.35"

system:
atlas:
listen: 127.0.0.1:50051
log: info
executor:
listen: 127.0.0.1:51161
log: info
pilot:
listen: 127.0.0.1:51071
log: debug
vlm:
upstream: ${VLM_BASE_URL}
api_key: ${VLM_API_KEY}
model: ${VLM_MODEL}
api_format: openai
liaison:
listen: 127.0.0.1:51081
log: info

primitive:
- name: audio_macos_bridge
path: ../webots/primitives/audio_macos_bridge
config:
host: 127.0.0.1
port: 60101

service:
- name: voiceprint
path: ../../services/voiceprint
config: {}

- name: speech
path: ../../services/speech
config:
speech_backend: tencent
tencent_asr_appid: ${TENCENT_ASR_APPID}
tencent_asr_engine: ${TENCENT_ASR_ENGINE}
tencent_tts_voice_type: ${TENCENT_TTS_VOICE_TYPE}
tencent_tts_region: ${TENCENT_TTS_REGION}

skill:
- name: status_skill
path: ./skills/status_skill
config: {}

- name: notes_skill
path: ./skills/notes_skill
config: {}

- name: summary_skill
path: ./skills/summary_skill
config: {}
41 changes: 41 additions & 0 deletions examples/remote_liaison_demo/scripts/check_mac_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: MulanPSL-2.0
import argparse
import asyncio
import json
import sys
import time

import websockets


async def _check(host: str, port: int, timeout: float) -> int:
url = f"ws://{host}:{port}/health"
started = time.time()
try:
async with websockets.connect(url, open_timeout=timeout) as ws:
msg = await asyncio.wait_for(ws.recv(), timeout=timeout)
except Exception as exc:
print(f"[mac-audio] FAIL {url}: {type(exc).__name__}: {exc}", file=sys.stderr)
return 1

elapsed = time.time() - started
try:
payload = json.loads(msg)
except Exception:
payload = msg
print(f"[mac-audio] OK {url} in {elapsed:.2f}s: {payload}")
return 0


def main() -> int:
parser = argparse.ArgumentParser(description="Check macOS audio bridge WebSocket health.")
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=60101)
parser.add_argument("--timeout", type=float, default=5.0)
args = parser.parse_args()
return asyncio.run(_check(args.host, args.port, args.timeout))


if __name__ == "__main__":
raise SystemExit(main())
106 changes: 106 additions & 0 deletions examples/remote_liaison_demo/scripts/clean_demo_state.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MulanPSL-2.0
set -euo pipefail

DEMO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
REPO_ROOT="$(cd "$DEMO_ROOT/../.." && pwd)"
STATE_FILE="${HOME}/.robonix/processes.json"

python3 - "$DEMO_ROOT" "$REPO_ROOT" <<'PY'
import os
import signal
import sys
import time

demo_root = sys.argv[1]
repo_root = sys.argv[2]
needles = [
f"rbnx boot",
f"robonix-remote-liaison-demo",
f"--manifest {demo_root}/robonix_manifest.yaml",
"python3 -m audio_macos_bridge.main",
"python -m audio_macos_bridge.main",
"python3 -m speech_service.service",
"python -m speech_service.service",
"python3 -m voiceprint_service.service",
"python -m voiceprint_service.service",
"uv run --active python -m voiceprint_service.service",
"python3 -m remote_demo_skill.service",
"python -m remote_demo_skill.service",
f"rbnx start -p {demo_root}",
f"rbnx start -p {repo_root}/examples/webots/primitives/audio_macos_bridge",
f"rbnx start -p {repo_root}/services/voiceprint",
f"rbnx start -p {repo_root}/services/speech",
]
system_ports = {
"robonix-atlas": "127.0.0.1:50051",
"robonix-executor": "127.0.0.1:51161",
"robonix-pilot": "127.0.0.1:51071",
"robonix-liaison": "127.0.0.1:51081",
}

def matches():
own = {os.getpid(), os.getppid()}
found = []
for name in os.listdir("/proc"):
if not name.isdigit():
continue
pid = int(name)
if pid in own:
continue
try:
raw = open(f"/proc/{pid}/cmdline", "rb").read()
except OSError:
continue
cmd = raw.replace(b"\0", b" ").decode("utf-8", "ignore").strip()
if cmd and any(needle in cmd for needle in needles):
found.append((pid, cmd))
continue
if cmd and any(name in cmd and port in cmd for name, port in system_ports.items()):
found.append((pid, cmd))
return found

for sig in (signal.SIGTERM, signal.SIGKILL):
targets = matches()
if not targets:
break
for pid, _cmd in targets:
try:
os.kill(pid, sig)
except ProcessLookupError:
pass
time.sleep(0.5)
PY

if [[ -f "$STATE_FILE" ]]; then
tmp="$(mktemp)"
python3 - "$STATE_FILE" >"$tmp" <<'PY'
import json
import sys
from pathlib import Path

state_path = Path(sys.argv[1])
remove = {
"com.robonix.example.audio_macos_bridge",
"com.robonix.example.voiceprint_service",
"com.robonix.example.speech_service",
"com.robonix.demo.status_skill",
"com.robonix.demo.notes_skill",
"com.robonix.demo.summary_skill",
}

try:
data = json.loads(state_path.read_text())
except Exception:
data = []

kept = [
item for item in data
if item.get("package_name") not in remove and item.get("std_name") not in remove
]
print(json.dumps(kept, ensure_ascii=False, indent=2))
PY
mv "$tmp" "$STATE_FILE"
fi

echo "[clean_demo_state] cleaned remote_liaison_demo stale package processes"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[contract]
id = "robonix/skill/notes_skill/driver"
version = "1"
kind = "skill"
idl = "lifecycle/srv/Driver.srv"

[mode]
type = "rpc"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[contract]
id = "robonix/skill/notes_skill/save"
version = "1"
kind = "skill"
idl = "memory/srv/Save.srv"

[mode]
type = "rpc"
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
manifestVersion: 1

package:
name: com.robonix.demo.notes_skill
version: 0.1.0
vendor: robonix
description: Remote Liaison demo note-recording skill.
license: MulanPSL-2.0

build: bash scripts/build.sh
start: bash scripts/start.sh

capabilities:
- name: robonix/skill/notes_skill/driver
path: capabilities/driver.v1.toml
- name: robonix/skill/notes_skill/save
path: capabilities/save.v1.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Remote Liaison demo notes skill."""
Loading
Loading