Skip to content

Add OpenAI-compatible POST /v1/audio/transcriptions route - #50

Open
sumgup0 wants to merge 2 commits into
paulbrav:masterfrom
sumgup0:openai-endpoint
Open

Add OpenAI-compatible POST /v1/audio/transcriptions route#50
sumgup0 wants to merge 2 commits into
paulbrav:masterfrom
sumgup0:openai-endpoint

Conversation

@sumgup0

@sumgup0 sumgup0 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

This adds an OpenAI-compatible transcription route on the existing local
service so any standard OpenAI client/frontend can use TransClip as its
transcription backend (base_url=http://127.0.0.1:8765/v1; the SDK appends
/audio/transcriptions).

Contract (deliberate compat subset): multipart file (formats = what
the bundled soundfile/libsndfile decodes — WAV/FLAC/OGG/mp3 on libsndfile
1.2.2; m4a/webm unsupported, no ffmpeg); model/language/prompt/
temperature accepted and ignored (single configured backend serves, the
local-OpenAI-server norm); response_format json (default, {"text"})
or text (text/plain); anything else 400. OpenAI error JSON shape on this
route only. 25 MB cap (26_214_400, OpenAI's own limit) enforced against
Content-Length BEFORE the body is read — which also gives the server the
request-size bound it previously lacked on this path. Streaming,
verbose_json/srt/vtt, and /v1/audio/translations are explicitly out.

Dictation path untouched: the route is a self-contained branch in
do_POST (after _guard(), before the JSON dispatch and its exception
wrapper) with its own writer and its own error handling — the internal
{"error": str, "debug_capture_dir": ...} shape is unreachable from it
(pinned by test). The only shared-surface changes: a thin
InferenceEngine.transcribe_raw() (raw ASR — no cleanup/voice-mode/
history/debug-capture: OpenAI semantics) and authorization added to the
CORS allow-headers (browser OpenAI clients preflight it; grants nothing —
the loopback-origin guard still gates everything).

Safety details: undecodable audio → 400 via soundfile pre-validation in
the route, with a static message (libsndfile's exception text embeds the
server temp path — never echoed; same discipline for the 500 path, both
pinned by tests). Uploaded-filename suffix sanitized before the tempfile
call. A module-level lock serializes THIS route's backend calls; note the
pre-existing service posture (no inference lock across /transcribe /
dictation) is unchanged — flagging rather than fixing in a thin PR.

The over-limit path drains the request body (bounded, in chunks) before
sending the 413, so a real oversize upload receives a clean status instead
of a connection reset that an SDK would retry.

Tests: 29 unit (multipart parser incl. UTF-8/RFC 2231 filenames, binary

  • trailing-CRLF + stray-Content-Transfer-Encoding round-trips, sanitizer,
    handler with fakes) + 10 socket round trips (json/text/400s, a real
    oversize upload → clean 413, 411, 500-no-leak, 403, preflight) via the
    existing serve_test_engine harness + a new multipart helper. Full suite:
    490 OK (5 skipped) at this branch.

One pre-existing quirk noticed, not touched (exists at ba25cb0): the
ruff format non-compliance in server.py's _guard/_cors blocks (and
_worker_loop). Also, on Windows the pre-existing _guard early-reject
path (403 before the body is read) can occasionally reset the connection
before a client with a body in flight reads the response — the new route's
own reject paths drain first to avoid this, but I left the shared _guard
untouched in a thin PR; happy to apply the same drain there if you'd like.

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.

1 participant