Update 2026-04-21 16:27:07
This commit is contained in:
parent
e0f6acd5c7
commit
af1d0c1b8a
@ -28,22 +28,27 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import wave
|
||||
import numpy as np
|
||||
|
||||
# ─── PATH CONFIG ─────────────────────────────────────────
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
BASE_PATH = os.environ.get("PROJECT_BASE", "/home/unitree")
|
||||
PROJECT_NAME = "Marcus"
|
||||
PROJECT_ROOT = os.path.join(BASE_PATH, PROJECT_NAME)
|
||||
# ─── PATH + CONFIG ───────────────────────────────────────
|
||||
# Use the canonical loaders from Core/ so path + config logic lives in one place.
|
||||
_PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _PROJECT_DIR not in sys.path:
|
||||
sys.path.insert(0, _PROJECT_DIR)
|
||||
from Core.env_loader import PROJECT_ROOT
|
||||
from Core.config_loader import load_config
|
||||
|
||||
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
# Note: logging.basicConfig() only takes effect on the first call per process.
|
||||
# If the voice module already configured logging (common path via run_marcus.py),
|
||||
# this call is a no-op. When audio_api is used standalone, it wires logs to
|
||||
# logs/voice.log + stderr.
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||
@ -55,12 +60,6 @@ logging.basicConfig(
|
||||
log = logging.getLogger("audio_api")
|
||||
|
||||
|
||||
def load_config(name: str) -> dict:
|
||||
path = os.path.join(PROJECT_ROOT, "Config", f"config_{name}.json")
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
# ─── AUDIO API CLASS ─────────────────────────────────────
|
||||
|
||||
class AudioAPI:
|
||||
@ -135,11 +134,12 @@ class AudioAPI:
|
||||
Speak `text` in English through the G1 built-in TTS (TtsMaker).
|
||||
|
||||
Mutes (flushes) the mic during playback so the voice loop doesn't
|
||||
hear the robot's own voice and transcribe itself. The `lang`
|
||||
argument is accepted for API compatibility but only "en" plays —
|
||||
non-ASCII text (Arabic) is rejected by BuiltinTTS.
|
||||
hear the robot's own voice and transcribe itself. `lang` is kept
|
||||
in the signature for API compatibility but only `"en"` is accepted
|
||||
— non-ASCII text (Arabic) is rejected by BuiltinTTS because the
|
||||
G1 firmware silently maps it to Chinese, which nobody wants.
|
||||
"""
|
||||
if lang and lang not in ("en", "auto"):
|
||||
if lang and lang != "en":
|
||||
log.warning("builtin_tts only supports English; got lang=%r — skipping", lang)
|
||||
return
|
||||
if self._tts_engine is None:
|
||||
|
||||
@ -31,7 +31,7 @@ Files saved
|
||||
summary.txt — auto-generated LLaVA summary
|
||||
frames/ — camera captures at interesting points
|
||||
|
||||
Import in marcus_llava.py
|
||||
Import in marcus_brain.py
|
||||
-------------------------
|
||||
from marcus_autonomous import AutonomousMode
|
||||
auto = AutonomousMode(
|
||||
@ -303,17 +303,20 @@ class AutonomousMode:
|
||||
def _move_forward(self) -> bool:
|
||||
"""
|
||||
Move forward for FORWARD_DURATION seconds.
|
||||
Returns True if blocked (no actual movement detected).
|
||||
|
||||
Returns True if blocked. Obstacle detection is not wired in here —
|
||||
LiDAR runs at the brain layer (API.lidar_api.obstacle_ahead) and
|
||||
isn't passed into AutonomousMode today. Extend __init__ with an
|
||||
optional `obstacle_fn=` hook if you want this loop to stop on
|
||||
obstacles detected outside YOLO's view.
|
||||
"""
|
||||
# TODO: integrate LiDAR when available for true obstacle detection
|
||||
# For now: move and assume not blocked
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < FORWARD_DURATION and self._enabled:
|
||||
self._send_vel(vx=0.25)
|
||||
time.sleep(0.05)
|
||||
self._send_vel(0, 0, 0)
|
||||
time.sleep(0.1)
|
||||
return False # Not blocked — no LiDAR yet
|
||||
return False
|
||||
|
||||
def _turn(self, direction: str, duration: float):
|
||||
"""Turn left or right for given duration."""
|
||||
|
||||
@ -501,14 +501,18 @@ def run_terminal():
|
||||
continue
|
||||
if cmd.lower() in ("q", "quit", "exit"):
|
||||
break
|
||||
if cmd.lower() == "mute/":
|
||||
import subprocess
|
||||
subprocess.run(["pactl", "set-source-mute", "0", "1"], capture_output=True)
|
||||
print(" Mic muted")
|
||||
if cmd.lower() in ("mute/", "unmute/"):
|
||||
# Route through the audio API so the action respects whichever
|
||||
# mic backend is active (BuiltinMic flushes the UDP buffer;
|
||||
# the legacy pactl path mutes PulseAudio source 3).
|
||||
if _audio_api is None:
|
||||
print(" Voice is not initialized")
|
||||
continue
|
||||
if cmd.lower() == "unmute/":
|
||||
import subprocess
|
||||
subprocess.run(["pactl", "set-source-mute", "0", "0"], capture_output=True)
|
||||
if cmd.lower() == "mute/":
|
||||
_audio_api._mute_mic()
|
||||
print(" Mic muted")
|
||||
else:
|
||||
_audio_api._unmute_mic()
|
||||
print(" Mic unmuted")
|
||||
continue
|
||||
result = process_command(cmd)
|
||||
|
||||
@ -19,7 +19,7 @@ Folder structure
|
||||
alerts.json ← [{time, type, detail}]
|
||||
summary.txt ← auto-generated session summary
|
||||
|
||||
Import in marcus_llava.py
|
||||
Import in marcus_brain.py
|
||||
-------------------------
|
||||
from marcus_memory import Memory
|
||||
mem = Memory() ← call once at startup
|
||||
|
||||
@ -14,6 +14,18 @@ import io
|
||||
import numpy as np
|
||||
import websockets
|
||||
|
||||
# Load network defaults from Config/config_Network.json. Falls back to sane
|
||||
# hardcoded values if the config isn't reachable (the GUI can run standalone
|
||||
# from a workstation that isn't on the same filesystem as the project).
|
||||
_PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _PROJECT_DIR not in sys.path:
|
||||
sys.path.insert(0, _PROJECT_DIR)
|
||||
try:
|
||||
from Core.config_loader import load_config
|
||||
_net = load_config("Network")
|
||||
JETSON_IP = _net.get("jetson_ip", "192.168.123.164")
|
||||
JETSON_PORT = _net.get("websocket_port", 8765)
|
||||
except Exception:
|
||||
JETSON_IP = "192.168.123.164"
|
||||
JETSON_PORT = 8765
|
||||
|
||||
@ -1000,7 +1012,10 @@ class MarcusGUI:
|
||||
p2 = ImageTk.PhotoImage(img_resized)
|
||||
self.cam_preview.configure(image=p2, text="")
|
||||
self.cam_preview.image = p2
|
||||
except Exception: pass
|
||||
except Exception as e:
|
||||
# Malformed base64 / torn frame / Tk disposed during shutdown —
|
||||
# log it so UI freezes become debuggable rather than silent.
|
||||
print(f" [Client] _update_frame dropped: {type(e).__name__}: {e}")
|
||||
|
||||
def _update_cam_config(self, d):
|
||||
p, w, h, f = d.get("profile","?"), d.get("width","?"), d.get("height","?"), d.get("fps","?")
|
||||
|
||||
@ -9,7 +9,7 @@ Two sources (priority order):
|
||||
1. ROS2 /dog_odom — joint encoder data → ±2cm accuracy
|
||||
2. Dead reckoning — velocity × time integration → ±10cm accuracy
|
||||
|
||||
Import in marcus_llava.py
|
||||
Import in marcus_brain.py
|
||||
--------------------------
|
||||
from marcus_odometry import Odometry
|
||||
odom = Odometry()
|
||||
@ -78,7 +78,7 @@ class Odometry:
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self._alive = [False]
|
||||
self._sock = None # ZMQ PUB socket (shared from marcus_llava)
|
||||
self._sock = None # ZMQ PUB socket (shared from marcus_brain)
|
||||
|
||||
# Position state
|
||||
self._x = 0.0 # meters from start
|
||||
@ -105,7 +105,7 @@ class Odometry:
|
||||
Start odometry tracking in background thread.
|
||||
|
||||
Args:
|
||||
zmq_sock : existing ZMQ PUB socket from marcus_llava.py.
|
||||
zmq_sock : existing ZMQ PUB socket from marcus_brain.py.
|
||||
If None, creates its own socket on ZMQ_PORT.
|
||||
Passing the shared socket avoids port conflicts.
|
||||
|
||||
@ -169,7 +169,7 @@ class Odometry:
|
||||
except zmq.ZMQError as e:
|
||||
if "Address already in use" in str(e):
|
||||
print(f" [Odom] ⚠️ ZMQ port {ZMQ_PORT} already in use.")
|
||||
print(" [Odom] Pass zmq_sock=sock from marcus_llava.py instead.")
|
||||
print(" [Odom] Pass zmq_sock=sock from marcus_brain.py instead.")
|
||||
else:
|
||||
print(f" [Odom] ⚠️ ZMQ error: {e}")
|
||||
self._sock = None
|
||||
|
||||
@ -15,7 +15,7 @@ How it works
|
||||
5. Optional: YOLO pre-filter speeds up search (find person class first,
|
||||
then LLaVA verifies it's the right person)
|
||||
|
||||
Usage in marcus_llava.py
|
||||
Usage in marcus_brain.py
|
||||
------------------------
|
||||
from marcus_imgsearch import ImageSearch
|
||||
searcher = ImageSearch(get_frame_fn=get_frame, send_vel_fn=send_vel,
|
||||
@ -420,11 +420,11 @@ class ImageSearch:
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# WIRE INTO marcus_llava.py — add to main loop
|
||||
# WIRE INTO marcus_brain.py — add to main loop
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
"""
|
||||
Add to marcus_llava.py imports:
|
||||
Add to marcus_brain.py imports:
|
||||
from marcus_imgsearch import ImageSearch
|
||||
|
||||
Add after Memory init:
|
||||
@ -504,7 +504,7 @@ if __name__ == "__main__":
|
||||
elif args.image or args.hint:
|
||||
# Real search — needs robot hardware
|
||||
print("Real search requires robot hardware.")
|
||||
print("Import ImageSearch into marcus_llava.py instead.")
|
||||
print("Import ImageSearch into marcus_brain.py instead.")
|
||||
print(f" image: {args.image}")
|
||||
print(f" hint: {args.hint}")
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ marcus_yolo.py — Marcus Vision Module
|
||||
=======================================
|
||||
Project : Marcus | YS Lootah Technology
|
||||
Purpose : YOLO-based person + object detection
|
||||
Import this module in marcus_llava.py — runs as background thread
|
||||
Import this module in marcus_brain.py — runs as background thread
|
||||
|
||||
Usage (imported):
|
||||
from marcus_yolo import start_yolo, yolo_sees, yolo_count, yolo_closest, yolo_summary
|
||||
@ -365,20 +365,20 @@ def _camera_loop(raw_frame_ref, frame_lock, cam_alive):
|
||||
time.sleep(2.0)
|
||||
|
||||
|
||||
# ── Start function — called by marcus_llava.py ────────────────────────────────
|
||||
# ── Start function — called by marcus_brain.py ────────────────────────────────
|
||||
|
||||
def start_yolo(raw_frame_ref=None, frame_lock=None):
|
||||
"""
|
||||
Start YOLO inference in background thread.
|
||||
|
||||
Called automatically by marcus_llava.py during startup.
|
||||
Shares the camera frame reference from marcus_llava's camera thread.
|
||||
Called automatically by marcus_brain.py during startup.
|
||||
Shares the camera frame reference from marcus_brain's camera thread.
|
||||
|
||||
Args:
|
||||
raw_frame_ref : list[np.ndarray|None] — shared raw BGR frame
|
||||
frame_lock : threading.Lock protecting raw_frame_ref
|
||||
|
||||
Example (in marcus_llava.py):
|
||||
Example (in marcus_brain.py):
|
||||
from marcus_yolo import start_yolo, yolo_sees, yolo_summary
|
||||
start_yolo(raw_frame_ref=_raw_frame, frame_lock=_raw_lock)
|
||||
"""
|
||||
|
||||
@ -22,23 +22,23 @@ Usage:
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
# ─── PATH CONFIG ─────────────────────────────────────────
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
BASE_PATH = os.environ.get("PROJECT_BASE", "/home/unitree")
|
||||
PROJECT_NAME = "Marcus"
|
||||
PROJECT_ROOT = os.path.join(BASE_PATH, PROJECT_NAME)
|
||||
|
||||
import json
|
||||
# ─── PATH + CONFIG ───────────────────────────────────────
|
||||
# Single source of truth lives in Core/; everyone else imports from there.
|
||||
_PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _PROJECT_DIR not in sys.path:
|
||||
sys.path.insert(0, _PROJECT_DIR)
|
||||
from Core.env_loader import PROJECT_ROOT
|
||||
from Core.config_loader import load_config
|
||||
|
||||
LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
# Idempotent — only the first call per process installs handlers.
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
||||
@ -50,12 +50,6 @@ logging.basicConfig(
|
||||
log = logging.getLogger("marcus_voice")
|
||||
|
||||
|
||||
def load_config(name: str) -> dict:
|
||||
path = os.path.join(PROJECT_ROOT, "Config", f"config_{name}.json")
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
# ─── STATE ENUM ──────────────────────────────────────────
|
||||
|
||||
class State:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user