opentui(phase3): launcher integration — HERMES_TUI_ENGINE dual-engine
hermes --tui launches the native OpenTUI engine (Bun) when HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config); Ink stays the default and the shipping path is untouched. - _resolve_tui_engine() (env > config > ink); refuses opentui on Windows/Termux (no Bun) -> falls back to ink with a notice. - _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step). - _bun_bin() with HERMES_BUN override. - Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host must not bootstrap Node). - Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun is JSC; the V8 flag errors/ignores). Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI -> real Python gateway streamed a real reply. No-flag default still ink.
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
import base64
|
||||
import struct
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -256,169 +255,6 @@ class TestGenerateGeminiTts:
|
||||
|
||||
assert mock_post.call_args[0][0].startswith("https://custom-gemini.example.com/v1beta/")
|
||||
|
||||
def test_persona_prompt_file_appends_labeled_transcript(
|
||||
self, tmp_path, monkeypatch, mock_gemini_response
|
||||
):
|
||||
from tools.tts_tool import _generate_gemini_tts
|
||||
|
||||
persona_file = tmp_path / "voice-persona.md"
|
||||
persona_file.write_text(
|
||||
"# AUDIO PROFILE: Dry Butler\n\n### DIRECTOR'S NOTES\nStyle: Understated.",
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = {"gemini": {"persona_prompt_file": str(persona_file)}}
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
|
||||
with patch("requests.post", return_value=mock_gemini_response) as mock_post:
|
||||
_generate_gemini_tts("Hi", str(tmp_path / "test.wav"), config)
|
||||
|
||||
prompt_text = mock_post.call_args[1]["json"]["contents"][0]["parts"][0]["text"]
|
||||
assert "Synthesize speech from the TRANSCRIPT only" in prompt_text
|
||||
assert "# AUDIO PROFILE: Dry Butler" in prompt_text
|
||||
assert "### DIRECTOR'S NOTES\nStyle: Understated." in prompt_text
|
||||
assert "#### TRANSCRIPT\nHi" in prompt_text
|
||||
|
||||
def test_persona_prompt_file_supports_transcript_placeholder(
|
||||
self, tmp_path, monkeypatch, mock_gemini_response
|
||||
):
|
||||
from tools.tts_tool import _generate_gemini_tts
|
||||
|
||||
persona_file = tmp_path / "voice-persona.md"
|
||||
persona_file.write_text(
|
||||
"### DIRECTOR'S NOTES\nPacing: Slow.\n\n#### TRANSCRIPT\n{{ transcript }}",
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = {"gemini": {"persona_prompt_file": str(persona_file)}}
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
|
||||
with patch("requests.post", return_value=mock_gemini_response) as mock_post:
|
||||
_generate_gemini_tts("Read this.", str(tmp_path / "test.wav"), config)
|
||||
|
||||
prompt_text = mock_post.call_args[1]["json"]["contents"][0]["parts"][0]["text"]
|
||||
assert "{{ transcript }}" not in prompt_text
|
||||
assert "#### TRANSCRIPT\nRead this." in prompt_text
|
||||
|
||||
def test_missing_persona_prompt_file_warns_and_continues(
|
||||
self, tmp_path, monkeypatch, caplog, mock_gemini_response
|
||||
):
|
||||
from tools.tts_tool import _generate_gemini_tts
|
||||
|
||||
config = {"gemini": {"persona_prompt_file": str(tmp_path / "missing.md")}}
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
|
||||
with patch("requests.post", return_value=mock_gemini_response) as mock_post:
|
||||
_generate_gemini_tts("Hi", str(tmp_path / "test.wav"), config)
|
||||
|
||||
prompt_text = mock_post.call_args[1]["json"]["contents"][0]["parts"][0]["text"]
|
||||
assert prompt_text == "Hi"
|
||||
assert "persona prompt file unavailable" in caplog.text
|
||||
|
||||
def test_audio_tags_disabled_does_not_call_rewriter(
|
||||
self, tmp_path, monkeypatch, mock_gemini_response
|
||||
):
|
||||
from tools.tts_tool import _generate_gemini_tts
|
||||
|
||||
config = {
|
||||
"gemini": {
|
||||
"model": "gemini-3.1-flash-tts-preview",
|
||||
"audio_tags": False,
|
||||
}
|
||||
}
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
|
||||
with patch("agent.auxiliary_client.call_llm") as mock_call_llm, \
|
||||
patch("requests.post", return_value=mock_gemini_response) as mock_post:
|
||||
_generate_gemini_tts("Hi there.", str(tmp_path / "test.wav"), config)
|
||||
|
||||
mock_call_llm.assert_not_called()
|
||||
prompt_text = mock_post.call_args[1]["json"]["contents"][0]["parts"][0]["text"]
|
||||
assert prompt_text == "Hi there."
|
||||
|
||||
def test_audio_tags_enabled_rewrites_hidden_tts_script(
|
||||
self, tmp_path, monkeypatch, mock_gemini_response
|
||||
):
|
||||
from tools.tts_tool import _generate_gemini_tts
|
||||
|
||||
persona_file = tmp_path / "voice-persona.md"
|
||||
persona_file.write_text(
|
||||
"### DIRECTOR'S NOTES\nStyle: Warm and amused.",
|
||||
encoding="utf-8",
|
||||
)
|
||||
response = SimpleNamespace(
|
||||
choices=[
|
||||
SimpleNamespace(
|
||||
message=SimpleNamespace(content="[warmly] Hi there. [soft laugh]")
|
||||
)
|
||||
]
|
||||
)
|
||||
config = {
|
||||
"gemini": {
|
||||
"model": "gemini-3.1-flash-tts-preview",
|
||||
"audio_tags": True,
|
||||
"persona_prompt_file": str(persona_file),
|
||||
}
|
||||
}
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
|
||||
with patch("agent.auxiliary_client.call_llm", return_value=response) as mock_call_llm, \
|
||||
patch("requests.post", return_value=mock_gemini_response) as mock_post:
|
||||
_generate_gemini_tts("Hi there.", str(tmp_path / "test.wav"), config)
|
||||
|
||||
mock_call_llm.assert_called_once()
|
||||
call_kwargs = mock_call_llm.call_args.kwargs
|
||||
assert call_kwargs["task"] == "tts_audio_tags"
|
||||
assert "Audio tags are inline square-bracket modifiers" in call_kwargs["messages"][0]["content"]
|
||||
assert "Style: Warm and amused." in call_kwargs["messages"][1]["content"]
|
||||
assert "Hi there." in call_kwargs["messages"][1]["content"]
|
||||
|
||||
prompt_text = mock_post.call_args[1]["json"]["contents"][0]["parts"][0]["text"]
|
||||
assert "Synthesize speech from the TRANSCRIPT only" in prompt_text
|
||||
assert "### DIRECTOR'S NOTES\nStyle: Warm and amused." in prompt_text
|
||||
assert "#### TRANSCRIPT\n[warmly] Hi there. [soft laugh]" in prompt_text
|
||||
|
||||
def test_audio_tags_enabled_skips_non_tag_capable_model(
|
||||
self, tmp_path, monkeypatch, mock_gemini_response, caplog
|
||||
):
|
||||
from tools.tts_tool import _generate_gemini_tts
|
||||
|
||||
config = {
|
||||
"gemini": {
|
||||
"model": "gemini-2.5-flash-preview-tts",
|
||||
"audio_tags": True,
|
||||
}
|
||||
}
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
|
||||
with patch("agent.auxiliary_client.call_llm") as mock_call_llm, \
|
||||
patch("requests.post", return_value=mock_gemini_response) as mock_post:
|
||||
_generate_gemini_tts("Hi there.", str(tmp_path / "test.wav"), config)
|
||||
|
||||
mock_call_llm.assert_not_called()
|
||||
prompt_text = mock_post.call_args[1]["json"]["contents"][0]["parts"][0]["text"]
|
||||
assert prompt_text == "Hi there."
|
||||
assert "not known to support Gemini audio tags" in caplog.text
|
||||
|
||||
def test_audio_tag_rewrite_failure_falls_back_to_original_text(
|
||||
self, tmp_path, monkeypatch, mock_gemini_response, caplog
|
||||
):
|
||||
from tools.tts_tool import _generate_gemini_tts
|
||||
|
||||
config = {
|
||||
"gemini": {
|
||||
"model": "gemini-3.1-flash-tts-preview",
|
||||
"audio_tags": True,
|
||||
}
|
||||
}
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
|
||||
with patch("agent.auxiliary_client.call_llm", side_effect=RuntimeError("boom")), \
|
||||
patch("requests.post", return_value=mock_gemini_response) as mock_post:
|
||||
_generate_gemini_tts("Hi there.", str(tmp_path / "test.wav"), config)
|
||||
|
||||
prompt_text = mock_post.call_args[1]["json"]["contents"][0]["parts"][0]["text"]
|
||||
assert prompt_text == "Hi there."
|
||||
assert "audio tag rewrite failed" in caplog.text
|
||||
|
||||
|
||||
class TestGeminiInCheckRequirements:
|
||||
def test_gemini_api_key_satisfies_requirements(self, monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user