fix: dictation cloud path — bearer subprotocol, access_token grant field
Two live-verified Deepgram facts from the first real mic-test round:
- /v1/auth/grant returns { access_token }, not { token } — the route now
parses the right field.
- Minted JWTs authenticate over the WebSocket via the "bearer"
subprotocol; ["token", jwt] is rejected at the handshake (token is
for raw API keys only, as in the pi dictate extension).
Also documents the env-precedence gotcha in docs/research/stt.md: a
DEEPGRAM_API_KEY exported in the shell (e.g. .zshrc) silently beats
.env.local in next dev and the desktop authEnv() seam.
This commit is contained in:
@@ -35,11 +35,11 @@ export async function GET() {
|
|||||||
{ status: 502 },
|
{ status: 502 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const data = (await res.json()) as { token?: string };
|
const data = (await res.json()) as { access_token?: string };
|
||||||
if (!data.token) {
|
if (!data.access_token) {
|
||||||
return Response.json({ message: "Deepgram grant returned no token" }, { status: 502 });
|
return Response.json({ message: "Deepgram grant returned no token" }, { status: 502 });
|
||||||
}
|
}
|
||||||
return Response.json({ token: data.token });
|
return Response.json({ token: data.access_token });
|
||||||
} catch {
|
} catch {
|
||||||
return Response.json({ message: "Deepgram unreachable" }, { status: 502 });
|
return Response.json({ message: "Deepgram unreachable" }, { status: 502 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,10 @@ export function useDictation({ append }: { append: (text: string) => void }): Di
|
|||||||
/** Open the Deepgram socket and wire its messages. `gen` guards staleness. */
|
/** Open the Deepgram socket and wire its messages. `gen` guards staleness. */
|
||||||
const openSocket = useCallback(
|
const openSocket = useCallback(
|
||||||
(gen: number, token: string) => {
|
(gen: number, token: string) => {
|
||||||
const ws = new WebSocket(DG_URL, ["token", token]);
|
// Minted JWTs authenticate via the "bearer" subprotocol — ["token",
|
||||||
|
// jwt] is rejected at the handshake (verified live; "token" is for
|
||||||
|
// raw API keys only, as in the owner's pi dictate extension).
|
||||||
|
const ws = new WebSocket(DG_URL, ["bearer", token]);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.addEventListener("open", () => {
|
ws.addEventListener("open", () => {
|
||||||
|
|||||||
@@ -30,8 +30,18 @@ against our codebase. Decisions at the bottom.
|
|||||||
|
|
||||||
**Alternatives (why still Deepgram)**: OpenAI live-transcribe $0.017/min (~3.5×), no word timestamps/confidence, 60-min session cap. AssemblyAI Universal-Streaming $0.0025/min is cheaper but smaller ecosystem. Speechmatics best for non-English but ~2×. GCP is service-account-shaped — bad BYOK fit. Verdict: Deepgram stays the pragmatic default.
|
**Alternatives (why still Deepgram)**: OpenAI live-transcribe $0.017/min (~3.5×), no word timestamps/confidence, 60-min session cap. AssemblyAI Universal-Streaming $0.0025/min is cheaper but smaller ecosystem. Speechmatics best for non-English but ~2×. GCP is service-account-shaped — bad BYOK fit. Verdict: Deepgram stays the pragmatic default.
|
||||||
|
|
||||||
|
## Cloud: Deepgram (the default) — landed
|
||||||
|
|
||||||
|
**Live-verified facts (2026-09-17, cloud wave)**
|
||||||
|
|
||||||
|
- The **grant response field is `access_token`**, not `token` — `POST /v1/auth/grant` returns `{ "access_token": "<jwt>" }`.
|
||||||
|
- **Minted JWTs authenticate over the WebSocket via the `bearer` subprotocol**: `new WebSocket(url, ["bearer", jwt])`. The `token` subprotocol is for **raw API keys only** (the pi dictate extension's path) — `["token", jwt]` is rejected at the handshake (TLS-level close, no 401).
|
||||||
|
- **The key needs ≥ Member permission** (console → API Keys → Create Key → *Advanced* options → permissions dropdown → Member). A default key can transcribe but gets `FORBIDDEN` from `/auth/grant`.
|
||||||
|
- **Env precedence gotcha**: a real `DEEPGRAM_API_KEY` exported in the shell (e.g. `.zshrc`) **beats `.env.local`** in both `next dev` and the desktop shell's `authEnv()` seam. Stale exports silently override the file — don't export the key in shell rc files; `.env.local`/auth.json are the canonical seams.
|
||||||
|
|
||||||
## Local: whisper.cpp (the Advanced option)
|
## Local: whisper.cpp (the Advanced option)
|
||||||
|
|
||||||
|
|
||||||
**Verified facts**
|
**Verified facts**
|
||||||
|
|
||||||
- **Ship the whisper.cpp CLI (v1.9.4, MIT) as a child process** — the 2026 community norm; every maintained Node wrapper (nodejs-whisper, whisper-node) is just a CLI spawner anyway, and native bindings (smart-whisper last published Oct 2024; the official `addon.node` is a self-compiled reference demo) add electron-rebuild/ABI churn for nothing. Our server already spawns children; per-utterance spawn is fine (model load <1s, stateless).
|
- **Ship the whisper.cpp CLI (v1.9.4, MIT) as a child process** — the 2026 community norm; every maintained Node wrapper (nodejs-whisper, whisper-node) is just a CLI spawner anyway, and native bindings (smart-whisper last published Oct 2024; the official `addon.node` is a self-compiled reference demo) add electron-rebuild/ABI churn for nothing. Our server already spawns children; per-utterance spawn is fine (model load <1s, stateless).
|
||||||
|
|||||||
Reference in New Issue
Block a user