Skip to content

feat(audio): add OpenAI-compatible /v1/audio/transcriptions endpoint - #405

Open
diegopzz wants to merge 1 commit into
su-kaka:masterfrom
diegopzz:diego/BYT-515-audio-transcriptions
Open

feat(audio): add OpenAI-compatible /v1/audio/transcriptions endpoint#405
diegopzz wants to merge 1 commit into
su-kaka:masterfrom
diegopzz:diego/BYT-515-audio-transcriptions

Conversation

@diegopzz

Copy link
Copy Markdown

What

Adds a Whisper-compatible speech-to-text endpoint on both channels:

Method Path Backend
POST /v1/audio/transcriptions geminicli
POST /antigravity/v1/audio/transcriptions antigravity

The uploaded file is turned into a Gemini inlineData part alongside a text prompt, and the request is handed to each backend's existing non_stream_request — so credential rotation, retries, auto-ban and error handling are inherited rather than reimplemented. The endpoint adds no new dependency (python-multipart is already required).

This ports the behaviour of Antigravity-Manager's handlers/audio.rs to this project.

API

multipart/form-data fields, matching OpenAI's transcription API:

  • file (required)
  • model, prompt, language, response_format, temperature

response_format supports json (default) and text. verbose_json / srt / vtt need per-segment timestamps, so they are rejected with a clear 400 instead of silently returning the wrong shape.

MIME type is resolved from the filename extension first and the declared Content-Type second, restricted to the formats Gemini documents (mp3, wav, m4a/aac, ogg/opus, flac, aiff). Unsupported containers such as webm fail with a message listing what is accepted. Inline uploads are capped at 15 MB, below the 20 MB request-body limit.

OpenAI SDK compatibility

Clients that send an OpenAI transcription model name (whisper-1, gpt-4o-transcribe, …) or omit model fall back to AUDIO_TRANSCRIPTION_MODEL (default gemini-2.5-flash), so an unmodified OpenAI SDK can be pointed at this service:

client = OpenAI(base_url="http://127.0.0.1:7861/antigravity/v1", api_key=API_PASSWORD)
client.audio.transcriptions.create(model="whisper-1", file=open("speech.mp3", "rb"))

Structure

  • src/converter/audio.py — pure helpers (MIME detection, size guard, request building, text extraction)
  • src/router/audio_common.py — shared FastAPI handler, one router per backend via build_audio_router()
  • src/router/{geminicli,antigravity}/audio.py — 3-line backend bindings
  • config.pyAUDIO_TRANSCRIPTION_MODEL / audio_transcription_model

Text extraction unwraps the v1internal {"response": {...}} envelope, joins all text parts and skips parts flagged thought, so thinking models do not leak reasoning into the transcript.

Testing

tests/test_audio.py — 9 unit tests over the pure helpers (MIME normalisation and fallback, unsupported-format rejection, size boundary, request shape, envelope unwrapping, malformed responses). All pass.

Verified live against the antigravity channel with a 6.4 s synthesised speech sample:

$ curl -s -X POST .../antigravity/v1/audio/transcriptions \
    -H "Authorization: Bearer ***" -F file=@speech.wav
{"text":"The quick brown fox jumps over the lazy dog. Gemini audio transcription test number 42."}

Also confirmed end to end:

  • whisper-1 + response_format=text200 text/plain with the transcript
  • filename without an extension → falls back to Content-Type200
  • unsupported webm400, oversized file → 413, empty file → 400, srt400, missing auth → 401
  • unknown model → upstream 404 body forwarded with its status
  • works with gemini-2.5-flash, gemini-3.5-flash-low, gemini-3.6-flash-medium, gemini-3.7-flash-tiered

The geminicli channel returned 403 SUBSCRIPTION_REQUIRED on my account, but /v1/chat/completions returns the same 403 there, so that is a credential-licensing condition rather than anything specific to this endpoint — the code path is identical to the antigravity one that passes.

Note: the pre-existing tests/test_gemini_fix.py fails to import on master (_ensure_empty_tool_schema_for_claude no longer exists in src/converter/gemini_fix.py). Untouched here, but it means pytest tests/ currently errors at collection.

Adds a Whisper-compatible speech-to-text endpoint on both the geminicli
and antigravity channels, porting the behaviour of Antigravity-Manager's
`src-tauri/src/proxy/handlers/audio.rs` to this project.

The uploaded file becomes a Gemini `inlineData` part alongside a text
prompt, and the request is handed to each backend's existing
`non_stream_request`, so credential rotation, retries, auto-ban and error
handling are inherited rather than reimplemented.

- POST /v1/audio/transcriptions            (geminicli)
- POST /antigravity/v1/audio/transcriptions (antigravity)

Form fields: file (required), model, prompt, language, response_format,
temperature. `response_format` supports json (default) and text; the
timestamp-based formats (verbose_json/srt/vtt) are rejected with a clear
400 rather than silently returning the wrong shape.

MIME type is resolved from the filename extension first and the declared
Content-Type second, restricted to the formats Gemini documents
(mp3/wav/aac/ogg/flac/aiff). Unsupported containers such as webm fail
with a message listing what is accepted. Inline uploads are capped at
15MB, below the 20MB request-body limit.

Clients that send an OpenAI transcription model name (whisper-1,
gpt-4o-transcribe, ...) or omit `model` fall back to
AUDIO_TRANSCRIPTION_MODEL (default gemini-2.5-flash), so an unmodified
OpenAI SDK can point at this service without changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012g4RaFC27E2zd7H15aizdE
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