Update 2026-04-22 11:43:20

This commit is contained in:
kassam 2026-04-22 11:43:21 +04:00
parent 2e3cc1ba5b
commit 3122a52966
3 changed files with 29 additions and 17 deletions

View File

@ -47,10 +47,10 @@ from Core.config_loader import load_config
LOG_DIR = os.path.join(PROJECT_ROOT, "logs") LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
os.makedirs(LOG_DIR, exist_ok=True) os.makedirs(LOG_DIR, exist_ok=True)
# logging.basicConfig is idempotent per process: if marcus_voice configured # All voice-subsystem logs go ONLY to logs/voice.log, not stdout — the
# the root logger first, this call is a no-op and both modules share the same # terminal REPL needs a clean `Command:` prompt. Anything the operator
# RotatingFileHandler (stdlib FileHandlers hold an internal lock, so concurrent # needs to see is print()-ed explicitly from the callback sites.
# writes to voice.log are safe). Rotation caps voice.log at 5 MB × 3 backups. # basicConfig is idempotent (no-op if marcus_voice installed handlers first).
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
@ -59,7 +59,6 @@ logging.basicConfig(
os.path.join(LOG_DIR, "voice.log"), os.path.join(LOG_DIR, "voice.log"),
maxBytes=5_000_000, backupCount=3, encoding="utf-8", maxBytes=5_000_000, backupCount=3, encoding="utf-8",
), ),
logging.StreamHandler(),
], ],
) )
log = logging.getLogger("audio_api") log = logging.getLogger("audio_api")

View File

@ -172,7 +172,10 @@ def _init_voice():
text = (text or "").strip() text = (text or "").strip()
if not text: if not text:
return return
print(f" [Voice] {text}") # One clean, distinctive line so the operator can see exactly
# what Whisper transcribed before the brain reacts. Everything
# else from the voice subsystem is file-only.
print(f' [Sanad] heard: "{text}"')
try: try:
result = process_command(text) result = process_command(text)
except Exception as e: except Exception as e:
@ -182,10 +185,12 @@ def _init_voice():
sp = (result.get("speak") or "").strip() sp = (result.get("speak") or "").strip()
if sp and _audio_api: if sp and _audio_api:
_audio_api.speak(sp) _audio_api.speak(sp)
# Redraw the Command: prompt that our print clobbered
print("Command: ", end="", flush=True)
_voice_module = VoiceModule(_audio_api, on_command=_on_command) _voice_module = VoiceModule(_audio_api, on_command=_on_command)
_voice_module.start() _voice_module.start()
print(" [Voice] Always listening (Whisper + G1 mic + TtsMaker)") print(" [Voice] listening in background — say \"Sanad\" + your command")
except Exception as e: except Exception as e:
print(f" [Voice] Init failed: {e} — continuing without voice") print(f" [Voice] Init failed: {e} — continuing without voice")
_audio_api = None _audio_api = None
@ -477,13 +482,15 @@ def run_terminal():
status = get_brain_status() status = get_brain_status()
print() print()
print("=" * 48) print("=" * 54)
print(" SANAD AI BRAIN — READY") print(" SANAD AI BRAIN — READY")
print("=" * 48) print("=" * 54)
for k, v in status.items(): for k, v in status.items():
print(f" {k:<10}: {v}") print(f" {k:<12}: {v}")
print("=" * 48) print("-" * 54)
print(" help | example | yolo | patrol | auto on/off | q") print(" Type a command, or say \"Sanad, <command>\".")
print(" Shortcuts: help | example | yolo | patrol | auto on/off | q")
print("=" * 54)
print() print()
try: try:

View File

@ -40,9 +40,11 @@ from Core.config_loader import load_config
LOG_DIR = os.path.join(PROJECT_ROOT, "logs") LOG_DIR = os.path.join(PROJECT_ROOT, "logs")
os.makedirs(LOG_DIR, exist_ok=True) os.makedirs(LOG_DIR, exist_ok=True)
# basicConfig is idempotent. Whichever of audio_api / marcus_voice imports # Voice runs as a background subsystem — its INFO/DEBUG logs go ONLY to
# first installs the rotating handler; the other no-ops. Both loggers then # logs/voice.log so they don't drown out the interactive `Command:` prompt.
# share the same file handle with stdlib's per-handler thread lock. # Anything the user needs to see (wake-word fired, command heard) is
# print()-ed explicitly from the callbacks below.
# basicConfig is idempotent; audio_api may have already called it.
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
@ -51,7 +53,6 @@ logging.basicConfig(
os.path.join(LOG_DIR, "voice.log"), os.path.join(LOG_DIR, "voice.log"),
maxBytes=5_000_000, backupCount=3, encoding="utf-8", maxBytes=5_000_000, backupCount=3, encoding="utf-8",
), ),
logging.StreamHandler(),
], ],
) )
log = logging.getLogger("marcus_voice") log = logging.getLogger("marcus_voice")
@ -248,6 +249,11 @@ class VoiceModule:
if self._check_wake_word(text): if self._check_wake_word(text):
log.info("Wake word detected!") log.info("Wake word detected!")
# One clean line to the terminal so the operator knows voice
# actually heard them, even though all other voice logs are
# file-only. \n leads because we may be painting over a
# half-drawn `Command:` prompt.
print("\n [Sanad] wake heard — recording command…")
self._state = State.WAKE_HEARD self._state = State.WAKE_HEARD
# Acknowledge # Acknowledge