82 lines
2.7 KiB
Swift
82 lines
2.7 KiB
Swift
//
|
|
// ElevenLabsTTSClient.swift
|
|
// leanring-buddy
|
|
//
|
|
// Streams text-to-speech audio from ElevenLabs and plays it back
|
|
// through the system audio output. Uses the streaming endpoint so
|
|
// playback begins before the full audio has been generated.
|
|
//
|
|
|
|
import AVFoundation
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class ElevenLabsTTSClient {
|
|
private let proxyURL: URL
|
|
private let session: URLSession
|
|
|
|
/// The audio player for the current TTS playback. Kept alive so the
|
|
/// audio finishes playing even if the caller doesn't hold a reference.
|
|
private var audioPlayer: AVAudioPlayer?
|
|
|
|
init(proxyURL: String) {
|
|
self.proxyURL = URL(string: proxyURL)!
|
|
|
|
let configuration = URLSessionConfiguration.default
|
|
configuration.timeoutIntervalForRequest = 30
|
|
configuration.timeoutIntervalForResource = 60
|
|
self.session = URLSession(configuration: configuration)
|
|
}
|
|
|
|
/// Sends `text` to ElevenLabs TTS and plays the resulting audio.
|
|
/// Throws on network or decoding errors. Cancellation-safe.
|
|
func speakText(_ text: String) async throws {
|
|
var request = URLRequest(url: proxyURL)
|
|
request.httpMethod = "POST"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("audio/mpeg", forHTTPHeaderField: "Accept")
|
|
|
|
let body: [String: Any] = [
|
|
"text": text,
|
|
"model_id": "eleven_flash_v2_5",
|
|
"voice_settings": [
|
|
"stability": 0.5,
|
|
"similarity_boost": 0.75
|
|
]
|
|
]
|
|
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
|
|
let (data, response) = try await session.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw NSError(domain: "ElevenLabsTTS", code: -1,
|
|
userInfo: [NSLocalizedDescriptionKey: "Invalid response"])
|
|
}
|
|
|
|
guard (200...299).contains(httpResponse.statusCode) else {
|
|
let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error"
|
|
throw NSError(domain: "ElevenLabsTTS", code: httpResponse.statusCode,
|
|
userInfo: [NSLocalizedDescriptionKey: "TTS API error (\(httpResponse.statusCode)): \(errorBody)"])
|
|
}
|
|
|
|
try Task.checkCancellation()
|
|
|
|
let player = try AVAudioPlayer(data: data)
|
|
self.audioPlayer = player
|
|
player.play()
|
|
print("🔊 ElevenLabs TTS: playing \(data.count / 1024)KB audio")
|
|
}
|
|
|
|
/// Whether TTS audio is currently playing back.
|
|
var isPlaying: Bool {
|
|
audioPlayer?.isPlaying ?? false
|
|
}
|
|
|
|
/// Stops any in-progress playback immediately.
|
|
func stopPlayback() {
|
|
audioPlayer?.stop()
|
|
audioPlayer = nil
|
|
}
|
|
}
|