The official Swift library for the Typecast API. Convert text to lifelike speech using AI-powered voices.
Compatible with Swift 5.9+ and supports all Apple platforms: iOS, macOS, tvOS, watchOS, and visionOS.
Requirements
| Platform | Minimum Version |
|---|---|
| iOS | 13.0+ |
| macOS | 10.15+ |
| tvOS | 13.0+ |
| watchOS | 6.0+ |
| visionOS | 1.0+ |
| Swift | 5.9+ |
Installation
Clone the tagged release, then reference its Swift package directory locally:
git clone --branch typecast-swift/v0.3.10 --depth 1 https://github.com/neosapience/typecast-sdk.git
dependencies: [
.package(path: "typecast-sdk/typecast-swift")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "Typecast", package: "typecast-swift")
]
)
]
In Xcode, use File → Add Package Dependencies... → Add Local... and select the cloned typecast-swift directory.
Quick Start
import AVFoundation
import Typecast
let client = TypecastClient(apiKey: "YOUR_API_KEY")
var audioPlayer: AVAudioPlayer?
// Simple usage with convenience method
let audio = try await client.speak(
"Hello! I'm your friendly text-to-speech assistant.",
voiceId: "tc_672c5f5ce59fac2a48faeaee"
)
// Play audio directly from data
audioPlayer = try AVAudioPlayer(data: audio.audioData)
audioPlayer?.play()
print("Duration: \(audio.duration)s, Format: \(audio.format.rawValue)")
Features
The Typecast Swift SDK provides powerful features for text-to-speech conversion:
- Multiple Voice Models: Support for
ssfm-v30(latest) andssfm-v21AI voice models - Multi-language Support: 37 languages including English, Korean, Spanish, Japanese, Chinese, and more
- Emotion Control: Preset emotions (normal, happy, sad, angry, whisper, toneup, tonedown) or smart context-aware inference
- Audio Customization: Control loudness (LUFS -70 to 0), pitch (-12 to +12 semitones), tempo (0.5x to 2.0x), and format (WAV/MP3)
- Voice Discovery: V2 Voices API with filtering by model, gender, age, and use cases
- Instant Voice Cloning: Upload a WAV/MP3 sample and create a custom voice ID
- Swift Concurrency: Full async/await support for modern Swift development
- Thread-Safe: All types conform to
Sendablefor safe concurrent usage - Timestamp TTS: Word- and character-level alignment data for subtitles, karaoke, and lip-sync
- Cross-Platform: Works on iOS, macOS, tvOS, watchOS, and visionOS
- Streaming: Real-time chunked audio delivery for low-latency playback
Voice Recommendations
Use recommendVoices when you know the desired style but not the exact voice_id.
let voices = try await client.recommendVoices(
query: "warm female voice for a product tutorial",
count: 3
)
for voice in voices {
print("\(voice.voiceId) \(voice.voiceName) \(voice.score)")
}
Recommendation results contain only voiceId, voiceName, and score. Use getVoice(voiceId:) or getVoices(filter:) when you need detailed metadata such as supported models, emotions, gender, age, or use cases.
Configuration
Initialize the client with your API key:
import Typecast
// Direct initialization
let client = TypecastClient(apiKey: "your-api-key")
// With custom base URL
let client = TypecastClient(
apiKey: "your-api-key",
baseURL: "https://api.typecast.ai"
)
// Using configuration struct
let config = TypecastConfiguration(apiKey: "your-api-key")
let client = TypecastClient(configuration: config)
let client = TypecastClient(
baseURL: "https://your-proxy.example.com"
)
Advanced Usage
Emotion Control (ssfm-v30)
ssfm-v30 offers two emotion control modes: Preset and Smart.
Let the AI infer emotion from context:
let request = TTSRequest(
voiceId: "tc_672c5f5ce59fac2a48faeaee", // Find voice IDs at https://studio.typecast.ai/developers/api/voices
text: "Everything is going to be okay.",
model: .ssfmV30,
prompt: .smart(SmartPrompt(
previousText: "I just got the best news!", // Optional context
nextText: "I can't wait to celebrate!" // Optional context
))
)
let response = try await client.textToSpeech(request)
audioPlayer = try AVAudioPlayer(data: response.audioData)
audioPlayer?.play()
Audio Customization
Control loudness, pitch, tempo, and output format:
let request = TTSRequest(
voiceId: "tc_672c5f5ce59fac2a48faeaee",
text: "Customized audio output!",
model: .ssfmV30,
output: OutputSettings(
targetLufs: -14.0, // Range: -70 to 0 (LUFS)
audioPitch: 2, // Range: -12 to +12 semitones
audioTempo: 1.2, // Range: 0.5x to 2.0x
audioFormat: .mp3 // Options: .wav, .mp3
),
seed: 42 // Unsigned seed for reproducible results
)
let response = try await client.textToSpeech(request)
audioPlayer = try AVAudioPlayer(data: response.audioData)
audioPlayer?.play()
print("Duration: \(response.duration)s, Format: \(response.format.rawValue)")
Generate audio to a file
Use generateToFile when you want the SDK to synthesize speech and write the audio bytes directly to a local file. The model defaults to ssfm-v30, and .mp3 / .wav extensions infer the output format when no output format is set. Browse available voice IDs on the Voices page.
try await client.generateToFile(
"output.mp3",
request: GenerateToFileRequest(
voiceId: "tc_672c5f5ce59fac2a48faeaee", // Find voice IDs at https://studio.typecast.ai/developers/api/voices
text: "Hello from Typecast."
)
)
Text pauses
Use text pause markup when you only need silent gaps inside one composed text segment. Put <|5s|>, <|1s|>, <|0.3s|>, or <|0.34413s|> directly in the text. The value is interpreted as seconds and must end with s. This keeps the pause expression visible in plain text without adding separate pause calls.
let audio = try await client.composeSpeech()
.defaults(ComposerSettings(voiceId: "tc_672c5f5ce59fac2a48faeaee", model: .ssfmV30))
.say("Hello<|5s|>Nice to meet you<|1s|>Today<|2s|>how does the weather feel?")
.generate()
Multi-speaker composition
Use the composer chaining API when one output file needs different voices or per-segment options such as pitch, tempo, prompt, or seed. The composer generates each segment as WAV, trims leading/trailing silent PCM samples, and concatenates the result. If you need MP3, generate WAV first and convert it in your app or server pipeline.
let audio = try await client.composeSpeech()
.defaults(ComposerSettings(voiceId: "tc_672c5f5ce59fac2a48faeaee", model: .ssfmV30))
.say("Hello there")
.pause(5)
.say("Nice to meet you", overrides: ComposerSettings(
voiceId: "tc_60e5426de8b95f1d3000d7b5",
output: OutputSettings(audioPitch: 2)
))
.say("Today")
.pause(2)
.say("How does the weather feel?")
.generate()
try audio.audioData.write(to: URL(fileURLWithPath: "conversation.wav"))
Voice Discovery (V2 API)
List and filter available voices with enhanced metadata:
// Get all voices
let voices = try await client.getVoices()
// Filter by criteria
let filteredVoices = try await client.getVoices(filter: VoicesV2Filter(
model: .ssfmV30,
gender: .female,
age: .youngAdult
))
// Get a specific voice
let voice = try await client.getVoice(voiceId: "tc_672c5f5ce59fac2a48faeaee")
// Display voice info
print("ID: \(voice.voiceId), Name: \(voice.voiceName)")
print("Gender: \(voice.gender?.rawValue ?? "N/A"), Age: \(voice.age?.rawValue ?? "N/A")")
for model in voice.models {
print("Model: \(model.version.rawValue), Emotions: \(model.emotions.joined(separator: ", "))")
}
if let useCases = voice.useCases {
print("Use cases: \(useCases.joined(separator: ", "))")
}
Multilingual Content
The SDK supports 37 languages with automatic language detection:
// Auto-detect language (recommended)
let request = TTSRequest(
voiceId: "tc_672c5f5ce59fac2a48faeaee",
text: "こんにちは。お元気ですか。",
model: .ssfmV30
)
let response = try await client.textToSpeech(request)
// Or specify language explicitly
let koreanRequest = TTSRequest(
voiceId: "tc_672c5f5ce59fac2a48faeaee",
text: "안녕하세요. 반갑습니다.",
model: .ssfmV30,
language: .korean // Explicit language code
)
let koreanResponse = try await client.textToSpeech(koreanRequest)
audioPlayer = try AVAudioPlayer(data: koreanResponse.audioData)
audioPlayer?.play()
Streaming
Stream audio chunks in real-time for low-latency playback:
import AVFoundation
import Typecast
let engine = AVAudioEngine()
let playerNode = AVAudioPlayerNode()
let format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 32000, channels: 1, interleaved: true)!
engine.attach(playerNode)
engine.connect(playerNode, to: engine.mainMixerNode, format: format)
try engine.start()
playerNode.play()
let stream = try await client.textToSpeechStream(request)
var first = true
for try await chunk in stream {
var pcmData = chunk
if first {
pcmData = chunk.dropFirst(44) // Skip 44-byte WAV header
first = false
}
let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(pcmData.count / 2))!
buffer.frameLength = buffer.frameCapacity
pcmData.withUnsafeBytes { ptr in
buffer.int16ChannelData!.pointee.update(from: ptr.bindMemory(to: Int16.self).baseAddress!, count: Int(buffer.frameLength))
}
playerNode.scheduleBuffer(buffer)
}
Timestamp TTS
textToSpeechWithTimestamps() wraps POST /v1/text-to-speech/with-timestamps and returns the audio together with per-word and per-character alignment data - useful for karaoke highlights, subtitle generation, and lip-sync applications.
Basic Usage
import Typecast
let client = TypecastClient(apiKey: "YOUR_API_KEY")
let request = TTSRequestWithTimestamps(
voiceId: "tc_60e5426de8b95f1d3000d7b5",
text: "Hello. How are you?",
model: .ssfmV30
)
let result = try await client.textToSpeechWithTimestamps(request)
audioPlayer = try AVAudioPlayer(data: result.audioData)
audioPlayer?.play()
print("Duration: \(result.audioDuration)s")
for word in result.words {
print(" [\(word.startTime)s – \(word.endTime)s] \(word.text)")
}
Granularity
Pass granularity: .word (default) or granularity: .char to control the alignment unit.
let request = TTSRequestWithTimestamps(
voiceId: "tc_60e5426de8b95f1d3000d7b5",
text: "Hello. How are you?",
model: .ssfmV30,
granularity: .char // required for Japanese / Chinese
)
Subtitle Export
let srt = result.toSrt()
print(srt)
let vtt = result.toVtt()
print(vtt)
Instant Voice Cloning
Clone a custom voice from a short audio sample, then pass the returned uc_ voice ID directly to TTS.
import AVFoundation
import Foundation
import Typecast
let client = TypecastClient(apiKey: "YOUR_API_KEY")
var audioPlayer: AVAudioPlayer?
let audioData = try Data(contentsOf: URL(fileURLWithPath: "sample.wav"))
let voice = try await client.cloneVoice(
audio: audioData,
filename: "sample.wav",
name: "My Voice",
model: "ssfm-v30"
)
let response = try await client.speak(
"Hello from my cloned voice!",
voiceId: voice.voiceId
)
audioPlayer = try AVAudioPlayer(data: response.audioData)
audioPlayer?.play()
try await client.deleteVoice(voice.voiceId)
Supported Languages
The SDK supports 37 languages with automatic language detection:
| Code | Language | Code | Language | Code | Language |
|---|---|---|---|---|---|
.english | English | .japanese | Japanese | .ukrainian | Ukrainian |
.korean | Korean | .greek | Greek | .indonesian | Indonesian |
.spanish | Spanish | .tamil | Tamil | .danish | Danish |
.german | German | .tagalog | Tagalog | .swedish | Swedish |
.french | French | .finnish | Finnish | .malay | Malay |
.italian | Italian | .chinese | Chinese | .czech | Czech |
.polish | Polish | .slovak | Slovak | .portuguese | Portuguese |
.dutch | Dutch | .arabic | Arabic | .bulgarian | Bulgarian |
.russian | Russian | .croatian | Croatian | .romanian | Romanian |
.bengali | Bengali | .hindi | Hindi | .hungarian | Hungarian |
.minNan | Hokkien | .norwegian | Norwegian | .punjabi | Punjabi |
.thai | Thai | .turkish | Turkish | .vietnamese | Vietnamese |
.cantonese | Cantonese |
Error Handling
The SDK provides a comprehensive TypecastError enum for handling API errors:
import Typecast
do {
let response = try await client.textToSpeech(request)
} catch let error as TypecastError {
switch error {
case .unauthorized(let message):
// 401: Invalid API key
print("Invalid API key: \(message)")
case .paymentRequired(let message):
// 402: Insufficient credits
print("Insufficient credits: \(message)")
case .notFound(let message):
// 404: Resource not found
print("Voice not found: \(message)")
case .validationError(let message):
// 422: Validation error
print("Validation error: \(message)")
case .rateLimitExceeded(let message):
// 429: Rate limit exceeded
print("Rate limit exceeded: \(message)")
case .serverError(let message):
// 500: Server error
print("Server error: \(message)")
case .networkError(let underlyingError):
// Network connectivity issues
print("Network error: \(underlyingError.localizedDescription)")
case .invalidResponse(let message):
// Invalid response from server
print("Invalid response: \(message)")
default:
print("Error: \(error.localizedDescription)")
}
// Access status code if available
if let statusCode = error.statusCode {
print("HTTP Status: \(statusCode)")
}
}
Error Types
| Error | Status Code | Description |
|---|---|---|
.badRequest | 400 | Invalid request parameters |
.unauthorized | 401 | Invalid or missing API key |
.paymentRequired | 402 | Insufficient credits |
.notFound | 404 | Resource not found |
.validationError | 422 | Validation error |
.rateLimitExceeded | 429 | Rate limit exceeded |
.serverError | 500 | Server error |
.networkError | - | Network connectivity issues |
.invalidResponse | - | Invalid response from server |
Platform-Specific Usage
iOS
import Typecast
import AVFoundation
class TTSManager {
private let client = TypecastClient(apiKey: "YOUR_API_KEY")
private var audioPlayer: AVAudioPlayer?
func speak(_ text: String) async throws {
let audio = try await client.speak(text, voiceId: "tc_672c5f5ce59fac2a48faeaee")
// Play audio directly from data
audioPlayer = try AVAudioPlayer(data: audio.audioData)
audioPlayer?.play()
}
}
macOS
import Typecast
import AppKit
import AVFoundation
class MacTTSManager {
private let client = TypecastClient(apiKey: "YOUR_API_KEY")
private var audioPlayer: AVAudioPlayer?
func speak(_ text: String) async throws {
let audio = try await client.speak(text, voiceId: "tc_672c5f5ce59fac2a48faeaee")
audioPlayer = try AVAudioPlayer(data: audio.audioData)
audioPlayer?.play()
}
}
watchOS
import Typecast
import AVFoundation
class WatchTTSManager {
private let client = TypecastClient(apiKey: "YOUR_API_KEY")
private var audioPlayer: AVAudioPlayer?
func speak(_ text: String) async throws {
let audio = try await client.speak(text, voiceId: "tc_672c5f5ce59fac2a48faeaee")
audioPlayer = try AVAudioPlayer(data: audio.audioData)
audioPlayer?.play()
}
}
API Reference
TypecastClient Methods
| Method | Description |
|---|---|
textToSpeech(_:) | Convert text to speech audio |
generateToFile(_:request:) | Generate speech and save it directly to a local file |
speak(_:voiceId:model:) | Simple TTS with minimal parameters |
speak(_:voiceId:model:emotion:intensity:) | TTS with emotion preset |
cloneVoice(audio:filename:name:model:) | Create a custom voice via instant cloning |
cloneVoice(audioFileURL:name:model:) | Create a custom voice from a local audio file |
deleteVoice(_:) | Delete a custom cloned voice |
getVoices(filter:) | Get available voices with optional filter |
getVoice(voiceId:) | Get a specific voice by ID |
TTSRequest Fields
| Field | Type | Required | Description |
|---|---|---|---|
voiceId | String | ✓ | Voice ID (format: tc_* or uc_*) |
text | String | ✓ | Text to synthesize (max 2000 chars) |
model | TTSModel | ✓ | TTS model (.ssfmV21 or .ssfmV30) |
language | LanguageCode | Language code (auto-detected if omitted) | |
prompt | TTSPrompt | Emotion settings (.basic, .preset, or .smart) | |
output | OutputSettings | Audio output settings | |
seed | UInt32 | Unsigned integer seed for reproducibility (≥ 0) |
TTSResponse Fields
| Field | Type | Description |
|---|---|---|
audioData | Data | Generated audio data |
duration | TimeInterval | Audio duration in seconds |
format | AudioFormat | Audio format (.wav or .mp3) |