Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion core/settings_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2656,6 +2656,12 @@ def _test_comfyui_connection(self):
"Ubuntu führt Parakeet-STT und Qwen3-TTS auf CUDA aus. Der erkannte Text wird "
"an den geschützten Trinity-Core-Endpunkt der Windows-VM weitergereicht.",
),
(
"Linux als vollständiger Trinity-Server",
"trinity-linux-server",
"Linux führt STT, Trinity-Core mit Memory und Agenten sowie TTS selbst aus. "
"Desktop und Companion verbinden sich als Clients über das private Netzwerk.",
),
(
"Diagnose: Ornith direkt, ohne Trinity",
"eve-direct-ornith",
Expand All @@ -2682,7 +2688,7 @@ def _load_voice_profile_form(self, _selection=None):
"Benutzerdefiniertes Eve-Profil.",
)
self.voice_profile_description.setText(description)
realtime = profile_name in {"eve-mac-server", "eve-windows-server", "eve-linux-gpu-server"}
realtime = profile_name in {"eve-mac-server", "eve-windows-server", "eve-linux-gpu-server", "trinity-linux-server"}
remote_client = profile_name == "eve-windows-remote"
remote_server = profile_name == "eve-linux-gpu-server"
for field in (
Expand Down
14 changes: 14 additions & 0 deletions core/voice/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@
"tts_model": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
"tts_backend": "torch",
},
"trinity-linux-server": {
"mode": "realtime",
"device": "cuda",
"runtime_role": "server",
"conversation_backend": "trinity",
"bind_host": "0.0.0.0",
"public_port": 8766,
"internal_port": 18766,
"local_audio": False,
"num_pipelines": 2,
"stt_model": "nvidia/parakeet-tdt-0.6b-v3",
"tts_model": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
"tts_backend": "torch",
},
"eve-windows-remote": {
"mode": "realtime",
"device": "cpu",
Expand Down
50 changes: 50 additions & 0 deletions docs/LINUX_UNIFIED_SERVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Trinity ohne Windows-VM: kontrollierter Linux-Server

Trinity kann den textuellen Kern, Memory, Bridge und die CUDA-Sprachpipeline
auf demselben Linux-Host betreiben. Das Profil `trinity-linux-server` verbindet
Parakeet-STT und Qwen3-TTS mit dem **lokalen** Trinity-Kern. Das bisherige
`eve-linux-gpu-server` bleibt unverändert und leitet weiterhin an einen
externen Kern weiter. Standalone-Profile auf Mac und Windows bleiben erhalten.

## Vor dem Umschalten

1. Die laufende Linux-Installation, lokale Änderungen, Dienste und Ports
inventarisieren. Nie über eine aktive Installation hinweg blind pullen.
2. Betriebsdaten, Konfiguration, Soul/User, RAG, Memory und Schlüssel getrennt
vom Git-Checkout sichern. Keine Secrets in Git oder Service-Logs schreiben.
3. Linux-Trinity in einem separaten Checkout mit eigener Runtime und freien
Testports installieren. Die bestehende Voice-Pipeline und die Windows-VM
bleiben dabei aktiv.
4. Im privaten `core/config.json` LLM-Endpunkt, Persona, Memory und benötigte
Agenten gezielt konfigurieren. Die Windows-Konfiguration nicht vollständig
kopieren: Windows-Pfade und gerätespezifische Integrationen sind unbrauchbar.
5. Bridge und Voice nur an Tailnet/private Interfaces binden und mit eigenen
Zugangstokens schützen. Für iPhone/iPad müssen Bridge- und Voice-Adresse
auf denselben Linux-Host zeigen. Niemals öffentliche Ports öffnen.

## Start als ein betreuter Prozess

Nach erfolgreichem Test und mit freien Ports:

```text
python trinity_cli.py server --host 100.70.50.6 --port 8765 \
--voice-profile trinity-linux-server
```

Der Server betreut Trinity-Kern, Bridge und Sprachpipeline. Ohne
`--voice-profile` bleibt das bisherige server-only-Verhalten unverändert.
Der Sprachpfad ist erst nach einem echten STT→Memory/LLM→TTS-Test freigegeben;
ein HTTP-Healthcheck allein genügt nicht.

## Noch nicht Teil dieses Schritts

- Der Desktop-Umschalter zu einem reinen Remote-Client ist noch zu bauen;
die bisherige Desktop-Checkbox allein routet noch nicht alle Funktionen.
- Bildschirminhalt, Folien und Dateien brauchen einen authentifizierten
Upload-/Kontextkanal mit Freigabe am jeweiligen Client. Dateien dürfen nicht
nur als lokaler Pfad an den Linux-Host gesendet werden.
- Der TrinityHUB ist ein eigener Dienst. Aufrufe seiner Werkstätten benötigen
definierte APIs und Berechtigungen; ein eingebetteter Browser ist noch keine
sichere Agentenverbindung.
- Alte Windows-Dienste erst nach Vergleich von Antworten, Memory, Agenten,
Vortrags-Vision, Mobilgeräten und Rückfallweg abschalten.
21 changes: 21 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,24 @@ def test_server_uses_saved_server_settings(tmp_path, monkeypatch):

assert result == 0
assert captured == {"home": home, "host": "0.0.0.0", "port": 8888, "token": "secret"}


def test_server_passes_optional_linux_voice_profile(tmp_path, monkeypatch):
import trinity_server

home = tmp_path
(home / "core").mkdir()
(home / "trinity_launcher.py").touch()
save_config(home / "core" / "config.json", {
"server": {"voice_profile": "trinity-linux-server"}
})
captured = {}
monkeypatch.setattr(trinity_server, "run_server", lambda home_arg, **kwargs:
captured.update(kwargs) or 0)

result = trinity_cli.run_server_command(
home, SimpleNamespace(host=None, port=None, token=None, voice_profile=None)
)

assert result == 0
assert captured["voice_profile"] == "trinity-linux-server"
11 changes: 11 additions & 0 deletions tests/voice/test_voice_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,14 @@ def test_ubuntu_server_uses_remote_windows_trinity_core(tmp_path):
assert command[command.index("--responses_api_base_url") + 1] == "http://100.64.0.20:18767/v1"
assert command[command.index("--responses_api_api_key") + 1] == "core-secret"
assert command[command.index("--model_name") + 1] == "trinity-core"


def test_linux_trinity_server_routes_voice_to_its_local_core(tmp_path):
config = configured_voice(tmp_path, profile="trinity-linux-server")
command = build_speech_to_speech_command(config)

assert command[command.index("--responses_api_base_url") + 1] == (
f"http://{config.backend_host}:{config.backend_port}/v1"
)
assert command[command.index("--device") + 1] == "cuda"
assert "remote_core_base_url" not in command
17 changes: 17 additions & 0 deletions tests/voice/test_voice_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@ def test_ubuntu_gpu_server_routes_back_to_windows_core(tmp_path):
assert config.validate() == []


def test_linux_trinity_server_uses_local_core_without_windows_vm(tmp_path):
audio = tmp_path / "Trinity.mp3"
audio.write_bytes(b"voice")
config = load_voice_config(
tmp_path,
{"voice": {"engine": "eve", "profile": "trinity-linux-server",
"access_token": "voice-secret", "reference_audio": str(audio),
"backend_token": "local-core-secret"}},
)

assert config.profile.runtime_role == "server"
assert config.profile.conversation_backend == "trinity"
assert config.profile.device == "cuda"
assert config.profile.local_audio is False
assert config.validate() == []


def test_windows_remote_profile_needs_no_local_voice_models(tmp_path):
config = load_voice_config(
tmp_path,
Expand Down
8 changes: 6 additions & 2 deletions trinity_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,9 +639,11 @@ def run_server_command(home, args):
port = args.port or server.get("port") or 8765
token = args.token if args.token is not None else server.get("token", "")
auth_enabled = bool(getattr(args, "auth", False) or server.get("auth_enabled", False))
voice_profile = getattr(args, "voice_profile", None) or server.get("voice_profile")
extra = {"voice_profile": voice_profile} if voice_profile else {}
if auth_enabled:
return run_server(home, host=host, port=port, token=token, auth_enabled=True)
return run_server(home, host=host, port=port, token=token)
return run_server(home, host=host, port=port, token=token, auth_enabled=True, **extra)
return run_server(home, host=host, port=port, token=token, **extra)


def run_client_command(home, args):
Expand Down Expand Up @@ -1126,6 +1128,8 @@ def build_parser():
server.add_argument("--port", type=int, default=None, help="HTTP-Port")
server.add_argument("--token", default=None, help="Bearer-Token für die WebUI")
server.add_argument("--auth", action="store_true", help="Passwort-Accounts und getrennte Nutzerbereiche aktivieren")
server.add_argument("--voice-profile", default=None,
help="Sprachpipeline mitstarten, z.B. trinity-linux-server")
client = subparsers.add_parser("client", help="Diese Desktop-Installation mit einem Trinity-Server verbinden")
client.add_argument("client_action", choices=("login", "status", "logout", "add-user"))
client.add_argument("--url", help="URL des Trinity-Servers, z.B. http://100.x.y.z:8765")
Expand Down
37 changes: 31 additions & 6 deletions trinity_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@
def _terminate(process):
if process and process.poll() is None:
process.terminate()
try:
process.wait(timeout=8)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=3)


def run_server(home, host="127.0.0.1", port=8765, token="", auth_enabled=False):
def run_server(home, host="127.0.0.1", port=8765, token="", auth_enabled=False,
voice_profile=None):
home = Path(home).resolve()
logs = home / "logs"
logs.mkdir(parents=True, exist_ok=True)
Expand All @@ -31,22 +37,38 @@ def run_server(home, host="127.0.0.1", port=8765, token="", auth_enabled=False):
if auth_enabled:
bridge_command.append("--auth")
bridge = subprocess.Popen(bridge_command, cwd=home, env=env, stdout=bridge_log, stderr=subprocess.STDOUT)
voice_log = None
voice = None
if voice_profile:
voice_log = (logs / "server-voice.log").open("a", encoding="utf-8")
voice = subprocess.Popen(
[sys.executable, "-u", str(home / "trinity_cli.py"), "voice", "serve", "--profile", voice_profile],
cwd=home, env=env, stdout=voice_log, stderr=subprocess.STDOUT,
)
print(f"Trinity Server laeuft auf http://{host}:{port}")
if voice_profile:
print(f"Trinity Voice laeuft mit Profil {voice_profile}; Details: logs/server-voice.log")
if auth_enabled:
print("WebUI: / | Erster Aufruf: Admin-Account anlegen | getrennte Nutzerbereiche aktiv")
else:
print("WebUI: / | Logs: logs/server-runtime.log und logs/server-web.log")
processes = [runtime, bridge] + ([voice] if voice is not None else [])
exit_code = 0
try:
while runtime.poll() is None and bridge.poll() is None:
while all(process.poll() is None for process in processes):
time.sleep(0.4)
exit_code = next((int(process.returncode or 1) for process in processes
if process.poll() is not None), 1)
except KeyboardInterrupt:
print("\nTrinity Server wird beendet.")
finally:
_terminate(runtime)
_terminate(bridge)
for process in reversed(processes):
_terminate(process)
runtime_log.close()
bridge_log.close()
return runtime.returncode or bridge.returncode or 0
if voice_log is not None:
voice_log.close()
return exit_code


def main(argv=None):
Expand All @@ -56,8 +78,11 @@ def main(argv=None):
parser.add_argument("--port", type=int, default=8765)
parser.add_argument("--token", default=os.environ.get("TRINITY_WEB_TOKEN", ""))
parser.add_argument("--auth", action="store_true", help="Passwort-Accounts und getrennte Nutzerbereiche aktivieren")
parser.add_argument("--voice-profile", default=None,
help="Sprachpipeline mitstarten, z.B. trinity-linux-server")
args = parser.parse_args(argv)
return run_server(args.home, args.host, args.port, args.token, args.auth)
return run_server(args.home, args.host, args.port, args.token, args.auth,
voice_profile=args.voice_profile)


if __name__ == "__main__":
Expand Down
Loading