Files
openwhispr/scripts/benchmark-orukeet.cjs
Nathan RollandChadpiha 67dee550db feat(dictation): stream Orukeet through managed Cloud sessions (#2231)
* feat(dictation): stream Orukeet through managed Cloud sessions

* Bound Orukeet capacity retry timers to the final deadline

* Select fixed local retry delays for Orukeet capacity backoff

* fix(dictation): Keep managed Orukeet rollouts recoverable on the desktop

Four gaps would have made the staged rollout unsafe to operate:

- Language gate: the managed route ignored the user's language and the
  wire protocol carries none, so a Japanese or Chinese user in the cohort
  would stream to a model that covers 25 languages. An explicitly selected
  language outside the registry list now takes the batch path, which
  honors the language end to end. "auto" keeps the model's own detection,
  as every other streaming provider does.
- Start-time fallback: a refused session (403 FEATURE_NOT_ENABLED, 429
  RATE_LIMITED, any 5xx) surfaced as a "Failed to start streaming" error
  and no recording started. It now starts a batch recording, the same
  path NO_API already took. Auth, policy, quota and upgrade denials still
  surface. Scoped to the managed Orukeet route.
- Config refresh: the STT config was fetched once and pinned for the life
  of the window, so a server-side rollback never reached a running app.
  A copy older than 15 minutes is refreshed in the background at recording
  start, and a FEATURE_NOT_ENABLED denial drops it so the next start
  refetches before deciding.
- Fallback telemetry: batch uploads from an eligible user carried no
  reason. They now send streamingFallbackReason (feature_disabled,
  rate_limited, session_unavailable, language_unsupported,
  stream_no_final) so the rollout's fallback rate is measurable.

Backend contract this relies on: the session route's cohort denial is
403 with code FEATURE_NOT_ENABLED and its mint limit is 429 with code
RATE_LIMITED.

* fix(dictation): Keep streaming fallback telemetry through cloud cleanup

With cloud cleanup on, the upload asks /api/transcribe not to log
(sendLogs=false) because /api/reason writes the combined STT log, but the
cleanup request never carried streamingFallbackReason, so every cleanup
user's streaming-to-batch fallback went untracked. Forward the reason
through cloud-reason into that combined log request.

Translation alone no longer suppresses the transcribe log. It writes no
combined STT log, so suppressing it dropped the row and its fallback
reason along with it.

* fix(dictation): Declare the Orukeet capability and refetch config uncached

The backend (OpenWhispr/openwhispr-api#218) now advertises and mints
Orukeet only for clients that send x-openwhispr-capabilities: orukeet,
since a version number cannot prove a build ships the Orukeet client: a
release without it would map the unknown provider to openai-realtime and
silently reroute dictation. Send the capability on every policy request
through withPolicyRequestHeaders.

Fetch cloud config with cache: "no-store", so the refetch after a refused
session cannot come back from Chromium's HTTP cache and re-advertise a
route the server already withdrew.

Tag stream_no_final only when the stream was Orukeet, so the rollout's
fallback rate is not mixed with other providers'. The batch fallback
itself still runs for every provider.

* fix(dictation): Fall back to batch on any Orukeet start failure

A managed Orukeet start only fell back to batch for FEATURE_NOT_ENABLED,
RATE_LIMITED and 5xx. A network error, the 10 s session timeout, a
WebSocket handshake failure or timeout, a rejected session body or a
cookie-only session instead showed a "Streaming Error" toast and lost the
recording, so an Orukeet outage past its token endpoint broke dictation for
the whole cohort.

Fall back as session_unavailable on every start failure except the denials
the user has to act on: expired or missing sign-in, an account switch
mid-start, account required, upgrade required, the word limit and any
workspace policy. Still scoped to the managed Orukeet route, so no other
provider changes. The fallback log line now carries the error message.

* fix(dictation): Show the word limit and upgrade prompt on an Orukeet start

At the weekly word limit the session route answers 429 with limitReached,
but the desktop dropped that flag, so a cohort user saw "Streaming Error:
Failed to start streaming: Weekly word limit reached" with no way to
upgrade.

Raise the same LIMIT_REACHED code the batch upload uses, carrying wordsUsed
and limit, so the error card gets the batch limit title and the server's
message, and open the existing upgrade prompt through notifyLimitReached. A
429 RATE_LIMITED from the mint cap is unchanged and still falls back to
batch.

* fix(dictation): Mint one Orukeet session per re-warm instead of two

Every managed Orukeet connection spends a single-use GPU token, and after
each dictation both re-warm paths (audioManager and useAudioRecording) call
dictation-realtime-warmup at once. The warmup always tore down the current
socket and reconnected, so the second call discarded the first's fresh
socket and minted again: two tokens per dictation, which also let a normal
dictation pace approach the 20-per-minute mint cap and fall back to batch.

Run warmups one at a time and have a warmup reuse a connected Orukeet socket
that has carried no audio and was opened for the same route, reporting
alreadyWarm. A socket that has carried audio is never reused. Other
providers keep reconnecting as before. The connection key that start,
connect and warmup compare now comes from one helper.

* Expose Orukeet audio language metadata for cloud fallback

* Bound Orukeet idle sessions and support account-bound tokens

---------

Co-authored-by: Chadpiha <chadpiha23@gmail.com>
2026-09-24 19:54:45 +02:00

119 lines
4.3 KiB
JavaScript

"use strict";
// Operator-only: this combines backend token issuance and desktop streaming.
// The shipped desktop receives a session from OpenWhispr's authenticated backend.
const fs = require("node:fs");
const { createHash } = require("node:crypto");
const { performance } = require("node:perf_hooks");
const { setTimeout: sleep } = require("node:timers/promises");
const { issueSession } = require("../docs/integrations/orukeet/issue-session.cjs");
const { OrukeetStreaming } = require("../src/helpers/orukeetStreaming.js");
function readPcmWav(file) {
const data = fs.readFileSync(file);
if (
data.length < 12 ||
data.toString("ascii", 0, 4) !== "RIFF" ||
data.toString("ascii", 8, 12) !== "WAVE"
)
throw new Error("Expected a WAV file");
let format, pcm;
for (let at = 12; at + 8 <= data.length;) {
const id = data.toString("ascii", at, at + 4);
const length = data.readUInt32LE(at + 4);
const start = at + 8;
if (start + length > data.length) throw new Error("Truncated WAV chunk");
if (id === "fmt " && length >= 16) {
format = [
data.readUInt16LE(start),
data.readUInt16LE(start + 2),
data.readUInt32LE(start + 4),
data.readUInt16LE(start + 14),
];
}
if (id === "data") pcm = data.subarray(start, start + length);
at = start + length + (length % 2);
}
if (JSON.stringify(format) !== "[1,1,16000,16]" || !pcm?.length || pcm.length % 2) {
throw new Error("Expected nonempty mono 16 kHz signed PCM16 WAV");
}
return pcm;
}
async function main() {
if (!process.argv[2])
throw new Error("Usage: node scripts/benchmark-orukeet.cjs audio.wav [expected.json]");
const pcm = readPcmWav(process.argv[2]);
const expected = process.argv[3] ? JSON.parse(fs.readFileSync(process.argv[3])).text : null;
const runs = Number(process.env.SMOKE_RUNS || 3);
if (!Number.isInteger(runs) || runs < 1 || runs > 10) throw new Error("SMOKE_RUNS must be 1-10");
const results = [];
for (let run = 0; run < runs; run++) {
const client = new OrukeetStreaming();
const sessionStart = performance.now();
const session = await issueSession({ serviceKey: process.env.ORUKEET_SERVICE_KEY });
const tokenIssueMs = performance.now() - sessionStart;
let region;
try {
const connectStart = performance.now();
const connected = client.connect({
baseUrl: session.baseUrl,
clientToken: session.clientToken,
});
client.ws.on("upgrade", (response) => {
region = response.headers["x-orukeet-region"];
});
await connected;
const connectionReadyMs = performance.now() - connectStart;
const recordingStart = performance.now();
for (let at = 0; at < pcm.length; at += 3200) {
// Send each frame only when capture would have produced its last sample.
// The final PCM frame is therefore queued immediately before commit.
const target = recordingStart + Math.min(at + 3200, pcm.length) / 32;
await sleep(Math.max(0, target - performance.now()));
client.sendAudio(pcm.subarray(at, at + 3200));
}
const stop = performance.now();
const final = await client.finalize();
const commitToFinalMs = performance.now() - stop;
if (expected !== null && final.text !== expected)
throw new Error("Fixture transcript mismatch");
results.push({
run: run + 1,
region,
tokenIssueMs,
connectionReadyMs,
audioSeconds: pcm.length / 32000,
commitToFinalMs,
inferenceMs: final.inferenceMs,
serverMs: final.serverMs,
queueMs: final.queueMs,
transcriptMatches: expected === null ? null : true,
transcriptSha256: createHash("sha256").update(final.text).digest("hex"),
});
} finally {
await client.disconnect();
}
}
console.log(
JSON.stringify(
{
measuredAt: new Date().toISOString(),
method:
"Paced PCM16 using the PR adapter; commit-to-final excludes token/connection setup and recording",
fixtureSha256: createHash("sha256").update(fs.readFileSync(process.argv[2])).digest("hex"),
results,
},
null,
2
)
);
}
if (require.main === module)
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
module.exports = { readPcmWav };