Update desktop app to android app capabilities.
This commit is contained in:
+33
-37
@@ -1,15 +1,24 @@
|
||||
"""Text-to-speech: ElevenLabs, requested as raw PCM so playback is just
|
||||
"""Text-to-speech: the Bolt server's own `/desk/tts` — the same endpoint the
|
||||
Android app streams from — requested as raw PCM so playback is just
|
||||
sounddevice — no external player binary (mpv/ffplay), unlike
|
||||
desk_client/bolt_desk.py which shells out because it only targets Linux.
|
||||
|
||||
Falls back to pyttsx3 (offline, cross-platform: SAPI5 on Windows, NSSpeech
|
||||
on macOS, espeak on Linux) if ElevenLabs isn't configured or the request
|
||||
fails, so the pet can still talk with zero cloud config.
|
||||
No local ElevenLabs account needed for this: the server picks the voice
|
||||
(ELEVENLABS_VOICE_ID, or a `speak_as` override) and the synthesis model
|
||||
itself, authenticated with this pet's own DESK_API_KEY. Falls back to
|
||||
pyttsx3 (offline, cross-platform: SAPI5 on Windows, NSSpeech on macOS,
|
||||
espeak on Linux) if the server call fails, so the pet can still talk even
|
||||
with the server unreachable.
|
||||
|
||||
Every entry point takes an optional *voice_id* that overrides
|
||||
`ELEVENLABS_VOICE_ID` for that call — that's how the server's `speak_as`
|
||||
reply marker reaches the speakers (see controller._apply_voice). The offline
|
||||
fallback has no such concept and always sounds like itself.
|
||||
|
||||
`synthesize_dialogue()` below is the one exception: multi-voice
|
||||
`dialoguectl` scenes have no server endpoint, so that one call still goes
|
||||
to ElevenLabs' Text to Dialogue API directly and still needs
|
||||
ELEVENLABS_API_KEY — see dialogue.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -33,19 +42,8 @@ def voice_for(voice_id: Optional[str] = None) -> str:
|
||||
return (voice_id or "").strip() or config.ELEVENLABS_VOICE_ID
|
||||
|
||||
|
||||
def model_for(text: str, voice_id: Optional[str] = None) -> str:
|
||||
"""Which ElevenLabs model to synthesize with.
|
||||
|
||||
The default (`eleven_flash_v2`) is English-only, and both things that
|
||||
reach this branch mean the reply probably isn't English: a voice the
|
||||
server picked mid-conversation is nearly always about a language or an
|
||||
accent, and non-ASCII text can't be English at all. Rendering either one
|
||||
through the English model gets you a mangled phonetic reading rather
|
||||
than a failure, which is worse — so those go through the multilingual
|
||||
model instead."""
|
||||
if (voice_id or "").strip() or not text.isascii():
|
||||
return config.ELEVENLABS_MULTILINGUAL_MODEL_ID
|
||||
return config.ELEVENLABS_MODEL_ID
|
||||
def _headers() -> dict:
|
||||
return {"X-Desk-Api-Key": config.API_KEY}
|
||||
|
||||
|
||||
def synthesize_pcm(text: str, voice_id: Optional[str] = None) -> tuple[np.ndarray, int]:
|
||||
@@ -53,48 +51,46 @@ def synthesize_pcm(text: str, voice_id: Optional[str] = None) -> tuple[np.ndarra
|
||||
callers should fall back to speak_offline() rather than treating this
|
||||
as fatal."""
|
||||
voice = voice_for(voice_id)
|
||||
if not (config.ELEVENLABS_API_KEY and voice):
|
||||
raise TtsError("ELEVENLABS_API_KEY / ELEVENLABS_VOICE_ID not set")
|
||||
if not (config.is_configured() and voice):
|
||||
raise TtsError("BOLT_SERVER_URL / DESK_API_KEY / ELEVENLABS_VOICE_ID not set")
|
||||
try:
|
||||
response = requests.post(
|
||||
f"https://api.elevenlabs.io/v1/text-to-speech/{voice}",
|
||||
headers={"xi-api-key": config.ELEVENLABS_API_KEY},
|
||||
params={"output_format": f"pcm_{config.TTS_SAMPLE_RATE}"},
|
||||
json={"text": text, "model_id": model_for(text, voice_id)},
|
||||
f"{config.SERVER_URL}/desk/tts",
|
||||
headers=_headers(),
|
||||
json={"session_id": config.SESSION_ID, "text": text, "voice_id": voice},
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except Exception as exc:
|
||||
raise TtsError(f"ElevenLabs request failed: {exc}") from exc
|
||||
raise TtsError(f"server tts request failed: {exc}") from exc
|
||||
pcm = np.frombuffer(response.content, dtype=np.int16)
|
||||
if pcm.size == 0:
|
||||
raise TtsError("ElevenLabs returned no audio")
|
||||
raise TtsError("server returned no audio")
|
||||
return pcm, config.TTS_SAMPLE_RATE
|
||||
|
||||
|
||||
def stream_pcm(
|
||||
text: str, chunk_bytes: int = 4096, voice_id: Optional[str] = None
|
||||
) -> Iterator[np.ndarray]:
|
||||
"""Same audio as synthesize_pcm(), but yielded as it arrives from
|
||||
ElevenLabs' /stream endpoint so playback can start on the first chunk
|
||||
(~300ms) instead of after the whole clip is synthesized. Raises TtsError
|
||||
before yielding anything if the request itself fails, so callers can fall
|
||||
back cleanly; a mid-stream failure just ends the generator."""
|
||||
"""Same audio as synthesize_pcm(), but yielded as it arrives from the
|
||||
server so playback can start on the first chunk instead of after the
|
||||
whole clip is synthesized. Raises TtsError before yielding anything if
|
||||
the request itself fails, so callers can fall back cleanly; a mid-stream
|
||||
failure just ends the generator."""
|
||||
voice = voice_for(voice_id)
|
||||
if not (config.ELEVENLABS_API_KEY and voice):
|
||||
raise TtsError("ELEVENLABS_API_KEY / ELEVENLABS_VOICE_ID not set")
|
||||
if not (config.is_configured() and voice):
|
||||
raise TtsError("BOLT_SERVER_URL / DESK_API_KEY / ELEVENLABS_VOICE_ID not set")
|
||||
try:
|
||||
response = requests.post(
|
||||
f"https://api.elevenlabs.io/v1/text-to-speech/{voice}/stream",
|
||||
headers={"xi-api-key": config.ELEVENLABS_API_KEY},
|
||||
params={"output_format": f"pcm_{config.TTS_SAMPLE_RATE}"},
|
||||
json={"text": text, "model_id": model_for(text, voice_id)},
|
||||
f"{config.SERVER_URL}/desk/tts",
|
||||
headers=_headers(),
|
||||
json={"session_id": config.SESSION_ID, "text": text, "voice_id": voice},
|
||||
timeout=60,
|
||||
stream=True,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except Exception as exc:
|
||||
raise TtsError(f"ElevenLabs stream request failed: {exc}") from exc
|
||||
raise TtsError(f"server tts stream request failed: {exc}") from exc
|
||||
return chunks_to_int16(response.iter_content(chunk_size=chunk_bytes))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user