feat: real-time reasoning speech, dynamic voice tags, and unified web UI bubbles

This commit is contained in:
Adolfo Reyna
2026-08-12 10:17:47 -04:00
parent b5034b4b16
commit 404a7071fb
8 changed files with 734 additions and 94 deletions
+27 -3
View File
@@ -80,7 +80,8 @@ say them:
- Spell out things that only make sense visually. Say "line forty-two of
bot dot py" rather than pasting a path.
- Use your available tools (listing directories, searching, reading files, shell execution) whenever the user asks about files, commands, CLI tools (such as Paseo), or workspace tasks.
- You can change your own voice! If the user asks to list available voices or switch voice, run `python bin/voice_tool.py list` or `python bin/voice_tool.py set <voice_name>` (voices: af_heart, af_bella, am_michael, am_fenrir, am_puck, bf_emma, bm_george, Moira, Daniel).
- You can dynamically change your spoken voice mid-response! Use markdown tags like `[Voice:af_bella]` or `[Voice:am_michael]` inline to switch voices (e.g. `[Voice:af_bella] Hello from Bella! [Voice:am_michael] And hello from Michael!`). The active voice will persist until you change it again.
- You can change your default voice! If the user asks to list available voices or switch voice permanently, run `python bin/voice_tool.py list` or `python bin/voice_tool.py set <voice_name>` (voices: af_heart, af_bella, am_michael, am_fenrir, am_puck, bf_emma, bm_george, Moira, Daniel).
- You can change your AI model on the fly! If the user asks to list available models or change model, run `python bin/model_tool.py list` or `python bin/model_tool.py set <model_name>` (models: luna, gemma, deepseek, gpt-oss, sonnet, etc.).
- You can reset or start a fresh conversation session! If the user asks to start a fresh session, reset the conversation, or clear session context, run `python bin/session_tool.py reset`.
- You can switch Hermes agent profiles! If the user asks to list Hermes profiles or switch profile, run `python bin/profile_tool.py list` or `python bin/profile_tool.py set <profile_name>`.
@@ -287,6 +288,18 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="Disable the Companion Web Chat UI server.",
)
parser.add_argument(
"--dual-engine",
action="store_true",
default=False,
help="Enable dual-engine mode: instant macOS foundation model (<400ms) + deep Hermes reasoning.",
)
parser.add_argument(
"--no-dual-engine",
action="store_false",
dest="dual_engine",
help="Disable dual-engine mode and run single engine directly.",
)
parser.add_argument("--log-level", default="INFO")
return parser.parse_args()
@@ -520,17 +533,28 @@ def build_llm(
model = (model_manager.load_saved_model() if model_manager else None) or getattr(args, "hermes_model", "hermes-3")
if args.llm_engine in ("hermes", "ollama"):
from hermes_llm import HermesLLM, probe_hermes
from dual_engine import DualEngineProcessor
available, reason = probe_hermes(model)
logger.info(f"LLM: Hermes ({reason})")
# Hermes handles persona, personality, and memory natively.
return HermesLLM(
deep_llm = HermesLLM(
model=model,
cwd=args.cwd,
session_name="Voice Agent",
observer=observer,
)
if getattr(args, "dual_engine", False):
from apple_llm import MacOSLLM, probe_apple_llm
fast_available, fast_reason = probe_apple_llm()
if fast_available:
logger.info(f"Dual-Engine: Pairing Hermes with fast macOS Foundation Model ({fast_reason})")
fast_llm = MacOSLLM(model=args.mlx_model, system_prompt="You are a fast voice assistant.")
return DualEngineProcessor(fast_llm=fast_llm, deep_llm=deep_llm, observer=observer)
return deep_llm
if args.llm_engine in ("apple", "macos"):
from apple_llm import MacOSLLM, probe_apple_llm