Update desktop app to android app capabilities.
This commit is contained in:
@@ -1,21 +1,26 @@
|
||||
"""Streaming speech-to-text — transcribing *while* you talk, not after.
|
||||
|
||||
The one-shot path (`stt.transcribe`) waits for the utterance to finish, then
|
||||
uploads the whole WAV and waits again. That second wait is dead time between
|
||||
sends the whole clip and waits again. That second wait is dead time between
|
||||
you stopping and the pet reacting, and it grows with the length of what you
|
||||
said — a thirty-second question costs noticeably more than a five-second one.
|
||||
|
||||
Deepgram's live endpoint removes it: frames go up as they are captured, so by
|
||||
the time the VAD decides you have stopped, the transcript is essentially
|
||||
already there. Same model, same account, same accuracy — the difference is
|
||||
purely when the work happens.
|
||||
This connects to the Bolt server's own `/desk/stt` — the same websocket relay
|
||||
the Android app uses — which forwards audio to Deepgram and Deepgram's JSON
|
||||
messages back untouched. Frames go up as they are captured, so by the time the
|
||||
VAD decides you have stopped, the transcript is essentially already there.
|
||||
There is no local Deepgram account or API key any more; auth is this pet's own
|
||||
`DESK_API_KEY`, same as every other call to the server.
|
||||
|
||||
Design constraints that shaped this:
|
||||
|
||||
- **Failure must be invisible.** No websocket, no network, a mid-utterance
|
||||
disconnect — all of it falls back to the one-shot path, which still has the
|
||||
full audio buffered. Streaming is an optimisation, never a dependency, so
|
||||
`open()` returning None is an ordinary outcome rather than an error.
|
||||
full audio buffered. Streaming is an optimisation here, never a dependency —
|
||||
`available()`/`open()` returning None/False is an ordinary outcome, not an
|
||||
error. `stt.transcribe()` (the *guaranteed* fallback) talks to the exact same
|
||||
server relay via `connect()` directly, bypassing that opportunistic gate,
|
||||
since there is no second, different backend left to fall back to.
|
||||
- **The VAD still decides when you stopped.** Deepgram has its own endpointing
|
||||
and using it would save more, but it would also move a decision the rest of
|
||||
the pipeline is built around (barge-in, follow-up listening, the grace
|
||||
@@ -42,16 +47,14 @@ from .. import config
|
||||
|
||||
logger = logging.getLogger("bolt_pet.stt_stream")
|
||||
|
||||
_ENDPOINT = (
|
||||
"wss://api.deepgram.com/v1/listen"
|
||||
"?encoding=linear16&channels=1&sample_rate={rate}&model={model}"
|
||||
"&language=en&smart_format=true&interim_results=false"
|
||||
)
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
"""Whether streaming STT can even be attempted in this install."""
|
||||
if not config.STT_STREAMING or not config.DEEPGRAM_API_KEY:
|
||||
"""Whether the streaming (transcribe-while-talking) optimization should
|
||||
be attempted opportunistically. Not a gate on transcription itself — the
|
||||
server relay is the only way to transcribe at all now, so
|
||||
stt.transcribe() connects via connect() directly rather than through
|
||||
this, and isn't affected by STT_STREAMING being off."""
|
||||
if not config.STT_STREAMING or not config.is_configured():
|
||||
return False
|
||||
try:
|
||||
import websocket # noqa: F401 (websocket-client)
|
||||
@@ -60,6 +63,33 @@ def available() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _connect(sample_rate: int = None):
|
||||
"""Open a websocket to the server's `/desk/stt` relay. Raises on any
|
||||
failure — this is the "no fallback left" connector `stt.transcribe()`
|
||||
uses directly, as well as the default for `StreamingTranscriber.open()`.
|
||||
|
||||
The server chooses the STT model and the endpointing behaviour; this
|
||||
only states the audio format about to be sent, which is fixed by the
|
||||
wake model upstream of it."""
|
||||
import websocket
|
||||
|
||||
rate = sample_rate or config.SAMPLE_RATE
|
||||
url = (
|
||||
config.SERVER_URL.replace("http", "ws", 1)
|
||||
+ "/desk/stt"
|
||||
+ f"?session_id={config.SESSION_ID}&encoding=linear16&sample_rate={rate}"
|
||||
)
|
||||
return websocket.create_connection(
|
||||
url, header=[f"X-Desk-Api-Key: {config.API_KEY}"], timeout=10,
|
||||
)
|
||||
|
||||
|
||||
# Public name for external callers (stt.py, tests) — named separately from
|
||||
# the module-private def so StreamingTranscriber.open()'s `connect` parameter
|
||||
# can shadow the bare name locally without losing access to this.
|
||||
connect = _connect
|
||||
|
||||
|
||||
class StreamingTranscriber:
|
||||
"""One utterance's worth of live transcription.
|
||||
|
||||
@@ -95,16 +125,7 @@ class StreamingTranscriber:
|
||||
return None
|
||||
rate = sample_rate or config.SAMPLE_RATE
|
||||
try:
|
||||
if connect is not None:
|
||||
socket = connect()
|
||||
else:
|
||||
import websocket
|
||||
|
||||
socket = websocket.create_connection(
|
||||
_ENDPOINT.format(rate=rate, model=config.DEEPGRAM_MODEL),
|
||||
header={"Authorization": f"Token {config.DEEPGRAM_API_KEY}"},
|
||||
timeout=10,
|
||||
)
|
||||
socket = connect() if connect is not None else _connect(rate)
|
||||
return cls(socket, sample_rate=rate)
|
||||
except Exception as exc:
|
||||
logger.info("Streaming STT unavailable (%s) — using the one-shot path.", exc)
|
||||
|
||||
Reference in New Issue
Block a user