G1_Lootah/Audio_Recorder/g1_voice_deploy.py

480 lines
17 KiB
Python

#!/usr/bin/env python3
"""
G1 Voice Deploy — Record on workstation, deploy & play on G1 robot.
Also sets up mic on robot and manages remote recordings.
Usage (run from workstation):
python3 g1_voice_deploy.py setup-mic # Unmute & configure wireless mic on robot
python3 g1_voice_deploy.py record --name hi # Record from workstation mic, save locally
python3 g1_voice_deploy.py record-robot --name arabic_greeting --seconds 5 # Record from robot mic
python3 g1_voice_deploy.py play --name hi # Deploy & play on G1 speaker
python3 g1_voice_deploy.py play-robot --path "/home/unitree/SanadVoice/recorded voices/welcome_single.wav"
python3 g1_voice_deploy.py tts --text "Hello" # TTS on G1 speaker
python3 g1_voice_deploy.py list # List local recordings
python3 g1_voice_deploy.py list-robot # List recordings on robot
"""
import argparse
import os
import subprocess
import sys
import time
import wave
import numpy as np
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
DATA_DIR = SCRIPT_DIR / "DataG1"
ROBOT_IP = "192.168.123.164"
ROBOT_USER = "unitree"
ROBOT_PYTHON = "/home/unitree/miniconda3/envs/gemini/bin/python3"
ROBOT_VOICES_DIR = "/home/unitree/SanadVoice/recorded voices"
ROBOT_DDS_IFACE = "eth0"
SAMPLE_RATE = 16000
CHANNELS = 1
SAMPLE_WIDTH = 2
CHUNK_SIZE = 4096
SSH_OPTS = ["-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5"]
def ssh_cmd(cmd: str, capture=True):
"""Run a command on the robot via SSH."""
target = f"{ROBOT_USER}@{ROBOT_IP}"
result = subprocess.run(
["ssh"] + SSH_OPTS + [target, cmd],
capture_output=capture, text=True,
)
if capture:
return result.returncode, result.stdout.strip(), result.stderr.strip()
return result.returncode, "", ""
def scp_to_robot(local_path: str, remote_path: str):
"""Copy a file to the robot."""
target = f"{ROBOT_USER}@{ROBOT_IP}"
ret = subprocess.run(
["scp"] + SSH_OPTS + [local_path, f"{target}:{remote_path}"],
capture_output=True, text=True,
)
return ret.returncode == 0
def scp_from_robot(remote_path: str, local_path: str):
"""Copy a file from the robot."""
target = f"{ROBOT_USER}@{ROBOT_IP}"
ret = subprocess.run(
["scp"] + SSH_OPTS + [f"{target}:{remote_path}", local_path],
capture_output=True, text=True,
)
return ret.returncode == 0
# ─── SETUP MIC ──────────────────────────────────────────────
def cmd_setup_mic(args):
"""Unmute and configure the wireless mic on the robot."""
print("Setting up wireless mic on G1...\n")
# List sources
code, out, _ = ssh_cmd("pactl list sources short")
print("PulseAudio sources:")
print(out)
print()
# Find wireless mic source
mic_source = None
for line in out.split("\n"):
if "Wireless" in line or "Hollyland" in line:
mic_source = line.split("\t")[1] if "\t" in line else None
mic_index = line.split("\t")[0] if "\t" in line else None
break
if not mic_source:
print("Wireless mic not found. Is it plugged in?")
return
print(f"Found mic: {mic_source} (index {mic_index})")
# Set as default, unmute, set volume
ssh_cmd(f"pactl set-default-source {mic_source}")
print(" Set as default source.")
ssh_cmd(f"pactl set-source-mute {mic_index} 0")
print(" Unmuted.")
ssh_cmd(f"pactl set-source-volume {mic_index} 100%")
print(" Volume set to 100%.")
# Check mute status
code, out, _ = ssh_cmd(f'pactl list sources | grep -A 3 "Wireless" | grep Mute')
print(f" Status: {out.strip()}")
# Verify recording
print("\n Verifying mic (2 second test)...")
ssh_cmd("timeout 2 parec -d 3 --format=s16le --rate=16000 --channels=1 --raw > /tmp/_mic_verify.pcm")
code, out, _ = ssh_cmd(
f'{ROBOT_PYTHON} -c "'
'import numpy as np;'
'a = np.fromfile(\'/tmp/_mic_verify.pcm\', dtype=np.int16);'
'print(f\'samples={len(a)} std={a.std():.0f}\')'
'"'
)
print(f" Result: {out}")
if "std=0" in out:
print(" WARNING: Mic is silent! Check transmitter is powered on.")
else:
print(" Mic is working!")
# ─── RECORD ON WORKSTATION ───────────────────────────────────
def cmd_record(args):
"""Record from workstation microphone."""
import pyaudio
DATA_DIR.mkdir(parents=True, exist_ok=True)
out_path = DATA_DIR / f"{args.name}.wav"
pa = pyaudio.PyAudio()
# Find best input device
device_index = args.device
if device_index is None:
for i in range(pa.get_device_count()):
info = pa.get_device_info_by_index(i)
if info["maxInputChannels"] > 0:
name = info["name"]
if name in ("default", "pipewire") or "pulse" in name.lower():
device_index = i
break
if device_index is None:
for i in range(pa.get_device_count()):
if pa.get_device_info_by_index(i)["maxInputChannels"] > 0:
device_index = i
break
# Find supported rate
info = pa.get_device_info_by_index(device_index)
rec_rate = int(info["defaultSampleRate"])
print(f"Using device [{device_index}] {info['name']} @ {rec_rate}Hz")
print(f"Recording {args.seconds}s... speak now!\n")
stream = pa.open(
format=pyaudio.paInt16, channels=1, rate=rec_rate,
input=True, input_device_index=device_index, frames_per_buffer=CHUNK_SIZE,
)
frames = []
total = int(rec_rate / CHUNK_SIZE * args.seconds)
start = time.time()
for i in range(total):
frames.append(stream.read(CHUNK_SIZE, exception_on_overflow=False))
elapsed = time.time() - start
pct = min(elapsed / args.seconds, 1.0)
bar = "" * int(30 * pct) + "" * (30 - int(30 * pct))
print(f"\r [{bar}] {elapsed:.1f}s / {args.seconds:.1f}s", end="", flush=True)
stream.stop_stream()
stream.close()
pa.terminate()
print()
# Resample to 16kHz
audio = np.frombuffer(b"".join(frames), dtype=np.int16)
if rec_rate != SAMPLE_RATE:
tl = int(len(audio) * SAMPLE_RATE / rec_rate)
audio = np.interp(
np.linspace(0, len(audio), tl, endpoint=False),
np.arange(len(audio)), audio.astype(np.float64),
).astype(np.int16)
wf = wave.open(str(out_path), "wb")
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(SAMPLE_RATE)
wf.writeframes(audio.tobytes())
wf.close()
print(f"Saved: {out_path} ({len(audio)/SAMPLE_RATE:.1f}s, {out_path.stat().st_size/1024:.1f}KB)")
# ─── RECORD ON ROBOT ─────────────────────────────────────────
def cmd_record_robot(args):
"""Record from the wireless mic on the robot."""
DATA_DIR.mkdir(parents=True, exist_ok=True)
seconds = args.seconds
remote_wav = f"/tmp/_rec_{args.name}.wav"
local_path = DATA_DIR / f"{args.name}.wav"
record_code = (
f"import time, subprocess, wave, numpy as np;"
f"print('Recording {seconds}s... speak now!');"
f"proc = subprocess.Popen(['parec','-d','3','--format=s16le','--rate=16000','--channels=1','--raw'], stdout=subprocess.PIPE);"
f"time.sleep({seconds});"
f"proc.terminate();"
f"raw = proc.stdout.read();"
f"audio = np.frombuffer(raw, dtype=np.int16);"
f"print(f'Recorded {{len(audio)}} samples, std={{audio.std():.0f}}');"
f"wf = wave.open('{remote_wav}', 'wb');"
f"wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(16000);"
f"wf.writeframes(audio.tobytes()); wf.close();"
f"print('Saved on robot')"
)
print(f"Recording {seconds}s from robot wireless mic...")
code, out, err = ssh_cmd(f'{ROBOT_PYTHON} -c "{record_code}"')
print(f" {out}")
if err:
print(f" stderr: {err}")
# Download to workstation
print(f"Downloading to {local_path}...")
if scp_from_robot(remote_wav, str(local_path)):
print(f" Saved: {local_path}")
ssh_cmd(f"rm -f {remote_wav}")
else:
print(" Download failed!")
# Also save on robot in recorded voices dir
remote_final = f"{ROBOT_VOICES_DIR}/{args.name}.wav"
ssh_cmd(f'cp "{remote_wav}" "{remote_final}" 2>/dev/null')
# ─── PLAY ON G1 ──────────────────────────────────────────────
def cmd_play(args):
"""Deploy a local WAV file to G1 and play on built-in speaker."""
wav_path = DATA_DIR / f"{args.name}.wav"
if not wav_path.exists():
print(f"Not found: {wav_path}")
cmd_list(args)
return
with wave.open(str(wav_path), "rb") as wf:
dur = wf.getnframes() / wf.getframerate()
print(f"Loaded: {wav_path.name} ({dur:.1f}s)")
# SCP to robot
remote_wav = f"/tmp/_play_{args.name}.wav"
print(f"Copying to robot...")
if not scp_to_robot(str(wav_path), remote_wav):
print(" SCP failed!")
return
print(" Copied.")
# Play on robot
_play_remote_wav(remote_wav, dur)
# Cleanup
ssh_cmd(f"rm -f {remote_wav}")
def cmd_play_robot(args):
"""Play a WAV file that already exists on the robot."""
# Get duration
code, out, _ = ssh_cmd(
f'{ROBOT_PYTHON} -c "import wave; '
f"wf = wave.open('{args.path}', 'rb'); "
f"print(wf.getnframes() / wf.getframerate()); wf.close()"
'"'
)
try:
dur = float(out)
except ValueError:
print(f"Cannot read: {args.path}")
print(f" {out}")
return
print(f"Playing: {args.path} ({dur:.1f}s)")
_play_remote_wav(args.path, dur)
def _play_remote_wav(remote_wav: str, duration: float):
"""Play a WAV file on the G1 speaker (file must already be on robot)."""
play_code = (
"import time, wave, json, numpy as np;"
"from unitree_sdk2py.core.channel import ChannelFactoryInitialize;"
"from unitree_sdk2py.g1.audio.g1_audio_client import AudioClient;"
"from unitree_sdk2py.g1.audio.g1_audio_api import *;"
f"ChannelFactoryInitialize(0, '{ROBOT_DDS_IFACE}');"
"c = AudioClient(); c.SetTimeout(10.0); c.Init(); c.SetVolume(100);"
"c._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, json.dumps({'app_name':'p'}));"
"time.sleep(0.3);"
f"wf = wave.open('{remote_wav}', 'rb');"
"rate = wf.getframerate(); nch = wf.getnchannels();"
"audio = np.frombuffer(wf.readframes(wf.getnframes()), dtype=np.int16); wf.close();"
"audio = audio.reshape(-1, 2).mean(axis=1).astype(np.int16) if nch == 2 else audio;"
"tl = int(len(audio) * 16000 / rate) if rate != 16000 else len(audio);"
"audio = np.interp(np.linspace(0, len(audio), tl, endpoint=False), np.arange(len(audio)), audio.astype(np.float64)).astype(np.int16) if rate != 16000 else audio;"
"pcm = audio.tobytes();"
"sid = f's_{int(time.time()*1000)}';"
"param = json.dumps({'app_name':'p','stream_id':sid,'sample_rate':16000,'channels':1,'bits_per_sample':16});"
"c._CallRequestWithParamAndBin(ROBOT_API_ID_AUDIO_START_PLAY, param, list(pcm));"
f"time.sleep({duration + 1});"
"c._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, json.dumps({'app_name':'p'}));"
"print('played')"
)
print(f"Playing on G1... ({duration:.1f}s)")
proc = subprocess.Popen(
["ssh"] + SSH_OPTS + [f"{ROBOT_USER}@{ROBOT_IP}",
f'{ROBOT_PYTHON} -c "{play_code}"'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
start = time.time()
while proc.poll() is None:
elapsed = time.time() - start
pct = min(elapsed / duration, 1.0)
bar = "" * int(30 * pct) + "" * (30 - int(30 * pct))
print(f"\r [{bar}] {min(elapsed, duration):.1f}s / {duration:.1f}s", end="", flush=True)
time.sleep(0.2)
out = proc.stdout.read().decode().strip()
if "played" in out:
print(f"\n Done.")
else:
print(f"\n Robot output: {out}")
# ─── TTS ──────────────────────────────────────────────────────
def cmd_tts(args):
"""Play text-to-speech on G1 speaker (English/Chinese only)."""
tts_code = (
"import json;"
"from unitree_sdk2py.core.channel import ChannelFactoryInitialize;"
"from unitree_sdk2py.g1.audio.g1_audio_client import AudioClient;"
f"ChannelFactoryInitialize(0, '{ROBOT_DDS_IFACE}');"
"c = AudioClient(); c.SetTimeout(10.0); c.Init();"
f"c.SetVolume({args.volume});"
f"c.TtsMaker('{args.text}', {args.speaker_id});"
"print('ok')"
)
print(f"TTS: \"{args.text}\" (speaker_id={args.speaker_id}, vol={args.volume})")
code, out, _ = ssh_cmd(f'{ROBOT_PYTHON} -c "{tts_code}"')
if "ok" in out:
print(" Sent.")
else:
print(f" Result: {out}")
# ─── LIST ─────────────────────────────────────────────────────
def cmd_list(args):
"""List local recordings."""
DATA_DIR.mkdir(parents=True, exist_ok=True)
wavs = sorted(DATA_DIR.glob("*.wav"))
if not wavs:
print(f"No recordings in {DATA_DIR}")
return
print(f"\n{'#':<4} {'Name':<35} {'Duration':>8} {'Size':>10}")
print("-" * 60)
for i, p in enumerate(wavs):
try:
with wave.open(str(p), "rb") as wf:
dur = wf.getnframes() / wf.getframerate()
size = p.stat().st_size / 1024
print(f"{i:<4} {p.stem:<35} {dur:>6.1f}s {size:>8.1f}KB")
except Exception:
print(f"{i:<4} {p.stem:<35} {'ERROR':>8}")
print()
def cmd_list_robot(args):
"""List recordings on the robot."""
code, out, _ = ssh_cmd(
f'{ROBOT_PYTHON} -c "'
"import wave, os; from pathlib import Path;"
f"d = Path('{ROBOT_VOICES_DIR}');"
"wavs = sorted(d.glob('*.wav'));"
"print(f'{{len(wavs)}} files in {d}');"
"for p in wavs:"
" try:"
" wf = wave.open(str(p),'rb'); dur = wf.getnframes()/wf.getframerate(); wf.close();"
" print(f'{p.stem:<45} {dur:>6.1f}s')"
" except: print(f'{p.stem:<45} ERROR')"
'"'
)
print(out)
# ─── MAIN ─────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="G1 Voice Deploy — Record, deploy & play audio on G1 robot",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s setup-mic
%(prog)s record --name greeting --seconds 5
%(prog)s record-robot --name arabic_hi --seconds 5
%(prog)s play --name greeting
%(prog)s play-robot --path "/home/unitree/SanadVoice/recorded voices/welcome_single.wav"
%(prog)s tts --text "Hello, welcome!"
%(prog)s list
%(prog)s list-robot
""",
)
sub = parser.add_subparsers(dest="command")
# setup-mic
sub.add_parser("setup-mic", help="Unmute & configure wireless mic on robot")
# record
p = sub.add_parser("record", help="Record from workstation mic")
p.add_argument("--name", "-n", required=True, help="Recording name")
p.add_argument("--seconds", "-s", type=float, default=5.0)
p.add_argument("--device", "-d", type=int, default=None)
# record-robot
p = sub.add_parser("record-robot", help="Record from robot wireless mic")
p.add_argument("--name", "-n", required=True, help="Recording name")
p.add_argument("--seconds", "-s", type=float, default=5.0)
# play
p = sub.add_parser("play", help="Deploy & play local WAV on G1 speaker")
p.add_argument("--name", "-n", required=True, help="Recording name")
# play-robot
p = sub.add_parser("play-robot", help="Play a WAV file already on robot")
p.add_argument("--path", "-p", required=True, help="Full path on robot")
# tts
p = sub.add_parser("tts", help="Text-to-speech on G1 (English/Chinese)")
p.add_argument("--text", "-t", required=True, help="Text to speak")
p.add_argument("--speaker-id", type=int, default=0, help="Voice ID (0,1,2)")
p.add_argument("--volume", "-v", type=int, default=100)
# list
sub.add_parser("list", help="List local recordings")
sub.add_parser("list-robot", help="List recordings on robot")
args = parser.parse_args()
commands = {
"setup-mic": cmd_setup_mic,
"record": cmd_record,
"record-robot": cmd_record_robot,
"play": cmd_play,
"play-robot": cmd_play_robot,
"tts": cmd_tts,
"list": cmd_list,
"list-robot": cmd_list_robot,
}
if args.command in commands:
commands[args.command](args)
else:
parser.print_help()
if __name__ == "__main__":
main()