server: add realtime transcription API (#30636)

* server: add realtime transcription API

* drop transcription_sessions

* proxy ws

* add docs

* server: let the API key middleware guard the websocket

httplib runs the pre-routing handler before the websocket upgrade, so
the API key middleware answers 401 at handshake like any other route.
This covers the router proxy, whose children run keyless, and accepts
the same key forms as HTTP (Bearer, raw key, X-Api-Key).

* server: keep realtime slots out of the prompt loop

A realtime slot waiting for audio is GENERATING with nothing to decode,
so it fell through the prompt loop and took slot_batched. A request on
a higher slot then failed can_batch_with on the task type and starved
for the whole session. Realtime slots have no prompt: the loop sets
them up once and always leaves them.

* server: start the realtime session once its slot is live

The audio stream was created in the prompt loop, which skips a slot
that cannot batch with the one generating, and session.created went out
before the task was posted: early appends failed and the reader quit,
leaving the socket open and the slot held until the HTTP timeout.

The slot now sets up its stream at launch and signals it with the begin
result, session.created waits for it, and a reader that stops closes
the socket so the session ends with it.

* server: name the committed item in input_audio_buffer.committed

Per the Realtime spec, item_id is the item made from the committed audio
and previous_item_id the one committed before it, null for the first.
committed now comes before the transcript that completes that item, and
both go out with the item switch under one lock, so a delta cannot land
between them.

* server: use the GA transcription session shape

The session object follows the GA Realtime schema: type transcription,
with the input format, transcription model, turn detection and noise
reduction under audio.input. The rate stays the model sample rate, which
is what the client must send for now.

---------

Co-authored-by: Pascal <admin@serveurperso.com>
This commit is contained in:
Xuan-Son NguyenandPascal authored and GitHub committed 2026-10-21 00:04:15 +02:00
1 parent 84e66d0321
commit 1242be66f6
9 files changed
+512 -14

No files matched your search

+51
View File
@@ -11,6 +11,7 @@
#include <memory>
#include <string>
#include <thread>
#include <mutex>
//
// HTTP implementation using cpp-httplib
@@ -744,6 +745,56 @@ void server_http_context::post(const std::string & path, const server_http_conte
}
}
void server_http_context::ws(const std::string & path, const server_http_context::ws_handler_t & handler) const {
const std::string full_path = path_prefix + path;
auto callback = [handler](const httplib::Request & req, httplib::ws::WebSocket & sock) {
struct conn_impl : server_http_context::ws_conn {
httplib::ws::WebSocket & sock;
std::mutex mutex_send;
conn_impl(httplib::ws::WebSocket & sock) : sock(sock) {}
int read(std::string & msg) override {
switch (sock.read(msg)) {
case httplib::ws::Text: return 1;
case httplib::ws::Binary: return 2;
default: return 0;
}
}
bool send(const std::string & msg) override {
std::lock_guard<std::mutex> lock(mutex_send);
return sock.send(msg);
}
bool send_binary(const std::string & msg) override {
std::lock_guard<std::mutex> lock(mutex_send);
return sock.send(msg.data(), msg.size());
}
bool is_open() const override {
return sock.is_open();
}
void close() override {
sock.close();
}
};
conn_impl conn(sock);
const std::function<bool()> should_stop = [&sock]() {
return !sock.is_open();
};
server_http_req request{
get_params(req),
get_headers(req),
req.path,
build_query_string(req),
req.body,
{},
should_stop
};
handler(request, conn);
};
for (const auto & srv : pimpl->servers) {
srv->WebSocket(full_path, callback);
}
}
void server_http_context::del(const std::string & path, const server_http_context::handler_t & handler) const {
handlers.emplace(path, handler);
auto callback = [handler](const httplib::Request & req, httplib::Response & res) {