Skip to content

feat(transcription): OpenAI-compatible cloud ASR engine (from #349) - #401

Merged
ruzin merged 5 commits into
mainfrom
feat/cloud-asr
Jul 23, 2026
Merged

feat(transcription): OpenAI-compatible cloud ASR engine (from #349)#401
ruzin merged 5 commits into
mainfrom
feat/cloud-asr

Conversation

@ruzin

@ruzin ruzin commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an OpenAI-compatible cloud transcription engine: alongside Parakeet and Whisper, you can point Steno at any OpenAI-compatible /audio/transcriptions endpoint (API URL + model + key). Extracted from @Vassista's #349, adapted to the current design, with the API-key storage reworked to match the app's safeStorage convention.

Security fix vs. the original PR

#349 wrote the ASR API key plaintext into config.json. This PR does not. The key is handled exactly like the cloud-summariser key:

  • Encrypted at rest via safeStorage in a .openai-asr-api-key file under the user-data dir (saveOpenAiAsrKey/loadOpenAiAsrKey/hasOpenAiAsrKey, with legacy-file migration).
  • Decrypted only in the main process and injected into the transcription subprocess env as STENOAI_OAI_API_KEY (new getTranscriptionEnv(), applied at both the batch and live-sidecar spawn sites).
  • Python reads it env-only (get_openai_asr_api_key()os.environ); there is no plaintext setter and no CLI path that takes the key as an argument. config.json only ever holds the non-secret URL + model.
  • The UI shows the key only as set/not-set (api_key_set, overridden by hasOpenAiAsrKey()); the value is never returned to the renderer. Clearing removes the file.

Other hardening (ported)

  • _run_openai_asr blocks cross-origin redirects (NoRedirectHandler) so the Bearer token can't leak to a redirected host; verbose_jsontext fallback.
  • Diagnostics redaction for the config channel.

UI

Re-homed onto the current AiTab engine picker (the PR's TranscriptionTab.tsx no longer exists) — a "Cloud API" option behind a privacy confirm dialog, plus an API URL / model / key sub-panel using the current SettingRow primitives. No settings-layout change.

Tests

  • New e2e/specs/cloud-asr-config.t2.spec.ts: sets/gets the non-secret config (persists to config.json) and asserts the key flips api_key_set true without the key value ever landing in config.json. Model-free (no network/real ASR).
  • typecheck:renderer, ipc-contract (7/7), and the Python unittest suite green locally. The new T2 needs the freshly-built backend bundle (new CLI commands), which CI builds — it validates there.

Credit: @Vassista (#349).


Summary by cubic

Adds an OpenAI-compatible cloud transcription engine (openai-asr) so you can send audio to any /audio/transcriptions endpoint, with safe, encrypted key storage and a simple settings UI. Also fixes diarised ordering for text-only endpoints and lets cloud ASR run without local model imports.

  • New Features

    • New openai-asr engine alongside Parakeet and Whisper; Python posts audio to /audio/transcriptions with verbose_json then text fallback and same-origin redirect protection.
    • API key stored via safeStorage, encrypted at rest in .openai-asr-api-key, injected as STENOAI_OAI_API_KEY; never in config.json or argv.
    • Settings > Transcription adds a “Cloud API” option (privacy confirm) with fields for API base URL and model, plus a key input that only shows api_key_set.
    • App wiring: env is injected for live sidecar and batch jobs; IPC/preload add get-openai-asr-config, set-openai-asr-config, set-openai-asr-key; diagnostics redact set-openai-asr-config args.
    • Tests: new e2e/specs/cloud-asr-config.t2.spec.ts verifies config round-trip and encrypted key storage.
  • Bug Fixes

    • Cloud ASR no longer requires Parakeet/Whisper imports; runs with pure HTTP even if local backends are unavailable.
    • Diarised transcript: correct speaker ordering for text-only endpoints and omit fake [00:00] timestamps when no real timing exists.
    • Config hardening: reject blank API URL/model; UI clears now reset to defaults. Additional unit tests cover these cases.

Written for commit da1fd68. Summary will update on new commits.

Review in cubic

ruzin and others added 4 commits July 23, 2026 19:32
Add an `openai-asr` transcription engine that POSTs audio to any
OpenAI-compatible /audio/transcriptions endpoint (stdlib urllib only:
verbose_json with a plain-text fallback, and a NoRedirectHandler that blocks
credential leaks to cross-origin redirects). Return shape matches the
parakeet/whisper backends so the pipeline is unchanged.

Security: the API key is read from STENOAI_OAI_API_KEY (env only), mirroring
get_cloud_api_key, and is NEVER persisted to config.json. Only the non-secret
api_url/model are stored. New CLI commands get-openai-asr-config /
set-openai-asr-config handle url/model only — no key is ever accepted via argv.

Co-authored-by: Vassista <vassista@icloud.com>
…tion env

Mirror the cloud summariser key pattern for the OpenAI ASR key: encrypted at
rest under getUserDataDir() (.openai-asr-api-key, honouring the e2e data-dir
override), migrated forward from the legacy path, and NEVER written to
config.json or argv. It is decrypted only to inject STENOAI_OAI_API_KEY into
the transcription subprocess env — both the batch process-streaming job and the
live transcribe-stream sidecar (getTranscriptionEnv).

Add IPC handlers: get/set-openai-asr-config (non-secret url/model, shells to the
CLI like set-cloud-api-url) with api_key_set overridden by the safeStorage
truth (hasOpenAiAsrKey), and set-openai-asr-key (saves, or clears on empty
string). Teach loadTranscriptionEngine/Context about the new engine, and redact
set-openai-asr-config args from the diagnostics echo (the url may embed
credentials; the key never travels via argv).

Co-authored-by: Vassista <vassista@icloud.com>
Add the `openaiAsr` preload bridge (getConfig/setConfig/setKey), its typed
mirror in ipc.ts (+ the 'openai-asr' TranscriptionEngine member), and the
useOpenAiAsrConfig / useSetOpenAiAsrConfig / useSetOpenAiAsrKey hooks.

In AiTab's engine picker, add a "Cloud API" option gated behind a privacy
ConfirmDialog (audio leaves the device), and render an OpenAI config sub-panel:
API base URL + model (save-on-blur) and an API-key input that shows only a
set/not-set state (via api_key_set, never the value) with save-on-blur and a
clear affordance. The section intro copy becomes engine-aware so the
on-device privacy promise isn't shown for the cloud engine. Keep the e2e mock
IPC contract in sync.

Co-authored-by: Vassista <vassista@icloud.com>
Drive the openaiAsr preload bridge to round-trip the non-secret url/model
through config.json, and assert the API key is stored encrypted (safeStorage)
under the temp dir — the plaintext key never appears in config.json — and that
clearing removes the file and flips api_key_set back to false. Model-free: no
real ASR endpoint is ever contacted. Skips loudly where safeStorage is
unavailable (headless runner), mirroring the cloud-key guard in ai-provider.t2.

Co-authored-by: Vassista <vassista@icloud.com>
@ruzin
ruzin requested a review from Optic00 as a code owner July 23, 2026 18:34

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files

You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/transcriber.py
Comment thread src/transcriber.py
Comment thread src/config.py
Comment thread src/config.py Outdated
Comment thread app/renderer/src/routes/settings/AiTab.tsx Outdated
… reset

Address cubic review on the cloud ASR PR:

- transcriber: don't require bundled local ASR (parakeet/whisper) imports
  when the user selected openai-asr. Cloud is a pure-Python urllib REST call
  and needs no local model, so the ImportError guard now runs only for the
  on-device engines.
- transcriber: fix diarised speaker ordering for text-only (non-verbose_json)
  endpoints. Those return no per-segment timestamps (a synthetic start=end=0
  whole-channel segment), so sorting by start always emitted [You] before
  [Others]. Now only sort when a channel carries real timing, keep timeless
  segments in insertion order after any timed content, and omit the fabricated
  [00:00] timestamp for timeless turns rather than implying a chronology.
- config: get_transcription_engine docstring now documents openai-asr.
- config: set_openai_asr_api_url rejects blank/whitespace URLs (mirrors
  set_openai_asr_model), keeping the prior value instead of persisting an
  unusable empty base URL.
- AiTab: clearing the API URL / model field on blur now resets to the default
  (persisted) rather than leaving a stale value that returns on refresh.
- tests: cover the text-only diarised ordering and blank URL/model rejection.

Co-authored-by: Vassista <vassista@icloud.com>
@Optic00

Optic00 commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Heads-up: my review of this PR was finalised just as it merged, so the findings went into issues instead - #403 carries two I'd treat as release-blocking for the feature (meetings over ~13 min fail against the 25 MB upload limit; unvalidated 200 responses can be mistaken for silence and delete the only audio when Save recordings is off), and #404 covers the privacy-docs contradiction. The safeStorage key handling here is exemplary - the issues are about the request path, not the credential story.

ruzin added a commit that referenced this pull request Jul 23, 2026
An empty config.json.lock (a runtime lock from Python config writes) was
accidentally committed in #401. Remove it and gitignore *.json.lock so it
can't be re-added.

Co-authored-by: ruzin <ruzin113@icloud.com>
ruzin added a commit that referenced this pull request Jul 24, 2026
Reverts #401 (cloud transcription) and drops it from the pending v0.6.4 notes. Notifications (#402) and announcer (#409) preserved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants