Kodio 0.2.0 Help

Web transcription via a backend proxy

This page explains how to use Kodio's OpenAIWhisperEngine from JS / WasmJS targets without exposing your OpenAI key in the browser.

The problem

Browsers refuse the cross-origin request to api.openai.com because:

  1. OpenAI's API does not include Access-Control-Allow-Origin headers in the response.

  2. Embedding an API key in browser requests would leak it to anyone who opens DevTools.

This affects live recording transcription on JS and WasmJS targets. Native targets (JVM, Android, iOS, macOS) are unaffected because their HTTP stacks are not bound by the browser CORS policy.

The fix: configure endpointUrl

OpenAIWhisperEngine accepts an optional endpointUrl and additionalHeaders:

  • endpointUrl: the URL the engine will POST audio chunks to. Defaults to OpenAI's public endpoint; override it to point at your backend.

  • additionalHeaders: extra headers appended to every request, for example an app-level token your backend uses to authenticate the call.

When endpointUrl is overridden you can pass an empty apiKey and the engine will skip adding the Authorization: Bearer ... header. Your backend should hold the OpenAI key.

val engine = OpenAIWhisperEngine( apiKey = "", endpointUrl = "https://my-app.example.com/api/transcribe", additionalHeaders = headersOf("X-App-Token", "browser-secret"), chunkDurationSeconds = 3, ) recorder.audioFlow?.transcribe(engine, TranscriptionConfig(language = "en-US")) ?.collect { result -> /* ... */ }

Live transcription

Create an engine and pipe a live AudioFlow into it:

val engine = OpenAIWhisperEngine(apiKey = "sk-...") val recorder = Kodio.recorder() recorder.start() recorder.audioFlow?.transcribe(engine, TranscriptionConfig.Default) ?.collect { result -> when (result) { is TranscriptionResult.Partial -> updateUI(result.text) is TranscriptionResult.Final -> saveTranscript(result.text) is TranscriptionResult.Error -> showError(result.message) } } recorder.stop()

TranscriptionConfig only exposes language (default "en-US") and the Default preset.

Transcribing a saved recording

There is no Kodio.transcribe(file) API. Load the recording, convert it to an AudioFlow, and pass it to the engine:

val recording = AudioRecording.fromBytes(wavBytes) val engine = OpenAIWhisperEngine(apiKey = "sk-...") recording.asFlow().let { flow -> AudioFlow(recording.format, flow).transcribe(engine, TranscriptionConfig.Default) }.collect { result -> when (result) { is TranscriptionResult.Final -> println(result.text) is TranscriptionResult.Error -> println(result.message) else -> Unit } }

On web, prefer posting file bytes to your backend proxy rather than calling OpenAI directly.

Example proxy: Ktor server

A minimal forwarder running on :8080 that takes the same multipart payload the engine produces and forwards it to OpenAI:

fun main() { embeddedServer(Netty, port = 8080) { install(ContentNegotiation) { json() } install(CORS) { anyHost() // tighten this to your front-end origin in production allowHeader(HttpHeaders.ContentType) allowHeader("X-App-Token") allowMethod(HttpMethod.Post) } val openAiKey = System.getenv("OPENAI_API_KEY") val client = HttpClient(CIO) routing { post("/api/transcribe") { if (call.request.headers["X-App-Token"] != "browser-secret") { call.respond(HttpStatusCode.Unauthorized); return@post } val body = call.receiveChannel().toByteArray() val response = client.post("https://api.openai.com/v1/audio/transcriptions") { header(HttpHeaders.Authorization, "Bearer $openAiKey") header(HttpHeaders.ContentType, call.request.contentType().toString()) setBody(body) } call.respondBytes( bytes = response.bodyAsBytes(), contentType = ContentType.Application.Json, status = response.status, ) } } }.start(wait = true) }

The same pattern works on Cloudflare Workers, Vercel/Netlify edge functions, or any other serverless runtime where you can hold a secret and forward a multipart POST.

Last modified: 03 July 2026