diff --git a/api/routes.py b/api/routes.py
index 279b3ca0..8fce6563 100644
--- a/api/routes.py
+++ b/api/routes.py
@@ -14765,7 +14765,7 @@ def _normalize_tts_prosody(value, *, unit: str) -> str | None:
def _handle_tts(handler, parsed):
- """Generate TTS audio via Edge TTS. POST JSON body only.
+ """Generate TTS audio via supported server TTS engines. POST JSON body only.
Design note addressing deep review blocker #4 (synchronous I/O):
The server uses ThreadingHTTPServer (see server.py:173), so each request
@@ -14785,7 +14785,7 @@ def _handle_tts(handler, parsed):
voice = "zh-CN-XiaoxiaoNeural"
rate_str = ""
pitch_str = ""
- engine = "edge" # "edge" | "elevenlabs" | "browser" (browser is client-side only)
+ engine = "edge" # "edge" | "elevenlabs" | "openai" | "browser" (browser is client-side only)
if handler.command != "POST":
from api.helpers import bad as _bad
@@ -14953,6 +14953,77 @@ def _handle_tts(handler, parsed):
pass
return True
+ # ── OpenAI-compatible TTS ──────────────────────────────────────────
+ if engine == "openai":
+ api_key = os.getenv("VOICE_TOOLS_OPENAI_KEY", "").strip()
+ if not api_key:
+ api_key = os.getenv("OPENAI_API_KEY", "").strip()
+ if not api_key:
+ try:
+ from api.onboarding import _load_env_file
+ from api.profiles import get_active_hermes_home
+ env_cfg = _load_env_file(get_active_hermes_home() / ".env")
+ api_key = env_cfg.get("VOICE_TOOLS_OPENAI_KEY", "") or env_cfg.get("OPENAI_API_KEY", "")
+ except Exception:
+ pass
+ if not api_key:
+ from api.helpers import bad as _bad
+ return _bad(handler, "OpenAI API key not configured", 503)
+
+ base_url = "https://api.openai.com/v1"
+ model = "gpt-4o-mini-tts"
+ oai_voice = "alloy"
+ try:
+ from api.config import get_config
+ tts_cfg = (get_config() or {}).get("tts", {})
+ if isinstance(tts_cfg, dict):
+ oai_cfg = tts_cfg.get("openai", {})
+ if isinstance(oai_cfg, dict):
+ base_url = (oai_cfg.get("base_url") or base_url).rstrip("/")
+ model = oai_cfg.get("model") or model
+ oai_voice = oai_cfg.get("voice") or oai_voice
+ except Exception:
+ pass
+
+ url = f"{base_url}/audio/speech"
+ req_body = json.dumps({
+ "model": model,
+ "input": text,
+ "voice": oai_voice,
+ }).encode("utf-8")
+
+ from urllib.request import Request, urlopen as _urlopen
+
+ req = Request(url, data=req_body, headers={
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json",
+ "Accept": "audio/mpeg",
+ })
+
+ audio_data = b""
+ try:
+ with _urlopen(req, timeout=30) as resp:
+ while True:
+ chunk = resp.read(65536)
+ if not chunk:
+ break
+ audio_data += chunk
+ except Exception:
+ logger.exception("OpenAI TTS generation failed")
+ from api.helpers import bad as _bad
+ return _bad(handler, "OpenAI TTS generation failed", 500)
+
+ handler.send_response(200)
+ handler.send_header("Content-Type", "audio/mpeg")
+ handler.send_header("Cache-Control", "no-store")
+ handler.send_header("Content-Length", str(len(audio_data)))
+ handler.end_headers()
+ try:
+ handler.wfile.write(audio_data)
+ except (BrokenPipeError, ConnectionResetError):
+ pass
+ return True
+
# ── Edge TTS ────────────────────────────────────────────────────────
allowed = {
"zh-CN-XiaoxiaoNeural", "zh-CN-XiaoyiNeural", "zh-CN-YunxiNeural",
diff --git a/static/boot.js b/static/boot.js
index 850d4e8b..a91bdac3 100644
--- a/static/boot.js
+++ b/static/boot.js
@@ -1290,6 +1290,46 @@ window._hermesTtsSynth=function(id, text, opts){
});
return;
}
+ if(engine==="openai"){
+ _ttsSpeaking=true;
+ fetch(new URL('api/tts', document.baseURI || location.href).href, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({text: clean, engine: 'openai'})
+ })
+ .then(r => {
+ if(!r.ok) throw new Error('TTS request failed: ' + r.status);
+ return r.blob();
+ })
+ .then(blob => {
+ const url = URL.createObjectURL(blob);
+ const audio = new Audio(url);
+ _playingEdgeAudio=audio;
+ audio.onended = () => {
+ _ttsSpeaking=false;
+ if(_playingEdgeAudio===audio) _playingEdgeAudio=null;
+ URL.revokeObjectURL(url);
+ if(_voiceModeActive) setTimeout(()=>_startListening(),500);
+ };
+ audio.onerror = () => {
+ _ttsSpeaking=false;
+ if(_playingEdgeAudio===audio) _playingEdgeAudio=null;
+ URL.revokeObjectURL(url);
+ if(_voiceModeActive) setTimeout(()=>_startListening(),1000);
+ };
+ audio.play().catch(() => {
+ _ttsSpeaking=false;
+ if(_playingEdgeAudio===audio) _playingEdgeAudio=null;
+ URL.revokeObjectURL(url);
+ if(_voiceModeActive) setTimeout(()=>_startListening(),1000);
+ });
+ })
+ .catch(() => {
+ _ttsSpeaking=false;
+ if(_voiceModeActive) setTimeout(()=>_startListening(),1000);
+ });
+ return;
+ }
if(engine==="edge"){
const voice=localStorage.getItem("hermes-tts-voice")||"zh-CN-XiaoxiaoNeural";
const savedRate=parseFloat(localStorage.getItem("hermes-tts-rate"));
diff --git a/static/index.html b/static/index.html
index 8287fc6a..34590dfb 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1209,7 +1209,7 @@
Record and send the original audio file to the agent instead of converting it to text first. The agent can then transcribe it or process the raw audio (emotion, background noise, custom STT). Like Telegram's voice message behavior.
-
Choose speech engine. Edge TTS uses Microsoft neural voices via the server.
+
Choose speech engine. Edge TTS uses Microsoft neural voices via the server.