Features:
- Bilingual support (English/Mandarin Chinese)
- Hotword detection: 'Hey Osiris' / '你好 Osiris'
- Music playback control (MP3, WAV, OGG, FLAC)
- OpenClaw integration for AI responses
- Google AIY Voice Kit V1 compatible
- Text-to-speech in both languages
- Voice command recognition
- Raspberry Pi ready with installation script
AI Now Inc - Del Mar Demo Unit 🏭
238 lines
6.9 KiB
Python
Executable File
238 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
OpenClaw Client for Voice Assistant
|
|
Connects to OpenClaw gateway for AI responses and command processing.
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import logging
|
|
import time
|
|
import threading
|
|
from typing import Optional, Callable, Dict, Any
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import websocket
|
|
HAS_WEBSOCKET = True
|
|
except ImportError:
|
|
HAS_WEBSOCKET = False
|
|
|
|
try:
|
|
import requests
|
|
HAS_REQUESTS = True
|
|
except ImportError:
|
|
HAS_REQUESTS = False
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class OpenClawClient:
|
|
"""
|
|
Client for OpenClaw gateway communication.
|
|
Supports WebSocket and HTTP APIs.
|
|
"""
|
|
|
|
def __init__(self, config_path: str = "config.json"):
|
|
self.config = self._load_config(config_path)
|
|
self.ws_url = self.config.get("openclaw", {}).get(
|
|
"ws_url", "ws://192.168.1.100:18790"
|
|
)
|
|
self.api_key = self.config.get("openclaw", {}).get("api_key", "")
|
|
self.enabled = self.config.get("openclaw", {}).get("enabled", True)
|
|
|
|
self.ws: Optional[websocket.WebSocketApp] = None
|
|
self.is_connected = False
|
|
self.message_handlers = []
|
|
self.reconnect_interval = self.config.get("openclaw", {}).get(
|
|
"reconnect_interval", 5
|
|
)
|
|
|
|
if HAS_WEBSOCKET and self.enabled:
|
|
self._init_websocket()
|
|
|
|
logger.info(f"OpenClawClient initialized (enabled={self.enabled})")
|
|
|
|
def _load_config(self, config_path: str) -> dict:
|
|
"""Load configuration from JSON file."""
|
|
try:
|
|
with open(config_path, 'r') as f:
|
|
return json.load(f)
|
|
except FileNotFoundError:
|
|
return {"openclaw": {"enabled": False}}
|
|
|
|
def _init_websocket(self):
|
|
"""Initialize WebSocket connection."""
|
|
if not HAS_WEBSOCKET:
|
|
logger.warning("websocket-client not installed")
|
|
return
|
|
|
|
def on_open(ws):
|
|
logger.info("WebSocket connected")
|
|
self.is_connected = True
|
|
self._on_connect()
|
|
|
|
def on_message(ws, message):
|
|
logger.debug(f"Received: {message}")
|
|
self._handle_message(message)
|
|
|
|
def on_error(ws, error):
|
|
logger.error(f"WebSocket error: {error}")
|
|
|
|
def on_close(ws, close_status_code, close_msg):
|
|
logger.info(f"WebSocket closed: {close_status_code} - {close_msg}")
|
|
self.is_connected = False
|
|
self._reconnect()
|
|
|
|
self.ws = websocket.WebSocketApp(
|
|
self.ws_url,
|
|
on_open=on_open,
|
|
on_message=on_message,
|
|
on_error=on_error,
|
|
on_close=on_close
|
|
)
|
|
|
|
# Start connection thread
|
|
thread = threading.Thread(target=self.ws.run_forever)
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
def _on_connect(self):
|
|
"""Called when connection is established."""
|
|
# Subscribe to relevant channels or send authentication
|
|
if self.api_key:
|
|
auth_message = {
|
|
"type": "auth",
|
|
"api_key": self.api_key
|
|
}
|
|
self.send(json.dumps(auth_message))
|
|
|
|
def _reconnect(self):
|
|
"""Attempt to reconnect after disconnection."""
|
|
logger.info(f"Reconnecting in {self.reconnect_interval}s...")
|
|
time.sleep(self.reconnect_interval)
|
|
if self.ws:
|
|
self._init_websocket()
|
|
|
|
def _handle_message(self, message: str):
|
|
"""Handle incoming message."""
|
|
try:
|
|
data = json.loads(message)
|
|
for handler in self.message_handlers:
|
|
handler(data)
|
|
except json.JSONDecodeError:
|
|
logger.warning(f"Invalid JSON: {message}")
|
|
|
|
def send(self, message: str) -> bool:
|
|
"""
|
|
Send message via WebSocket.
|
|
|
|
Args:
|
|
message: JSON string to send
|
|
|
|
Returns:
|
|
True if sent successfully
|
|
"""
|
|
if not self.is_connected or not self.ws:
|
|
logger.warning("Not connected to OpenClaw")
|
|
return False
|
|
|
|
try:
|
|
self.ws.send(message)
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Send error: {e}")
|
|
return False
|
|
|
|
def send_request(self, query: str, context: Optional[Dict] = None) -> Dict:
|
|
"""
|
|
Send a query to OpenClaw and get response.
|
|
|
|
Args:
|
|
query: User query string
|
|
context: Optional context dictionary
|
|
|
|
Returns:
|
|
Response dictionary
|
|
"""
|
|
if not self.enabled:
|
|
return {"error": "OpenClaw client disabled"}
|
|
|
|
message = {
|
|
"type": "query",
|
|
"query": query,
|
|
"timestamp": time.time()
|
|
}
|
|
|
|
if context:
|
|
message["context"] = context
|
|
|
|
# Send via WebSocket
|
|
if self.send(json.dumps(message)):
|
|
# Wait for response (simplified - real implementation needs async handling)
|
|
time.sleep(0.5)
|
|
return {"status": "sent"}
|
|
else:
|
|
# Fall back to HTTP if WebSocket unavailable
|
|
return self._http_request(query, context)
|
|
|
|
def _http_request(self, query: str, context: Optional[Dict] = None) -> Dict:
|
|
"""Fallback HTTP request."""
|
|
if not HAS_REQUESTS:
|
|
return {"error": "HTTP client not available"}
|
|
|
|
try:
|
|
response = requests.post(
|
|
f"{self.ws_url.replace('ws://', 'http://').replace('wss://', 'https://')}/api/query",
|
|
json={"query": query, "context": context},
|
|
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
timeout=10
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except Exception as e:
|
|
logger.error(f"HTTP request failed: {e}")
|
|
return {"error": str(e)}
|
|
|
|
def add_message_handler(self, handler: Callable[[Dict], None]):
|
|
"""Add a handler for incoming messages."""
|
|
self.message_handlers.append(handler)
|
|
|
|
def get_status(self) -> Dict:
|
|
"""Get client status."""
|
|
return {
|
|
"enabled": self.enabled,
|
|
"connected": self.is_connected,
|
|
"ws_url": self.ws_url
|
|
}
|
|
|
|
|
|
def main():
|
|
"""Test the OpenClaw client."""
|
|
client = OpenClawClient()
|
|
|
|
# Add message handler
|
|
def on_message(data):
|
|
print(f"Received: {data}")
|
|
|
|
client.add_message_handler(on_message)
|
|
|
|
# Test connection
|
|
print(f"OpenClaw Client Status: {client.get_status()}")
|
|
|
|
# Test query
|
|
response = client.send_request("Hello, how are you?")
|
|
print(f"Response: {response}")
|
|
|
|
# Keep alive
|
|
try:
|
|
while True:
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down...")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(level=logging.INFO)
|
|
main()
|