Skip to content

Commit 1e057d5

Browse files
committed
Add audio chat API support for gpt-4o-audio-preview model
Implements audio-to-audio chat completion replacing the traditional STT→Chat→TTS pipeline with a single API call for 2-3x faster response times. Includes streaming support, AudioConversationManager utility for multi-turn conversations with history tracking, and comprehensive test coverage.
1 parent 4ed3ea9 commit 1e057d5

14 files changed

Lines changed: 2072 additions & 4 deletions

README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ This repository contains Swift community-maintained implementation over [OpenAI]
3232
- [Audio Create Speech](#audio-create-speech)
3333
- [Audio Transcriptions](#audio-transcriptions)
3434
- [Audio Translations](#audio-translations)
35+
- [Audio Chat](#audio-chat-gpt-4o-audio-preview)
3536
- [Structured Outputs](#structured-outputs)
3637
- [Specialized models](#specialized-models)
3738
- [Embeddings](#embeddings)
@@ -735,6 +736,131 @@ openAI.audioTranslations(query: query) { result in
735736
let result = try await openAI.audioTranslations(query: query)
736737
```
737738

739+
### Audio Chat (gpt-4o-audio-preview)
740+
741+
The Audio Chat API enables audio-to-audio conversations with GPT-4o Audio models. This replaces the traditional STTChatTTS pipeline with a single API call, providing 2-3x faster response times and better voice quality.
742+
743+
**Supported Models:** `gpt-4o-audio-preview`, `gpt-4o-mini-audio-preview`
744+
745+
**Important Format Requirements:**
746+
- **Input audio formats:** Only `wav` and `mp3` are supported
747+
- **Output audio formats:** `wav`, `mp3`, `flac`, `opus`, `pcm16`
748+
- **Recommended for streaming:** Use `pcm16` for output to get optimal streaming performance
749+
750+
**Request:**
751+
752+
```swift
753+
public struct AudioChatQuery: Codable, Equatable, Streamable, Sendable {
754+
public let model: Model
755+
public let messages: [Message]
756+
public let modalities: [String]? // ["text", "audio"]
757+
public let audio: AudioConfig?
758+
public var stream: Bool
759+
760+
public struct AudioConfig {
761+
public let voice: Voice // .alloy, .echo, .fable, .onyx, .nova, .shimmer
762+
public let format: AudioFormat // .wav, .mp3, .flac, .opus, .pcm16
763+
}
764+
765+
public struct Message {
766+
public let role: ChatQuery.ChatCompletionMessageParam.Role
767+
public let content: Content // .text(String) or .parts([ContentPart])
768+
}
769+
}
770+
```
771+
772+
**Response:**
773+
774+
```swift
775+
public struct AudioChatResult: Codable, Equatable, Sendable {
776+
public let id: String
777+
public let choices: [Choice]
778+
779+
public struct Choice {
780+
public let message: Message
781+
}
782+
783+
public struct Message {
784+
public let content: String? // Text transcript
785+
public let audio: AudioOutput? // Base64-encoded audio data
786+
}
787+
788+
public struct AudioOutput {
789+
public let data: String // Base64-encoded audio
790+
public let transcript: String
791+
}
792+
}
793+
```
794+
795+
**Example (Non-Streaming):**
796+
797+
```swift
798+
let audioData = Data(contentsOf: audioFileURL)
799+
let base64Audio = audioData.base64EncodedString()
800+
801+
let query = AudioChatQuery(
802+
model: .gpt_4o_audio_preview,
803+
messages: [
804+
.init(role: .system, content: .text("You are a helpful voice assistant.")),
805+
.init(role: .user, content: .parts([
806+
.init(inputAudio: .init(data: base64Audio, format: .wav))
807+
]))
808+
],
809+
modalities: ["text", "audio"],
810+
audio: .init(voice: .alloy, format: .pcm16)
811+
)
812+
813+
let result = try await openAI.audioChats(query: query)
814+
if let audioOutput = result.choices.first?.message.audio,
815+
let audioData = Data(base64Encoded: audioOutput.data) {
816+
// Use audioData and audioOutput.transcript
817+
}
818+
```
819+
820+
**Example (Streaming):**
821+
822+
```swift
823+
for try await chunk in openAI.audioChatsStream(query: query) {
824+
if let audioDelta = chunk.choices.first?.delta.audio?.data,
825+
let audioChunk = Data(base64Encoded: audioDelta) {
826+
// Play audio chunk in real-time
827+
}
828+
}
829+
```
830+
831+
**AudioConversationManager Utility:**
832+
833+
The SDK includes a convenient `AudioConversationManager` actor for managing multi-turn conversations with automatic history tracking:
834+
835+
```swift
836+
let manager = AudioConversationManager(
837+
openAI: openAI,
838+
systemPrompt: "You are a helpful voice assistant.",
839+
maxHistoryTurns: 10
840+
)
841+
842+
// Send audio and get audio response
843+
let (audioData, transcript) = try await manager.sendAudio(
844+
audioData,
845+
audioFormat: .wav,
846+
voice: .alloy,
847+
responseFormat: .pcm16
848+
)
849+
850+
// Send text and get audio response
851+
let (audioData, transcript) = try await manager.sendText(
852+
"What's the weather like?",
853+
voice: .alloy,
854+
responseFormat: .pcm16
855+
)
856+
857+
// Get conversation transcript
858+
let transcript = manager.getTranscript()
859+
860+
// Reset conversation
861+
manager.reset()
862+
```
863+
738864
Review [Audio Documentation](https://platform.openai.com/docs/api-reference/audio) for more info.
739865

740866
## Structured Outputs

Sources/OpenAI/OpenAI+OpenAIAsync.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,21 @@ extension OpenAI: OpenAIAsync {
105105
request: makeAudioTranslationsRequest(query: query)
106106
)
107107
}
108-
108+
109+
public func audioChats(query: AudioChatQuery) async throws -> AudioChatResult {
110+
try await performRequestAsync(
111+
request: makeAudioChatsRequest(query: query)
112+
)
113+
}
114+
115+
public func audioChatsStream(
116+
query: AudioChatQuery
117+
) -> AsyncThrowingStream<AudioChatStreamResult, Error> {
118+
makeAsyncStream { onResult, completion in
119+
audioChatsStream(query: query, onResult: onResult, completion: completion)
120+
}
121+
}
122+
109123
public func assistants() async throws -> AssistantsResult {
110124
try await assistants(after: nil)
111125
}

Sources/OpenAI/OpenAI.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,19 @@ final public class OpenAI: OpenAIProtocol, @unchecked Sendable {
330330
public func audioTranslations(query: AudioTranslationQuery, completion: @escaping @Sendable (Result<AudioTranslationResult, Error>) -> Void) -> CancellableRequest {
331331
performRequest(request: makeAudioTranslationsRequest(query: query), completion: completion)
332332
}
333-
333+
334+
public func audioChats(query: AudioChatQuery, completion: @escaping @Sendable (Result<AudioChatResult, Error>) -> Void) -> CancellableRequest {
335+
performRequest(request: makeAudioChatsRequest(query: query.makeNonStreamable()), completion: completion)
336+
}
337+
338+
public func audioChatsStream(query: AudioChatQuery, onResult: @escaping @Sendable (Result<AudioChatStreamResult, Error>) -> Void, completion: (@Sendable (Error?) -> Void)?) -> CancellableRequest {
339+
performStreamingRequest(
340+
request: JSONRequest<AudioChatStreamResult>(body: query.makeStreamable(), url: buildURL(path: .chats)),
341+
onResult: onResult,
342+
completion: completion
343+
)
344+
}
345+
334346
public func audioCreateSpeech(query: AudioSpeechQuery, completion: @escaping @Sendable (Result<AudioSpeechResult, Error>) -> Void) -> CancellableRequest {
335347
performSpeechRequest(request: makeAudioCreateSpeechRequest(query: query), completion: completion)
336348
}

Sources/OpenAI/Private/OpenAI+MakeRequest.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ extension OpenAI {
4848
func makeAudioTranslationsRequest(query: AudioTranslationQuery) -> MultipartFormDataRequest<AudioTranslationResult> {
4949
.init(body: query, url: buildURL(path: .audioTranslations))
5050
}
51-
51+
52+
func makeAudioChatsRequest(query: AudioChatQuery) -> JSONRequest<AudioChatResult> {
53+
.init(body: query, url: buildURL(path: .chats))
54+
}
55+
5256
func makeAudioCreateSpeechRequest(query: AudioSpeechQuery) -> JSONRequest<AudioSpeechResult> {
5357
.init(body: query, url: buildURL(path: .audioSpeech))
5458
}

0 commit comments

Comments
 (0)