> ## Documentation Index
> Fetch the complete documentation index at: https://typecast.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Swift

The official Swift library for the [Typecast API](https://typecast.ai). 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.

<CardGroup cols={2}>
<Card title="Swift Package" icon="cube" href="https://github.com/neosapience/typecast-sdk/tree/main/typecast-swift">
Typecast Swift SDK
</Card>

<Card title="Source Code" icon="github" href="https://github.com/neosapience/typecast-sdk/tree/main/typecast-swift">
Typecast Swift SDK Source Code
</Card>
</CardGroup>

## 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:

```bash
git clone --branch typecast-swift/v0.3.14 --depth 1 https://github.com/neosapience/typecast-sdk.git
```

```swift
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.

<Warning>
Latest registered version: **typecast-swift/v0.3.14** in the SDK Git tags. Make sure you have **Swift 5.9 or higher** installed. The SDK uses Swift Concurrency (async/await) which requires this minimum version.
</Warning>

## Quick Start

```swift
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) and `ssfm-v21` AI voice models
- **Multi-language Support**: 35+ 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 `Sendable` for 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`.

```swift
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:

```swift
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)
```

<Info>
When requests go through your own proxy, set `baseURL` to the proxy endpoint and omit `apiKey`. The SDK will not send the `X-API-KEY` header for nil or empty keys. Requests to the default Typecast host still require an API key.
</Info>

```swift Proxy without API key
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**.

<Tabs>
  <Tab title="Smart Mode">
    Let the AI infer emotion from context:

    ```swift
    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()
    ```
  </Tab>

  <Tab title="Preset Mode">
    Explicitly set emotion with preset values:

    ```swift
    let request = TTSRequest(
        voiceId: "tc_672c5f5ce59fac2a48faeaee",
        text: "I am so excited to show you these features!",
        model: .ssfmV30,
        prompt: .preset(PresetPrompt(
            emotionPreset: .happy,  // normal, happy, sad, angry, whisper, toneup, tonedown
            emotionIntensity: 1.5   // Range: 0.0 to 2.0
        ))
    )

    let response = try await client.textToSpeech(request)
    audioPlayer = try AVAudioPlayer(data: response.audioData)
    audioPlayer?.play()
    ```
  </Tab>

  <Tab title="Convenience Method">
    Use the convenience method for quick emotion control:

    ```swift
    let audio = try await client.speak(
        "I'm so excited!",
        voiceId: "tc_672c5f5ce59fac2a48faeaee",
        emotion: .happy,
        intensity: 1.5
    )

    audioPlayer = try AVAudioPlayer(data: audio.audioData)
    audioPlayer?.play()
    ```
  </Tab>
</Tabs>

### Audio Customization

Control loudness, pitch, tempo, and output format:

```swift
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
    )
)

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](https://studio.typecast.ai/developers/api/voices) page.

```swift
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.

```swift
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, or prompt. The composer sends segments to `POST /v1/text-to-speech/compose`, which returns WAV or MP3 directly. Set silence removal explicitly on TTS segments; explicit pauses are preserved.

```swift
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:

```swift
// 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 35+ languages with automatic language detection:

```swift
// 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:

```swift
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)
}
```

<Note>
**WAV streaming format:** 32000 Hz, 16-bit, mono PCM. The first chunk includes a 44-byte WAV header (size = `0xFFFFFFFF`); subsequent chunks are raw PCM only. For MP3: 320 kbps, 44100 Hz, each chunk is independently decodable. Use `Typecast.OutputStream` to avoid collision with `Foundation.OutputStream`.
</Note>

## 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

```swift
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.

```swift
let request = TTSRequestWithTimestamps(
    voiceId: "tc_60e5426de8b95f1d3000d7b5",
    text: "Hello. How are you?",
    model: .ssfmV30,
    granularity: .char  // required for Japanese / Chinese
)
```

### Subtitle Export

```swift
let srt = result.toSrt()
print(srt)

let vtt = result.toVtt()
print(vtt)
```

<Note>
**Japanese / Chinese:** Word-level segmentation is not meaningful for languages without whitespace delimiters (jpn, zho). Use `.char` granularity for these languages to get character-level alignment.
</Note>

## Instant Voice Cloning

Clone a custom voice from a short audio sample, then pass the returned `uc_` voice ID directly to TTS.

```swift
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)
```

<Warning>
Voice cloning audio must be **25 MB or smaller**, the audio duration must be **5-150 seconds**, and the custom voice name must be **1-30 characters**.
</Warning>

## Supported Languages

The SDK supports 35+ 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 | | | | |

<Info>
If not specified, the language will be automatically detected from the input text.
</Info>

## Error Handling

The SDK provides a comprehensive `TypecastError` enum for handling API errors:

```swift
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

```swift
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

```swift
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

```swift
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 |

### TTSResponse Fields

| Field | Type | Description |
|-------|------|-------------|
| `audioData` | `Data` | Generated audio data |
| `duration` | `TimeInterval` | Audio duration in seconds |
| `format` | `AudioFormat` | Audio format (`.wav` or `.mp3`) |

## Control silence duration

Requires **0.3.14 or later**.

`remove_silence_ms` specifies the **silence duration to retain**, not the amount to remove. Use an integer from `0` to `1000` ms. `0` removes detected silence; omission or `null` disables duration-based silence removal.

Standard, streaming, and timestamp TTS use `output.remove_silence_ms`; Compose uses `segments[].output.remove_silence_ms` on each `tts` segment. Returned timestamps align with the processed audio, and explicit `pause` segments are preserved.

Streaming's default leading-silence trimming is separate. Small values such as `0` can leave gaps between playable chunks; allow sufficient playback buffering and test with your content.

Pass these output settings to the corresponding request's `output`. For streaming, use the streaming output type.

```swift
let output = OutputSettings(removeSilenceMs: 300)
let streamOutput = Typecast.OutputStream(removeSilenceMs: 300)
```
