Repository navigation
feat(server): audio endpoint plumbing (multipart upload, binary response, WAV output) - #368
Conversation
Phase 1 server foundation for the OpenAI-compatible audio API surface. Registers the speech and transcription/translation routes, parses their request shapes, and returns a structured 501 "audio model kind not loaded" until a speech model is wired in. No model is added here. Changes: - Add the `multipart` axum feature and parse the speech-to-text upload via `axum::extract::Multipart` (the file part plus the model, language, response_format, and temperature fields). - Register six routes in `create_app`: `/audio/speech`, `/audio/transcriptions`, and `/audio/translations`, each in both the unversioned and `/v1`-prefixed forms. - Add `src/server/routes/audio.rs` with the JSON text-to-speech handler (binary `audio/wav` body with a `Content-Disposition` attachment header) and the multipart speech-to-text handlers, reusing `can_accept_request` admission control, the request metrics, and `ErrorResponse`. - Define a generic audio-model seam in `src/server/audio_model.rs`: an `AudioModelKind` enum plus an `AudioModelProvider` trait with independent transcribe and synthesize methods, so the speech-to-text and text-to-speech work plugs in without reworking the route layer. Add an `Option` provider slot to `AppState`, left `None` in this phase. - Add `AudioSpeechRequest` and `AudioTranscriptionRequest` request types and an `AudioTranscriptionResponse`, plus an `ErrorResponse::not_implemented` (501) constructor. - Add a PCM16 little-endian RIFF WAV writer (`encode_wav_pcm16`) in `src/audio/wav_writer.rs` as the counterpart to the existing reader, with a unit test that round-trips encoded audio back through `load_wav_from_bytes`. - Note the new endpoint families in `docs/architecture.md`.
Raise the request body limit on the six /audio/* routes to 25 MiB through a dedicated sub-router carrying DefaultBodyLimit, so realistic audio uploads are not rejected by Axum's 2 MiB default before the handler runs. The outer auth, CORS, and trace layers still apply to the merged routes. Reject unsupported transcription response_format values (for example srt, vtt, or a typo) with a 400 instead of silently returning JSON, matching the speech path's resolve_audio_format strictness. The check stays on the post-provider success path so the Phase 1 501 still dominates for every format. Add unit tests that drive the multipart parser with real multipart/form-data bytes (file part round-trip, scalar fields, empty and invalid temperature, unknown-part draining, missing file), add a deserialize test for AudioTranscriptionRequest, and fix the self-contradictory doc comment on that type.
encode_wav_pcm16 truncated samples.len() to u32 before the saturating multiply, so an input larger than u32::MAX samples advertised a RIFF data length that no longer matched the bytes written, producing a corrupt header. Cap the encoded sample count to what a 32-bit RIFF data length can represent so the advertised length and the emitted payload always agree. Behavior for normal inputs is unchanged. Add regression tests covering header and payload length consistency and confirming that pathological f32 input (NaN, infinities, out of range) saturates without panicking.
Security and performance review: Phase 1 audio plumbingReviewed the new audio surface: the multipart upload and JSON parsing in Findings by severity
Confirmed safe (no change needed)
Verification
Note (no action this phase)
No CRITICAL or HIGH items remain. The PR was not merged. |
Seven tests in server::app::tests covering the Phase 1 audio route surface that the existing per-module tests could not reach because they only exercise internal functions, not the router itself. - audio_upload_limit_is_25_mib: verifies AUDIO_MAX_UPLOAD_BYTES == 25 MiB. - audio_speech_is_reachable_at_v1_path, audio_transcriptions_is_reachable_at_v1_path, audio_translations_is_reachable_at_v1_path: confirm each v1 path returns non-404 for POST. - get_to_audio_speech_returns_method_not_allowed: 405 (not 404) proves the route is mounted. - audio_alias_paths_are_reachable_without_v1_prefix: checks the /audio/* alias set. - body_limit_layer_enforces_upload_cap: confirms DefaultBodyLimit is wired onto the audio sub-router; uses a small test limit to avoid allocating 25 MiB in the test binary.
New docs/audio-api.md documents the three endpoint families added in this PR: /v1/audio/speech (TTS), /v1/audio/transcriptions, and /v1/audio/translations (STT). Covers request/response shape, supported fields, error codes, Phase 1 501 behavior, the ordering of parse-before-model-check, WAV encoding details, and how to wire in a future AudioModelProvider. docs/README.md updated to list audio-api.md in the GitHub-facing docs index.
PR Finalization CompleteTestsAdded 7 tests to
Existing test counts unchanged: 16 in DocumentationNew
Lint/Format
|
Audio I/O on the OpenAI /v1/audio/* surface (Whisper speech-to-text #371, Kokoro-82M text-to-speech #374, endpoint plumbing #368), reasoning_content on non-streaming chat (#359), pre-M5 decode +8-12% via a hardware-gated op cap (#360), Turbo4Asym decode rerouted through dequant-SDPA (#369), batched sampling fixes (#339, #340, #344, #356), and the Turbo KV advisor/docs aligned to the measured sweep with ADR 0002 (#343, #376). README: consolidate the v0.3.0 and v0.3.1 highlights under a single v0.3 entry and add v0.3.2.
Closes lablup#364. Phase 1 of the OpenAI audio endpoints epic (lablup#363): routes for /v1/audio/{speech,transcriptions,translations}, multipart upload parsing, binary audio responses, a WAV PCM16 writer, and the AudioModelProvider seam returning 501 until the STT and TTS models land.
Audio I/O on the OpenAI /v1/audio/* surface (Whisper speech-to-text lablup#371, Kokoro-82M text-to-speech lablup#374, endpoint plumbing lablup#368), reasoning_content on non-streaming chat (lablup#359), pre-M5 decode +8-12% via a hardware-gated op cap (lablup#360), Turbo4Asym decode rerouted through dequant-SDPA (lablup#369), batched sampling fixes (lablup#339, lablup#340, lablup#344, lablup#356), and the Turbo KV advisor/docs aligned to the measured sweep with ADR 0002 (lablup#343, lablup#376). README: consolidate the v0.3.0 and v0.3.1 highlights under a single v0.3 entry and add v0.3.2.
Summary
Phase 1 server plumbing for the OpenAI-compatible audio API surface (part of epic #363). This registers the speech and transcription/translation routes, parses their request shapes (JSON for text-to-speech, multipart/form-data for speech-to-text), and adds the audio-output WAV encoder. No speech model is implemented here; every audio route returns a structured HTTP 501 "audio model kind not loaded" until the model sub-issues land. The audio-model interface is deliberately generic so the speech-to-text and text-to-speech work plugs in without reworking the server surface.
What changed
multipartaxum feature and a multipart parser for the speech-to-text upload (thefilepart plus themodel,language,response_format, andtemperaturefields) viaaxum::extract::Multipart.create_app(src/server/app.rs):/audio/speech,/audio/transcriptions, and/audio/translations, each in both the unversioned and/v1-prefixed forms.src/server/routes/audio.rs: the JSON text-to-speech handler returns a binaryaudio/wavbody with aContent-Disposition: attachment; filename=speech.wavheader; the multipart speech-to-text handlers parse the upload and return OpenAI transcription JSON. All handlers reusestate.can_accept_request()admission control, the existing request metrics, andErrorResponse.src/server/audio_model.rs: anAudioModelKind { Stt, Tts }enum plus anAudioModelProvidertrait whosetranscribeandsynthesizemethods default to a "kind not loaded" error, so each direction can be implemented independently. Added anOption<Arc<dyn AudioModelProvider>>slot toAppState(Nonein this phase) with awith_audio_modelbuilder.src/server/types/:AudioSpeechRequest,AudioTranscriptionRequest, andAudioTranscriptionResponse, plus anErrorResponse::not_implemented(HTTP 501) constructor.src/audio/wav_writer.rswithencode_wav_pcm16, a PCM16 little-endian RIFF WAV encoder that is the counterpart to the existing reader. The reader is reused, not duplicated.docs/architecture.md.Behavior in this phase
All six routes are mounted and reachable. The text-to-speech route parses its JSON body and the speech-to-text routes parse their multipart body through the real request flow; with no model loaded each returns a structured HTTP 501 with the message "audio model kind not loaded". The synthesize and transcribe code paths (binary WAV response, transcription JSON) are written and compile so the model sub-issues only need to supply an
AudioModelProvider.Test plan
cargo check --lib --tests --features metal,acceleratecargo clippy --lib --tests --features metal,accelerate -- -D warnings(clean)cargo fmt --check(clean)cargo test --lib --features metal,accelerate audio::wav_writer(WAV encode round-trips throughload_wav_from_bytes)cargo test --lib --features metal,accelerate server::audio_modelcargo test --lib --features metal,accelerate server::routes::audioCloses #364