Skill: irachex/local-tts (version 1.0.0)

--- name: local-tts description: Local text-to-speech using Qwen3-TTS with mlx_audio (macOS Apple Silicon) or qwen-tts (Linux/Windows). Privacy-first offline TTS with natural, realistic voice cloning and voice design. Use for local, secure, high-quality multilingual speech synthesis. license: MIT --- # Local TTS with Qwen3-TTS **Privacy-First | Offline | High-Quality | Natural Real Voices** Local text-to-speech synthesis using Qwen3-TTS models. Your text never leaves your machine. ## Why Local TTS? Unlike cloud TTS (Google, AWS, Azure), **local-tts** ensures: - **Zero data transmission** - 100% on-device processing - **Works offline** - No network required - **No API keys** - No external dependencies - **GDPR/HIPAA friendly** - Simplified compliance See [privacy & security details](references/privacy_security.md). ## Platform Overview | Platform | Backend | Installation | Best For | |----------|---------|--------------|----------| | macOS (Apple Silicon) | `mlx_audio` | `pip install mlx-audio` | M1/M2/M3/M4 Macs | | Linux/Windows | `qwen-tts` | `pip install qwen-tts` | CUDA GPUs | ## Quick Start ### macOS ```bash pip install mlx-audio brew install ffmpeg # Natural female voice python -m mlx_audio.tts.generate \ --text "Hello world" \ --model mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit \ --voice Chelsie ``` ### Linux/Windows ```bash pip install qwen-tts # With optimizations (FlashAttention, bfloat16, auto-device) python scripts/tts_linux.py "Hello world" --female ``` ## Key Concepts ### `--voice` vs `--instruct` (Important) | Model | `--voice` | `--instruct` | Notes | |-------|-----------|--------------|-------| | **CustomVoice** | Select preset voice | Add style/emotion | **Can use together** - voice + style control | | **VoiceDesign** | N/A | Create voice from description | `--instruct` only | | **Base** | N/A | N/A | For voice cloning with `--ref_audio` | **CustomVoice with style control:** ```bash python -m mlx_audio.tts.generate \ --text "Hello there!" \ --model mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit \ --voice Serena \ --instruct "excited and enthusiastic" ``` ### 9 Preset Voices (Open Source CustomVoice) | Voice | Gender | Language | Character | |-------|--------|----------|-----------| | Chelsie | Female | English (American) | Gentle, empathetic | | Serena | Female | English | Warm, gentle | | Ono Anna | Female | Japanese | Playful | | Sohee | Female | Korean | Warm | | Aiden | Male | English (American) | Sunny | | Dylan | Male | English | Natural | | Eric | Male | English | Real | | Ryan | Male | English | Natural | | Uncle Fu | Male | Chinese | Youthful Beijing | **Defaults:** Female=`Serena`, Male=`Aiden` ## Usage Examples ### CustomVoice (Preset Voices) ```bash # Natural female python -m mlx_audio.tts.generate \ --text "Your text" --voice Serena --lang_code en \ --model mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit # Real male python -m mlx_audio.tts.generate \ --text "Your text" --voice Aiden --lang_code en \ --model mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit ``` ### VoiceDesign (Text-Based) ```bash python -m mlx_audio.tts.generate \ --text "Hello" \ --model mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit \ --instruct "A warm female voice, professional and clear" ``` ### Long Text Generation For long text, increase `--max_tokens` and enable `--join_audio` (macOS/MLX only): ```bash python -m mlx_audio.tts.generate \ --text "Your very long text here..." \ --model mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit \ --voice Serena \ --max_tokens 4096 \ --join_audio \ --output long_audio.wav ``` ### Voice Cloning ```bash python -m mlx_audio.tts.generate \ --text "Cloned voice speaking" \ --model mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit \ --ref_audio sample.wav --ref_text "Sample transcript" ``` ## Parameters | Parameter | Description | Values | |-----------|-------------|--------| | `--text` | Text to speak | Required | | `--model` | Model ID | See table below | | `--voice` | Preset voice (CustomVoice) | Chelsie, Serena, Aiden, Ryan... | | `--instruct` | Voice description (VoiceDesign) or style/emotion (CustomVoice) | e.g., "excited", "calm", "professional" | | `--speed` | Speaking rate | 0.5-2.0 (default: 1.0) | | `--pitch` | Voice pitch | 0.5-2.0 (default: 1.0) | | `--lang_code` | Language | en, cn, ja, ko, de, fr... | | `--ref_audio` | Reference for cloning | File path | | `--output` | Output file | Path (auto-generated if omitted) | | `--max_tokens` | Max generation tokens | Integer (default: 2048) - Increase for long text | | `--join_audio` | Merge audio segments | `true` (default) or `false` - Recommended for long text | ## Models | Model | Size | Purpose | |-------|------|---------| | `Qwen3-TTS-12Hz-1.7B-CustomVoice` | 1.7B | 9 preset voices + style control | | `Qwen3-TTS-12Hz-1.7B-VoiceDesign` | 1.7B | Text-based voice creation | | `Qwen3-TTS-12Hz-1.7B-Base` | 1.7B | Voice cloning | | `Qwen3-TTS-12Hz-0.6B-*` | 0.6B | Lightweight versions | macOS: Add `mlx-community/` prefix (e.g., `mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit`) ## Scripts - `scripts/tts_macos.py` - macOS wrapper - `scripts/tts_linux.py` - Linux/Windows wrapper with optimizations ## Optimizations (Linux/Windows) `tts_linux.py` automatically enables: - **FlashAttention** - Faster, less memory - **bfloat16** - Better precision - **Auto device** - CUDA → CPU fallback - **Mixed precision** - Speed + quality ## Troubleshooting | Issue | Solution | |-------|----------| | macOS: Model not found | Use `mlx-community/` prefix | | macOS: Audio format | `brew install ffmpeg` | | Linux: CUDA OOM | Use `0.6B` models | | Linux: Slow | Check CUDA: `torch.cuda.is_available()` | ## References - [macOS Details](references/macos_mlx_audio.md) - [Linux/Windows Details](references/linux_windows_transformers.md) - [Privacy & Security](references/privacy_security.md) ## Version **1.0.0** - See [VERSION](VERSION) and [package.json](package.json)

path: package.json
content: {
  "name": "local-tts",
  "version": "1.0.0",
  "description": "Local text-to-speech using Qwen3-TTS with mlx_audio (macOS) or qwen-tts (Linux/Windows). Privacy-first offline TTS with voice cloning and voice design capabilities.",
  "author": "irachex",
  "license": "MIT",
  "keywords": [
    "tts",
    "text-to-speech",
    "qwen3-tts",
    "local",
    "offline",
    "privacy",
    "voice-cloning",
    "voice-design",
    "mlx-audio",
    "qwen-tts",
    "natural",
    "real",
    "realistic",
    "human-like",
    "high-quality"
  ],
  "homepage": "https://github.com/irachex/local-tts",
  "repository": {
    "type": "git",
    "url": "https://github.com/irachex/local-tts.git"
  },
  "engines": {
    "python": ">=3.8"
  },
  "platforms": [
    "macOS (Apple Silicon)",
    "Linux",
    "Windows"
  ],
  "requirements": {
    "macos": {
      "python": ">=3.8",
      "packages": ["mlx-audio", "ffmpeg"],
      "hardware": "Apple Silicon (M1/M2/M3/M4)"
    },
    "linux": {
      "python": ">=3.8",
      "packages": ["qwen-tts", "torch", "transformers"],
      "hardware": "NVIDIA GPU with CUDA (recommended)"
    },
    "windows": {
      "python": ">=3.8",
      "packages": ["qwen-tts", "torch", "transformers"],
      "hardware": "NVIDIA GPU with CUDA (recommended)"
    }
  }
}


path: references/linux_windows_transformers.md
content: # Linux/Windows qwen-tts Reference

## Overview

The Linux/Windows implementation uses the `qwen-tts` Python library with automatic optimizations:

-  **FlashAttention** - Faster attention computation, reduced memory usage
-  **bfloat16** - Better numerical stability than float16, faster than float32
-  **Auto device selection** - CUDA if available, CPU fallback
-  **Auto mixed precision** - Optimal speed/quality balance

## Installation

### System Requirements

| Component | Minimum | Recommended |
|-----------|---------|-------------|
| GPU | NVIDIA with 4GB VRAM | NVIDIA with 8GB+ VRAM |
| RAM | 8GB | 16GB+ |
| Disk | 5GB | 10GB |
| Python | 3.8 | 3.10 or 3.12 |

### Step-by-Step Installation

#### 1. Create Virtual Environment

```bash
python -m venv venv

# Linux/macOS
source venv/bin/activate

# Windows
.\venv\Scripts\activate
```

#### 2. Install qwen-tts

```bash
pip install qwen-tts
```

This automatically installs compatible versions of:
- `torch`
- `transformers`
- `accelerate`

#### 3. Install FlashAttention (Recommended)

FlashAttention significantly improves speed and reduces memory usage:

```bash
pip install flash-attn --no-build-isolation
```

**Requirements:**
- CUDA 11.6+ or ROCm (AMD GPUs)
- For CUDA 12.x, use PyTorch with CUDA 12.x

**Troubleshooting:**
- If installation fails, the script will still work (just slower)
- On Windows, you may need Visual C++ Build Tools
- On older GPUs (pre-Ampere), FlashAttention v1 will be used

#### 4. Install FFmpeg

**Ubuntu/Debian:**
```bash
sudo apt update
sudo apt install ffmpeg
```

**Windows:**
1. Download from https://ffmpeg.org/download.html
2. Extract to `C:\ffmpeg`
3. Add `C:\ffmpeg\bin` to PATH

#### 5. Verify Installation

```bash
# Check qwen-tts is installed
qwen-tts --help

# Run wrapper script test
python scripts/tts_linux.py "Hello world" --female
```

## Python API Usage

### Basic TTS with Optimizations

```python
from qwen_tts import Qwen3TTSModel, Qwen3TTSTokenizer
import torch
import soundfile as sf

# Configuration
MODEL_ID = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"

# Auto device selection
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

# Auto dtype selection (bfloat16 for Ampere+, float16 for older, float32 for CPU)
if device == "cuda":
    capability = torch.cuda.get_device_capability()
    if capability[0] >= 8:  # Ampere (SM 8.0+) - A100, RTX 30xx, RTX 40xx
        dtype = torch.bfloat16
        print("Using bfloat16 for optimal precision")
    else:  # Older GPUs
        dtype = torch.float16
        print("Using float16")
else:
    dtype = torch.float32
    print("Using float32 for CPU")

# Load model with optimizations
model = Qwen3TTSModel.from_pretrained(
    MODEL_ID,
    torch_dtype=dtype,
    device_map="auto" if device == "cuda" else None,
    attn_implementation="flash_attention_2",  # Enable FlashAttention
)

tokenizer = Qwen3TTSTokenizer.from_pretrained(MODEL_ID)

if device == "cpu":
    model = model.to(device)

# Generate
text = "Hello, this is optimized text-to-speech."
inputs = tokenizer(text, return_tensors="pt").to(device)

with torch.no_grad():
    if device == "cuda":
        # Use automatic mixed precision
        with torch.cuda.amp.autocast(dtype=dtype):
            audio = model.generate(**inputs, voice="Chelsie", language="en")
    else:
        audio = model.generate(**inputs, voice="Chelsie", language="en")

# Save
if isinstance(audio, torch.Tensor):
    audio = audio.cpu().numpy()
sf.write("output.wav", audio, 24000)
```

### CustomVoice with All Parameters

```python
from qwen_tts import Qwen3TTSModel, Qwen3TTSTokenizer
import torch
import soundfile as sf

model_id = "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice"

# Load with optimizations
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.bfloat16 if device == "cuda" and torch.cuda.get_device_capability()[0] >= 8 else torch.float16

model = Qwen3TTSModel.from_pretrained(
    model_id,
    torch_dtype=dtype,
    device_map="auto",
    attn_implementation="flash_attention_2",
)
tokenizer = Qwen3TTSTokenizer.from_pretrained(model_id)

text = "Welcome to our service"
inputs = tokenizer(text, return_tensors="pt").to(device)

# Generate with all parameters
with torch.no_grad(), torch.cuda.amp.autocast(dtype=dtype):
    audio = model.generate(
        **inputs,
        voice="Chelsie",        # Preset voice
        language="en-US",       # Language code
        speed=1.2,              # Speaking speed (0.5-2.0)
        pitch=1.0,              # Voice pitch (0.5-2.0)
        temperature=0.7,        # Sampling temperature
        top_p=0.9,              # Top-p sampling
    )

sf.write("output.wav", audio.cpu().numpy(), 24000)
```

### VoiceDesign with bfloat16

```python
from qwen_tts import Qwen3TTSModel, Qwen3TTSTokenizer
import torch
import soundfile as sf

model_id = "Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign"

device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.bfloat16 if device == "cuda" and torch.cuda.get_device_capability()[0] >= 8 else torch.float16

model = Qwen3TTSModel.from_pretrained(
    model_id,
    torch_dtype=dtype,
    device_map="auto",
    attn_implementation="flash_attention_2",
)
tokenizer = Qwen3TTSTokenizer.from_pretrained(model_id)

text = "I am your AI assistant"
inputs = tokenizer(text, return_tensors="pt").to(device)

# Generate with voice description
with torch.no_grad(), torch.cuda.amp.autocast(dtype=dtype):
    audio = model.generate(
        **inputs,
        instruct="A professional female voice, calm and authoritative",
        language="en-US"
    )

sf.write("output.wav", audio.cpu().numpy(), 24000)
```

### Voice Cloning

```python
from qwen_tts import Qwen3TTSModel, Qwen3TTSTokenizer
import torch
import soundfile as sf
import librosa

model_id = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"

device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.bfloat16 if device == "cuda" and torch.cuda.get_device_capability()[0] >= 8 else torch.float16

model = Qwen3TTSModel.from_pretrained(
    model_id,
    torch_dtype=dtype,
    device_map="auto",
    attn_implementation="flash_attention_2",
)
tokenizer = Qwen3TTSTokenizer.from_pretrained(model_id)

# Load reference audio
ref_audio, sr = librosa.load("reference.wav", sr=24000)

# Text to synthesize
text = "This is my cloned voice speaking"
inputs = tokenizer(text, return_tensors="pt").to(device)

# Generate with reference
with torch.no_grad(), torch.cuda.amp.autocast(dtype=dtype):
    audio = model.generate(
        **inputs,
        ref_audio=ref_audio,
        ref_text="Reference audio transcript",
        language="en-US"
    )

sf.write("cloned.wav", audio.cpu().numpy(), 24000)
```

## CLI Usage

### Web UI

```bash
# Launch with default optimizations
qwen-tts-demo Qwen/Qwen3-TTS-12Hz-1.7B-Base --ip 127.0.0.1 --port 8000
```

Access at http://127.0.0.1:8000

### Wrapper Script

```bash
# Basic usage with auto-optimizations
python scripts/tts_linux.py "Hello world" --female

# With custom parameters
python scripts/tts_linux.py "Hello world" \
    --voice Chelsie \
    --speed 1.2 \
    --pitch 1.0 \
    --lang_code en-US

# Voice design
python scripts/tts_linux.py "Hello world" \
    --model voicedesign \
    --instruct "A warm male voice with authority"

# Voice cloning
python scripts/tts_linux.py "Cloned voice text" \
    --ref_audio sample.wav \
    --ref_text "Sample transcript"

# Chinese TTS
python scripts/tts_linux.py "" \
    --voice Aiden \
    --lang_code zh-CN
```

## Optimization Guide

### FlashAttention Benefits

| Metric | Without FlashAttention | With FlashAttention | Improvement |
|--------|----------------------|---------------------|-------------|
| Memory Usage | 100% | ~60% | **40% reduction** |
| Speed | 1.0x | 1.3-2.0x | **30-100% faster** |
| Max Sequence Length | Limited | Extended | **Longer texts** |

### bfloat16 vs float16 vs float32

| Feature | float32 | float16 | bfloat16 |
|---------|---------|---------|----------|
| Memory | 4 bytes | 2 bytes | 2 bytes |
| Speed | Baseline | 2x faster | 2x faster |
| Precision | Best | Good (range issues) | Good (better range) |
| GPU Support | All | Most | Ampere+ (SM 8.0+) |

**Recommendation:**
- **Ampere+ GPUs (A100, RTX 30xx, RTX 40xx):** Use bfloat16
- **Older GPUs:** Use float16
- **CPU:** Use float32

### Device Selection Priority

```python
# Automatic (recommended)
device = "cuda" if torch.cuda.is_available() else "cpu"

# Force CPU
device = "cpu"

# Force specific GPU
device = "cuda:0"
```

### Memory Optimization

If you encounter OOM errors:

1. **Use smaller models:**
   ```python
   model_id = "Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice"  # Instead of 1.7B
   ```

2. **Enable gradient checkpointing (if training):**
   ```python
   model.gradient_checkpointing_enable()
   ```

3. **Use CPU offload:**
   ```python
   model = model.to("cpu")
   # Generate in chunks
   ```

4. **Clear cache between generations:**
   ```python
   torch.cuda.empty_cache()
   ```

## Model Identifiers

### Hugging Face Models

```
Qwen/Qwen3-TTS-12Hz-1.7B-Base
Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign
Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice
Qwen/Qwen3-TTS-12Hz-0.6B-Base
Qwen/Qwen3-TTS-12Hz-0.6B-VoiceDesign
Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice
```

## Parameters Reference

### Generation Parameters

| Parameter | Type | Description | Default |
|-----------|------|-------------|---------|
| `voice` | str | Preset voice name (CustomVoice) | None |
| `instruct` | str | Voice description (VoiceDesign) | None |
| `language` | str | Language code | "en-US" |
| `speed` | float | Speaking speed (0.5-2.0) | 1.0 |
| `pitch` | float | Voice pitch (0.5-2.0) | 1.0 |
| `temperature` | float | Sampling temperature | 0.7 |
| `top_p` | float | Top-p sampling | 0.9 |
| `repetition_penalty` | float | Repetition penalty | 1.0 |
| `ref_audio` | array | Reference audio for cloning | None |
| `ref_text` | str | Reference text for cloning | None |

### Model Loading Parameters

| Parameter | Description | Recommended Value |
|-----------|-------------|-------------------|
| `torch_dtype` | Model precision | `torch.bfloat16` or `torch.float16` |
| `device_map` | Device placement | `"auto"` for CUDA, `None` for manual |
| `attn_implementation` | Attention implementation | `"flash_attention_2"` if available |

### Supported Languages

| Code | Language |
|------|----------|
| "en", "en-US" | English (US) |
| "en-GB" | English (UK) |
| "zh", "zh-CN" | Chinese (Simplified) |
| "zh-TW" | Chinese (Traditional) |
| "ja" | Japanese |
| "ko" | Korean |
| "de" | German |
| "fr" | French |
| "ru" | Russian |
| "pt" | Portuguese |
| "es" | Spanish |
| "it" | Italian |
| "auto" | Auto-detect |

### Preset Voices (CustomVoice)

| Voice | Gender | Description |
|-------|--------|-------------|
| "Chelsie" | Female | Bright, slightly edgy young female |
| "Serena" | Female | English | Warm, gentle young female |
| "Ryan" | Male | Seasoned male voice with low, mellow timbre |
| "Aiden" | Male | Youthful Beijing male voice, clear natural timbre |

Use `model.get_supported_speakers()` to list all available voices.

## Troubleshooting

### FlashAttention Installation Fails

```bash
# Option 1: Skip FlashAttention (script will still work)
pip install qwen-tts

# Option 2: Install from source
pip install flash-attn --no-build-isolation --verbose
```

### CUDA Out of Memory

1. Use smaller models:
   ```python
   model_id = "Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice"
   ```

2. Use float16 instead of bfloat16 on older GPUs

3. Close other GPU applications

### Model Download Issues

```bash
# Set Hugging Face cache directory
export HF_HOME=/path/to/cache

# Or use huggingface-cli
huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-Base
```

### Slow Generation

1. **Check CUDA is being used:**
   ```python
   import torch
   print(torch.cuda.is_available())  # Should be True
   ```

2. **Verify FlashAttention:**
   The script prints  if FlashAttention is active

3. **Check dtype:**
   bfloat16/float16 should be 2x faster than float32

### Windows-Specific Issues

1. **Visual C++ Redistributables**: Install if compilation errors occur
2. **PATH issues**: Ensure Python and ffmpeg are in PATH
3. **Long paths**: Enable long path support in Windows

```powershell
# Enable long paths (run as Administrator)
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
```

## Performance Benchmarks

Expected performance on various GPUs (1.7B model, 20 tokens):

| GPU | FlashAttention | dtype | Time | VRAM |
|-----|---------------|-------|------|------|
| RTX 4090 | Yes | bfloat16 | ~0.5s | ~6GB |
| RTX 4090 | No | float16 | ~1.0s | ~8GB |
| RTX 3090 | Yes | bfloat16 | ~0.7s | ~6GB |
| A100 | Yes | bfloat16 | ~0.3s | ~6GB |
| CPU (16 cores) | N/A | float32 | ~10s+ | N/A |

*Times are approximate and vary based on text length and system load.*


path: references/macos_mlx_audio.md
content: # macOS mlx_audio Reference

**Privacy-First | Local Processing | Optimized for Apple Silicon**

This guide covers using Qwen3-TTS on macOS with Apple Silicon (M1/M2/M3/M4) using the `mlx_audio` library.

## Why Local TTS on macOS?

### Privacy Benefits

Using `mlx_audio` on macOS ensures:

- **100% Offline Processing** - No internet required after model download
- **Zero Data Transmission** - Your text never leaves your Mac
- **No Cloud Dependencies** - No API keys, no external services
- **Apple Silicon Optimization** - Leverages Neural Engine and GPU

### Security Advantages

| Feature | Cloud TTS | mlx_audio (Local) |
|---------|-----------|-------------------|
| Network Required | Yes | No (after download) |
| Data Leaves Device | Yes | Never |
| API Keys | Required | Not needed |
| Audit Trail | External | You control |

## Installation

### Requirements
- macOS with Apple Silicon (M1/M2/M3/M4)
- Python 3.8+
- ffmpeg

### Install

```bash
pip install mlx-audio
brew install ffmpeg
```

## Command Structure

```bash
python -m mlx_audio.tts.generate [OPTIONS]
```

## All Parameters

### Required

| Parameter | Description |
|-----------|-------------|
| `--text` | Text to synthesize |
| `--model` | Model identifier (e.g., `mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit`) |

### Optional

| Parameter | Description | Default |
|-----------|-------------|---------|
| `--output` / `-o` | Output file path | Auto-generated |
| `--voice` | Preset voice (CustomVoice only) | None |
| `--instruct` | Voice description (VoiceDesign only) | None |
| `--ref_audio` | Reference audio for cloning | None |
| `--ref_text` | Reference text for cloning | None |
| `--speed` | Speaking speed (0.5-2.0) | 1.0 |
| `--gender` | Voice gender | None |
| `--pitch` | Voice pitch (0.5-2.0) | 1.0 |
| `--lang_code` | Language code | en-US |
| `--temperature` | Sampling temperature | 0.7 |
| `--top_p` | Top-p sampling | 0.9 |
| `--repetition_penalty` | Repetition penalty | 1.0 |

## Model Identifiers

All models use 8bit quantization for efficiency.

### 1.7B Models (Best Quality)

```
mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit
mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit
mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit
```

### 0.6B Models (Faster)

```
mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit
mlx-community/Qwen3-TTS-12Hz-0.6B-VoiceDesign-8bit
mlx-community/Qwen3-TTS-12Hz-0.6B-CustomVoice-8bit
```

## Preset Voices (CustomVoice)

The CustomVoice model includes **9 premium preset voices** (Open Source Version):

1. **Chelsie** - Female, English (American) - Gentle, breathy, empathetic
2. **Serena** - Female, English - Warm, gentle young female
3. **Ono Anna** - Female, Japanese - Playful Japanese female voice
4. **Sohee** - Female, Korean - Warm Korean female voice
5. **Aiden** - Male, English (American) - Sunny American male voice
6. **Dylan** - Male, English - English male voice
7. **Eric** - Male, English - English male voice
8. **Ryan** - Male, English - English male voice
9. **Uncle Fu** - Male, Chinese (Beijing) - Youthful Beijing male voice

**Recommended Natural/Real Voices:**
- **Natural Female:** `Chelsie` (gentle, empathetic) or `Serena` (warm)
- **Real Male:** `Aiden` (sunny, American) or `Ryan` (natural English)

**Note:** Use `model.get_supported_speakers()` to list available voices programmatically.

## Language Codes

| Code | Language |
|------|----------|
| `en-US` | American English |
| `en-GB` | British English |
| `zh-CN` | Mandarin Chinese (Simplified) |
| `zh-TW` | Mandarin Chinese (Traditional) |
| `ja` | Japanese |
| `ko` | Korean |
| `de` | German |
| `fr` | French |
| `ru` | Russian |
| `pt` | Portuguese |
| `es` | Spanish |
| `it` | Italian |
| `auto` | Auto-detect |

## Usage Examples

### Basic TTS

```bash
python -m mlx_audio.tts.generate \
    --text "Hello world" \
    --model mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit \
    --output hello.wav
```

### CustomVoice with Female Voice (Natural, Real)

```bash
python -m mlx_audio.tts.generate \
    --text "Welcome to our service" \
    --model mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit \
    --voice Chelsie \
    --speed 1.1 \
    --lang_code en-US \
    --output welcome.wav
```

### CustomVoice with Male Voice (Natural, Real)

```bash
python -m mlx_audio.tts.generate \
    --text "Thank you for calling" \
    --model mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit \
    --voice Aiden \
    --speed 0.9 \
    --lang_code en-US \
    --output thankyou.wav
```

### VoiceDesign

```bash
python -m mlx_audio.tts.generate \
    --text "I am your AI assistant" \
    --model mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit \
    --instruct "A professional female voice, calm and clear" \
    --speed 1.0 \
    --lang_code en-US \
    --output assistant.wav
```

### Voice Cloning

```bash
python -m mlx_audio.tts.generate \
    --text "This is my cloned voice speaking" \
    --model mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit \
    --ref_audio /path/to/reference.wav \
    --ref_text "The reference audio transcript" \
    --output cloned.wav
```

### Chinese TTS

```bash
python -m mlx_audio.tts.generate \
    --text "" \
    --model mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit \
    --voice Aiden \
    --lang_code zh-CN \
    --output chinese.wav
```

## Advanced Parameters

### Temperature and Top-p

Control the randomness and diversity of the generated speech:

```bash
python -m mlx_audio.tts.generate \
    --text "Hello" \
    --model mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit \
    --temperature 0.8 \
    --top_p 0.95
```

- **Temperature**: Higher values (e.g., 0.9) produce more varied output, lower values (e.g., 0.5) produce more deterministic output
- **Top-p**: Nucleus sampling parameter (0.0-1.0)

### Repetition Penalty

Prevent repetitive patterns:

```bash
python -m mlx_audio.tts.generate \
    --text "Hello hello hello" \
    --model mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit \
    --repetition_penalty 1.2
```

## Performance Tips

1. **Use 0.6B models** for faster generation with slight quality trade-off
2. **First run downloads models** - subsequent runs are faster
3. **Apple Silicon optimization** - runs efficiently on M-series chips
4. **Memory usage** - 1.7B models use ~4GB RAM, 0.6B models use ~2GB

## Troubleshooting

### Model Download Issues

If models fail to auto-download:

```bash
# Manually download with huggingface-cli
pip install huggingface-hub
huggingface-cli download mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit
```

### Audio Format Issues

Ensure ffmpeg is installed:

```bash
brew install ffmpeg
which ffmpeg  # Should return /opt/homebrew/bin/ffmpeg
```

### Out of Memory

Use smaller models:

```bash
# Use 0.6B instead of 1.7B
--model mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit
```

### Slow Generation

- Close other memory-intensive applications
- Use 0.6B models for faster inference
- Ensure you're on Apple Silicon (Intel Macs will be slower)


path: references/privacy_security.md
content: # Privacy & Security Benefits of Local TTS

## Complete Data Privacy | Natural Realistic Voices | Offline Processing

Local text-to-speech (TTS) using Qwen3-TTS provides **natural**, **realistic**, **human-like** voice synthesis while keeping your data completely private and secure.

### The Problem with Cloud TTS Services

When you use cloud-based Text-to-Speech services like:
- Google Cloud Text-to-Speech
- AWS Amazon Polly
- Microsoft Azure Speech Services
- ElevenLabs API
- OpenAI TTS

Your data flows through this path:

```
Your Device -> Internet -> Provider's Servers -> Processing -> Storage -> Back to You
```

**Risks include:**
- **Data interception** in transit
- **Storage on external servers** (you don't control retention policies)
- **Data mining** for AI training
- **Compliance violations** (GDPR, HIPAA, CCPA)
- **Geopolitical risks** (data stored in foreign jurisdictions)

### The Local TTS Solution

With **local-tts**, your data stays on your machine:

```
Your Device -> Local Processing -> Local Output
     ^
   Zero network transmission
```

**Benefits:**
- **Zero data transmission** - Nothing leaves your device
- **No external dependencies** - Works offline
- **No API keys** - No authentication required
- **No usage logs** - No one tracks what you synthesize
- **Full compliance** - Easier GDPR, HIPAA, CCPA compliance

## Industry-Specific Privacy Benefits

### Healthcare (HIPAA Compliance)

**Scenario:** Converting patient medical records to audio for accessibility.

| Cloud TTS Risk | Local TTS Solution |
|----------------|-------------------|
| PHI sent to external servers | Patient data never leaves the hospital network |
| Unknown data retention | You control all data retention |
| Potential breaches | Isolated from external attack vectors |
| Audit complications | Simple compliance auditing |

### Legal (Attorney-Client Privilege)

**Scenario:** Converting confidential legal documents to audio.

| Cloud TTS Risk | Local TTS Solution |
|----------------|-------------------|
| Privileged communications exposed | Complete confidentiality preserved |
| Jurisdiction issues | Data never crosses borders |
| Discovery complications | No third-party data processors |

### Financial Services

**Scenario:** Internal financial reports, trading strategies.

| Cloud TTS Risk | Local TTS Solution |
|----------------|-------------------|
| Insider information exposed | Sensitive data stays internal |
| Regulatory violations | Simplified compliance |
| Competitive intelligence leaks | No external processing |

### Personal Use

**Scenario:** Private journals, creative writing, personal notes.

| Cloud TTS Risk | Local TTS Solution |
|----------------|-------------------|
| Personal content analyzed | Complete privacy for personal content |
| Profile building | No behavioral tracking |
| Content restrictions | No censorship or filtering |

## Security Security Advantages

### Attack Surface Comparison

| Attack Vector | Cloud TTS | Local TTS |
|--------------|-----------|-----------|
| Man-in-the-middle attacks | No Vulnerable | Yes No network = no MITM |
| API key theft | No Risk | Yes No API keys |
| Server-side breaches | No Risk | Yes No external servers |
| Supply chain attacks | No Risk | Yes Minimal dependencies |
| Insider threats (provider) | No Risk | Yes You control everything |

### Network Security

**Local TTS requires:**
- No open ports
- No API endpoints
- No authentication tokens
- No network connectivity (after model download)

**This means:**
- Firewall-friendly
- Air-gapped compatible
- Zero network attack surface during operation

## Offline Offline Capability

### Works Without Internet

Once models are downloaded, local-tts operates entirely offline:

```bash
# Download models (one-time, requires internet)
python scripts/tts_linux.py "test" --female

# Disconnect from internet
# ...

# Continue working offline
python scripts/tts_linux.py "This works offline!" --male
```

**Use cases:**
- Air-gapped environments
- Secure facilities
- Remote locations
- Network outage scenarios
- High-latency environments

## Protection Compliance Benefits

### GDPR (European Union)

| Requirement | Cloud TTS | Local TTS |
|-------------|-----------|-----------|
| Data minimization | No Hard to ensure | Yes Natural default |
| Purpose limitation | No Provider may use data for ML | Yes You control all uses |
| Storage limitation | No Unknown retention | Yes You set retention |
| Security | No Complex due diligence | Yes Simplified |
| Cross-border transfers | No May apply | Yes Not applicable |

### HIPAA (United States Healthcare)

Local TTS simplifies HIPAA compliance by:
- Eliminating Business Associate Agreements (BAAs)
- Removing external data processors from scope
- Simplifying risk assessments
- Reducing audit complexity

### CCPA/CPRA (California)

Local TTS helps with:
- Consumer privacy rights (no third parties to notify)
- Data deletion (immediate and complete)
- Opt-out rights (no "sale" of data possible)

##  Transparency & Auditing

### Open Source = Trust

Unlike proprietary cloud services:
- **Full source code visibility** - Audit exactly what happens to your data
- **No black boxes** - Understand the complete processing pipeline
- **Community verification** - Security researchers can audit
- **No hidden telemetry** - Verify no data collection in code

### Self-Hosting Benefits

- **Infrastructure control** - You own the hardware
- **Update control** - Choose when to update
- **Configuration control** - Full customization
- **Logging control** - Choose what to log

##  Performance Benefits

### Latency Comparison

| Operation | Cloud TTS | Local TTS |
|-----------|-----------|-----------|
| Network round-trip | 50-500ms | 0ms |
| Queue wait time | Variable | None |
| Processing | ~100-500ms | ~100-500ms (GPU) |
| **Total** | **200ms - 1s+** | **~100-500ms** |

With local GPU, you often get **faster** results than cloud APIs.

### Cost Benefits

| Cost Type | Cloud TTS | Local TTS |
|-----------|-----------|-----------|
| Per-character fees | No $$$ | Yes Free |
| API call charges | No $ | Yes Free |
| Data transfer | No Egress fees | Yes None |
| Subscription | No Monthly | Yes Hardware only |
| **Long-term cost** | **Ongoing $$$** | **One-time hardware** |

##  When to Choose Local TTS

### Choose Local TTS When:

- Yes Privacy is paramount
- Yes Handling sensitive data
- Yes Compliance requirements are strict
- Yes Internet connectivity is unreliable
- Yes You need complete control
- Yes Cost predictability matters
- Yes Latency is critical

### Cloud TTS May Be Better When:

-  You need the absolute latest voice models
-  You have minimal technical resources
-  Usage is very sporadic
-  You need pre-built integrations

## Conclusion

Local TTS with **local-tts** provides the best combination of:
-  **Privacy** - Your data never leaves your machine
- Security **Security** - Minimal attack surface
-  **Independence** - No external dependencies
-  **Performance** - GPU-accelerated, low latency
-  **Cost** - No per-use fees
-  **Natural Quality** - Realistic, human-like voices
-  **Real Voices** - 9 premium preset voices that sound natural

**Your voice data belongs to you. Keep it that way with natural, real, high-quality local TTS.**


path: scripts/tts_linux.py
content: #!/usr/bin/env python3
"""
Local TTS wrapper for Linux/Windows using qwen-tts library.

This script provides a simplified interface for Qwen3-TTS on Linux and Windows
with automatic optimizations: FlashAttention, bfloat16, and auto device selection.

Features:
- FlashAttention for faster inference
- bfloat16 for better precision
- Automatic device selection (CUDA > CPU)
- Automatic mixed precision
"""

import argparse
import sys
import os
import warnings
import torch

# Suppress warnings for cleaner output
warnings.filterwarnings("ignore")

DEFAULT_FEMALE_VOICE = "Chelsie"
DEFAULT_MALE_VOICE = "Aiden"

MODELS = {
    "base": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
    "voicedesign": "Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign",
    "customvoice": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
    "base-small": "Qwen/Qwen3-TTS-12Hz-0.6B-Base",
    "voicedesign-small": "Qwen/Qwen3-TTS-12Hz-0.6B-VoiceDesign",
    "customvoice-small": "Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice",
}


def get_optimal_device():
    """Automatically select the best available device."""
    if torch.cuda.is_available():
        device = "cuda"
        # Print GPU info
        gpu_name = torch.cuda.get_device_name(0)
        vram_gb = torch.cuda.get_device_properties(0).total_memory / (1024**3)
        print(f" Using CUDA device: {gpu_name} ({vram_gb:.1f} GB VRAM)")
        return device
    else:
        print(" CUDA not available, using CPU (will be slower)")
        return "cpu"


def get_optimal_dtype(device):
    """Get optimal dtype for the device with bfloat16 support."""
    if device == "cuda":
        # Check if bfloat16 is supported (Ampere architecture and newer)
        capability = torch.cuda.get_device_capability()
        if capability[0] >= 8:  # Ampere (SM 8.0+) supports bfloat16
            print(" Using bfloat16 for optimal precision and speed")
            return torch.bfloat16
        else:
            print(" Using float16 (bfloat16 not supported on this GPU)")
            return torch.float16
    else:
        print(" Using float32 for CPU")
        return torch.float32


def check_flash_attention():
    """Check if FlashAttention is available."""
    try:
        import flash_attn
        print(" FlashAttention available - using for faster inference")
        return True
    except ImportError:
        print(" FlashAttention not installed (optional, for faster inference)")
        print("  Install with: pip install flash-attn --no-build-isolation")
        return False


def run_tts(args):
    """Run TTS with the given arguments and optimizations."""
    
    try:
        from qwen_tts import Qwen3TTSModel, Qwen3TTSTokenizer
        import soundfile as sf
        import numpy as np
    except ImportError as e:
        print(f"Error: Missing dependency - {e}")
        print("\nPlease install qwen-tts:")
        print("  pip install qwen-tts")
        print("\nFor optimal performance, also install:")
        print("  pip install flash-attn --no-build-isolation")
        return 1
    
    # Check optimizations
    has_flash_attn = check_flash_attention()
    
    # Get optimal device and dtype
    device = get_optimal_device()
    dtype = get_optimal_dtype(device)
    
    # Get model ID
    model_id = MODELS.get(args.model, args.model)
    print(f"\nLoading model: {model_id}")
    
    # Load model with optimizations
    try:
        # Enable FlashAttention if available
        model_kwargs = {
            "torch_dtype": dtype,
            "device_map": "auto" if device == "cuda" else None,
        }
        
        # Add FlashAttention config if available
        if has_flash_attn:
            model_kwargs["attn_implementation"] = "flash_attention_2"
        
        model = Qwen3TTSModel.from_pretrained(model_id, **model_kwargs)
        tokenizer = Qwen3TTSTokenizer.from_pretrained(model_id)
        
        if device == "cpu":
            model = model.to(device)
        
        print(f" Model loaded successfully")
        
    except Exception as e:
        print(f"Error loading model: {e}")
        print("\nTroubleshooting:")
        print("1. Ensure you have enough GPU memory (6-8GB for 1.7B models)")
        print("2. Try using smaller models: --model base-small")
        print("3. Check internet connection for model download")
        return 1
    
    # Prepare generation kwargs
    gen_kwargs = {
        "language": args.lang_code,
    }
    
    # Add voice parameters based on model type
    if "voicedesign" in args.model:
        if args.instruct:
            gen_kwargs["instruct"] = args.instruct
        elif args.female:
            gen_kwargs["instruct"] = "A warm female voice, clear and friendly"
        elif args.male:
            gen_kwargs["instruct"] = "A deep male voice, warm and professional"
    else:
        # CustomVoice or Base
        if args.voice:
            gen_kwargs["voice"] = args.voice
        elif args.female:
            gen_kwargs["voice"] = DEFAULT_FEMALE_VOICE
        elif args.male:
            gen_kwargs["voice"] = DEFAULT_MALE_VOICE
    
    # Add optional parameters
    if args.speed is not None:
        gen_kwargs["speed"] = args.speed
    if args.pitch is not None:
        gen_kwargs["pitch"] = args.pitch
    if args.temperature is not None:
        gen_kwargs["temperature"] = args.temperature
    
    # Handle reference audio for cloning
    if args.ref_audio:
        try:
            import librosa
            ref_audio, sr = librosa.load(args.ref_audio, sr=24000)
            gen_kwargs["ref_audio"] = ref_audio
            if args.ref_text:
                gen_kwargs["ref_text"] = args.ref_text
        except ImportError:
            print("Warning: librosa not installed. Install with: pip install librosa")
    
    # Tokenize input
    print(f"Synthesizing: '{args.text[:50]}{'...' if len(args.text) > 50 else ''}'")
    inputs = tokenizer(args.text, return_tensors="pt").to(device)
    
    # Generate audio with mixed precision for speed
    try:
        with torch.no_grad():
            if device == "cuda" and dtype != torch.float32:
                # Use automatic mixed precision for faster inference
                with torch.cuda.amp.autocast(dtype=dtype):
                    audio = model.generate(**inputs, **gen_kwargs)
            else:
                audio = model.generate(**inputs, **gen_kwargs)
        
        # Convert to numpy
        if isinstance(audio, torch.Tensor):
            audio = audio.cpu().numpy()
        
        # Ensure correct shape
        if audio.ndim == 1:
            audio = audio.reshape(-1)
        
        # Save output
        output_path = args.output or "output.wav"
        sf.write(output_path, audio, 24000)
        print(f" Audio saved to: {output_path}")
        
        return 0
        
    except torch.cuda.OutOfMemoryError:
        print("\n CUDA Out of Memory!")
        print("Suggestions:")
        print("1. Use a smaller model: --model customvoice-small")
        print("2. Close other GPU applications")
        print("3. Use CPU instead (slow): --device cpu")
        return 1
        
    except Exception as e:
        print(f"Error generating audio: {e}")
        return 1


def main():
    parser = argparse.ArgumentParser(
        description="Local TTS using Qwen3-TTS with qwen-tts (Linux/Windows). "
                    "Auto-optimizations: FlashAttention, bfloat16, auto-device.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Basic usage with optimizations
  python tts_linux.py "Hello world" --female
  
  # Voice design
  python tts_linux.py "Hello world" --model voicedesign --instruct "A warm male voice"
  
  # Voice cloning  
  python tts_linux.py "Cloned voice" --ref_audio sample.wav --ref_text "Sample text"
  
  # Chinese TTS
  python tts_linux.py "" --voice Aiden --lang_code zh-CN
        """
    )
    
    # Required arguments
    parser.add_argument("text", help="Text to synthesize")
    
    # Model selection
    parser.add_argument(
        "--model", "-m",
        default="customvoice",
        choices=list(MODELS.keys()),
        help="Model to use (default: customvoice)"
    )
    
    # Output
    parser.add_argument(
        "--output", "-o",
        default="output.wav",
        help="Output audio file path (default: output.wav)"
    )
    
    # Voice selection (mutually exclusive)
    voice_group = parser.add_mutually_exclusive_group()
    voice_group.add_argument(
        "--voice", "-v",
        help="Preset voice name (for CustomVoice model): Chelsie, Breeze, Ryan, Aiden"
    )
    voice_group.add_argument(
        "--instruct", "-i",
        help="Voice description (for VoiceDesign model), e.g., 'A warm female voice'"
    )
    
    # Quick voice presets
    parser.add_argument(
        "--female", "-f",
        action="store_true",
        help="Use default female voice (Chelsie)"
    )
    parser.add_argument(
        "--male", "-M",
        action="store_true",
        help="Use default male voice (Ryan)"
    )
    
    # Voice cloning
    parser.add_argument(
        "--ref_audio", "-r",
        help="Reference audio file for voice cloning"
    )
    parser.add_argument(
        "--ref_text",
        help="Reference text for voice cloning"
    )
    
    # Voice parameters
    parser.add_argument(
        "--speed", "-s",
        type=float,
        help="Speaking speed (0.5-2.0, default: 1.0)"
    )
    parser.add_argument(
        "--pitch", "-p",
        type=float,
        help="Voice pitch (0.5-2.0, default: 1.0)"
    )
    parser.add_argument(
        "--lang_code", "-l",
        default="en-US",
        help="Language code (default: en-US, use 'zh-CN' for Chinese)"
    )
    parser.add_argument(
        "--temperature", "-t",
        type=float,
        help="Sampling temperature (default: 0.7)"
    )
    
    args = parser.parse_args()
    
    # Run TTS
    return run_tts(args)


if __name__ == "__main__":
    sys.exit(main())


path: scripts/tts_macos.py
content: #!/usr/bin/env python3
"""
Local TTS wrapper for macOS using mlx_audio.

This script provides a simplified interface for Qwen3-TTS on macOS with Apple Silicon.
"""

import argparse
import subprocess
import sys
import os

# Default configurations
DEFAULT_FEMALE_VOICE = "Chelsie"
DEFAULT_MALE_VOICE = "Aiden"

MODELS = {
    "base": "mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit",
    "voicedesign": "mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-8bit",
    "customvoice": "mlx-community/Qwen3-TTS-12Hz-1.7B-CustomVoice-8bit",
    "base-small": "mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit",
    "voicedesign-small": "mlx-community/Qwen3-TTS-12Hz-0.6B-VoiceDesign-8bit",
    "customvoice-small": "mlx-community/Qwen3-TTS-12Hz-0.6B-CustomVoice-8bit",
}

PRESET_VOICES = {
    "female": ["Chelsie", "Breeze"],
    "male": ["Ryan", "Aiden"],
}


def run_tts(args):
    """Run TTS with the given arguments."""
    
    # Build command
    cmd = [
        sys.executable, "-m", "mlx_audio.tts.generate",
        "--text", args.text,
        "--model", MODELS.get(args.model, args.model),
    ]
    
    # Add output path
    if args.output:
        cmd.extend(["--output", args.output])
    
    # Add voice (CustomVoice)
    if args.voice:
        cmd.extend(["--voice", args.voice])
    
    # Add instruct (VoiceDesign)
    if args.instruct:
        cmd.extend(["--instruct", args.instruct])
    
    # Add reference audio for cloning
    if args.ref_audio:
        cmd.extend(["--ref_audio", args.ref_audio])
    if args.ref_text:
        cmd.extend(["--ref_text", args.ref_text])
    
    # Add parameters
    if args.speed is not None:
        cmd.extend(["--speed", str(args.speed)])
    if args.gender:
        cmd.extend(["--gender", args.gender])
    if args.pitch is not None:
        cmd.extend(["--pitch", str(args.pitch)])
    if args.lang_code:
        cmd.extend(["--lang_code", args.lang_code])
    
    print(f"Running: {' '.join(cmd)}")
    result = subprocess.run(cmd)
    return result.returncode


def main():
    parser = argparse.ArgumentParser(
        description="Local TTS using Qwen3-TTS with mlx_audio (macOS)"
    )
    
    # Required arguments
    parser.add_argument("text", help="Text to synthesize")
    
    # Model selection
    parser.add_argument(
        "--model", "-m",
        default="customvoice",
        choices=list(MODELS.keys()),
        help="Model to use (default: customvoice)"
    )
    
    # Output
    parser.add_argument(
        "--output", "-o",
        help="Output audio file path"
    )
    
    # Voice selection (mutually exclusive)
    voice_group = parser.add_mutually_exclusive_group()
    voice_group.add_argument(
        "--voice", "-v",
        help="Preset voice name (for CustomVoice model): Chelsie, Breeze, Ryan, Aiden"
    )
    voice_group.add_argument(
        "--instruct", "-i",
        help="Voice description (for VoiceDesign model), e.g., 'A warm female voice'"
    )
    
    # Quick voice presets
    parser.add_argument(
        "--female", "-f",
        action="store_true",
        help="Use default female voice (Chelsie)"
    )
    parser.add_argument(
        "--male", "-M",
        action="store_true",
        help="Use default male voice (Ryan)"
    )
    
    # Voice cloning
    parser.add_argument(
        "--ref_audio", "-r",
        help="Reference audio file for voice cloning"
    )
    parser.add_argument(
        "--ref_text",
        help="Reference text for voice cloning"
    )
    
    # Voice parameters
    parser.add_argument(
        "--speed", "-s",
        type=float,
        help="Speaking speed (0.5-2.0, default: 1.0)"
    )
    parser.add_argument(
        "--gender", "-g",
        choices=["male", "female"],
        help="Voice gender"
    )
    parser.add_argument(
        "--pitch", "-p",
        type=float,
        help="Voice pitch (0.5-2.0, default: 1.0)"
    )
    parser.add_argument(
        "--lang_code", "-l",
        default="en-US",
        help="Language code (default: en-US, use 'zh-CN' for Chinese)"
    )
    
    args = parser.parse_args()
    
    # Handle quick presets
    if args.female:
        if args.model == "voicedesign":
            args.instruct = args.instruct or "A warm female voice, clear and friendly"
        else:
            args.voice = args.voice or DEFAULT_FEMALE_VOICE
    elif args.male:
        if args.model == "voicedesign":
            args.instruct = args.instruct or "A deep male voice, warm and professional"
        else:
            args.voice = args.voice or DEFAULT_MALE_VOICE
    
    # Validate arguments
    if args.model == "voicedesign" and not args.instruct and not (args.female or args.male):
        print("Warning: VoiceDesign model requires --instruct parameter. Use --female/--male for defaults.")
    
    if args.model == "customvoice" and not args.voice and not args.female and not args.male:
        print("Warning: CustomVoice model works best with --voice parameter. Use --female/--male for defaults.")
    
    # Run TTS
    return run_tts(args)


if __name__ == "__main__":
    sys.exit(main())


path: tests/test_integration.py
content: #!/usr/bin/env python3
"""
Integration tests for local-tts.

These tests actually run TTS generation. They require:
- For macOS: mlx-audio installed
- For Linux/Windows: qwen-tts and torch installed

Skipped by default in CI. Run with: python -m pytest tests/test_integration.py -v
"""

import unittest
import os
import sys
import tempfile
import subprocess


class TestMacOSTTS(unittest.TestCase):
    """Integration tests for macOS TTS (requires mlx-audio)."""
    
    @classmethod
    def setUpClass(cls):
        """Check if running on macOS with Apple Silicon."""
        import platform
        cls.is_macos = platform.system() == 'Darwin'
        cls.is_apple_silicon = platform.machine() in ['arm64', 'aarch64']
        cls.has_mlx = cls._check_mlx()
    
    @staticmethod
    def _check_mlx():
        """Check if mlx-audio is installed."""
        try:
            import mlx_audio
            return True
        except ImportError:
            return False
    
    def setUp(self):
        """Skip if not on macOS or mlx not available."""
        if not self.is_macos:
            self.skipTest("macOS-specific test")
        if not self.has_mlx:
            self.skipTest("mlx-audio not installed (pip install mlx-audio)")
    
    def test_basic_tts_command(self):
        """Test basic TTS generation on macOS."""
        with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
            output_path = f.name
        
        try:
            cmd = [
                sys.executable, '-m', 'mlx_audio.tts.generate',
                '--text', 'Hello world',
                '--model', 'mlx-community/Qwen3-TTS-12Hz-0.6B-Base-8bit',
                '--output', output_path
            ]
            
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
            
            # Check command succeeded
            self.assertEqual(result.returncode, 0, 
                           f"Command failed: {result.stderr}")
            
            # Check output file exists and has content
            self.assertTrue(os.path.exists(output_path))
            self.assertGreater(os.path.getsize(output_path), 1000,
                             "Output file too small, likely failed")
            
        finally:
            if os.path.exists(output_path):
                os.unlink(output_path)
    
    def test_wrapper_script_import(self):
        """Test that wrapper script can be imported."""
        script_path = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'tts_macos.py')
        
        # Just check the script exists and is valid Python
        with open(script_path, 'r', encoding='utf-8') as f:
            code = f.read()
        
        # Should compile without errors
        compile(code, script_path, 'exec')


class TestLinuxWindowTTS(unittest.TestCase):
    """Integration tests for Linux/Windows TTS (requires qwen-tts)."""
    
    @classmethod
    def setUpClass(cls):
        """Check if qwen-tts is available."""
        cls.has_qwen_tts = cls._check_qwen_tts()
        cls.has_cuda = cls._check_cuda()
    
    @staticmethod
    def _check_qwen_tts():
        """Check if qwen-tts is installed."""
        try:
            import qwen_tts
            return True
        except ImportError:
            return False
    
    @staticmethod
    def _check_cuda():
        """Check if CUDA is available."""
        try:
            import torch
            return torch.cuda.is_available()
        except ImportError:
            return False
    
    def setUp(self):
        """Skip if qwen-tts not available."""
        if not self.has_qwen_tts:
            self.skipTest("qwen-tts not installed (pip install qwen-tts)")
    
    def test_basic_tts_generation(self):
        """Test basic TTS generation with qwen-tts."""
        with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
            output_path = f.name
        
        try:
            cmd = [
                sys.executable,
                os.path.join(os.path.dirname(__file__), '..', 'scripts', 'tts_linux.py'),
                'Hello world',
                '--model', 'customvoice-small',  # Use smaller model for faster test
                '--female',
                '--output', output_path
            ]
            
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
            
            # Note: This may fail in CI without GPU, so we just check it runs
            # without crashing (return code 0 means success)
            if result.returncode == 0:
                self.assertTrue(os.path.exists(output_path))
                self.assertGreater(os.path.getsize(output_path), 1000)
            else:
                # In CI without GPU, this is expected to fail
                print(f"TTS generation skipped (likely no GPU): {result.stderr}")
                self.skipTest("GPU not available for TTS generation")
                
        finally:
            if os.path.exists(output_path):
                os.unlink(output_path)
    
    def test_wrapper_script_import(self):
        """Test that wrapper script can be imported."""
        script_path = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'tts_linux.py')
        
        # Just check the script exists and is valid Python
        with open(script_path, 'r', encoding='utf-8') as f:
            code = f.read()
        
        # Should compile without errors
        compile(code, script_path, 'exec')
    
    def test_device_detection(self):
        """Test device detection logic."""
        try:
            import torch
            # Should be able to detect CUDA if available
            device = "cuda" if torch.cuda.is_available() else "cpu"
            self.assertIn(device, ["cuda", "cpu"])
        except ImportError:
            self.skipTest("torch not installed")


class TestPrivacySecurityDocs(unittest.TestCase):
    """Tests for privacy and security documentation."""
    
    def test_privacy_doc_exists(self):
        """Test privacy/security documentation exists."""
        doc_path = os.path.join(os.path.dirname(__file__), '..', 'references', 'privacy_security.md')
        self.assertTrue(os.path.exists(doc_path))
    
    def test_privacy_keywords(self):
        """Test privacy doc contains important keywords for SEO."""
        doc_path = os.path.join(os.path.dirname(__file__), '..', 'references', 'privacy_security.md')
        
        with open(doc_path, 'r', encoding='utf-8') as f:
            content = f.read().lower()
        
        # SEO-important keywords
        keywords = [
            'privacy', 'security', 'offline', 'local', 'data privacy',
            'gdpr', 'hipaa', 'compliance', 'confidential', 'encryption'
        ]
        
        for keyword in keywords:
            self.assertIn(keyword, content, f"Missing important keyword: {keyword}")
    
    def test_skill_md_privacy_mentions(self):
        """Test that SKILL.md mentions privacy benefits."""
        skill_path = os.path.join(os.path.dirname(__file__), '..', 'SKILL.md')
        
        with open(skill_path, 'r', encoding='utf-8') as f:
            content = f.read().lower()
        
        self.assertIn('privacy', content)
        self.assertIn('security', content)
        self.assertIn('offline', content)


if __name__ == '__main__':
    unittest.main()


path: tests/test_unit.py
content: #!/usr/bin/env python3
"""
Unit tests for local-tts scripts.

These tests verify that the TTS scripts can be imported and have correct structure.
For actual TTS generation tests, see test_integration.py
"""

import unittest
import sys
import os

# Add scripts directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))


class TestScriptImports(unittest.TestCase):
    """Test that scripts can be imported."""
    
    def test_tts_macos_imports(self):
        """Test that tts_macos.py is valid Python syntax."""
        script_path = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'tts_macos.py')
        with open(script_path, 'r', encoding='utf-8') as f:
            code = f.read()
        # Should compile without errors
        compile(code, script_path, 'exec')
    
    def test_tts_linux_imports(self):
        """Test that tts_linux.py is valid Python syntax."""
        script_path = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'tts_linux.py')
        with open(script_path, 'r', encoding='utf-8') as f:
            code = f.read()
        # Should compile without errors
        compile(code, script_path, 'exec')


class TestScriptStructure(unittest.TestCase):
    """Test script structure and constants."""
    
    def test_macos_models_defined(self):
        """Test that macOS script has models defined."""
        script_path = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'tts_macos.py')
        with open(script_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        self.assertIn('MODELS', content)
        self.assertIn('base', content)
        self.assertIn('customvoice', content)
        self.assertIn('voicedesign', content)
    
    def test_linux_models_defined(self):
        """Test that Linux script has models defined."""
        script_path = os.path.join(os.path.dirname(__file__), '..', 'scripts', 'tts_linux.py')
        with open(script_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        self.assertIn('MODELS', content)
        self.assertIn('base', content)
        self.assertIn('customvoice', content)
        self.assertIn('voicedesign', content)
    
    def test_default_voices_defined(self):
        """Test that default voices are defined."""
        for script in ['tts_macos.py', 'tts_linux.py']:
            script_path = os.path.join(os.path.dirname(__file__), '..', 'scripts', script)
            with open(script_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            self.assertIn('DEFAULT_FEMALE_VOICE', content)
            self.assertIn('DEFAULT_MALE_VOICE', content)
            self.assertIn('Chelsie', content)
            self.assertIn('Aiden', content)
    
    def test_argument_parsing(self):
        """Test that scripts have argument parsing."""
        for script in ['tts_macos.py', 'tts_linux.py']:
            script_path = os.path.join(os.path.dirname(__file__), '..', 'scripts', script)
            with open(script_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            self.assertIn('argparse', content)
            self.assertIn('text', content)  # 'text' argument (positional)
            self.assertIn('--voice', content)
            self.assertIn('--model', content)


class TestDocumentation(unittest.TestCase):
    """Test that documentation exists and is valid."""
    
    def test_skill_md_exists(self):
        """Test that SKILL.md exists."""
        skill_path = os.path.join(os.path.dirname(__file__), '..', 'SKILL.md')
        self.assertTrue(os.path.exists(skill_path))
    
    def test_skill_md_has_frontmatter(self):
        """Test that SKILL.md has YAML frontmatter."""
        skill_path = os.path.join(os.path.dirname(__file__), '..', 'SKILL.md')
        with open(skill_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        self.assertTrue(content.startswith('---'))
        self.assertIn('name:', content)
        self.assertIn('description:', content)
    
    def test_references_exist(self):
        """Test that reference files exist."""
        ref_dir = os.path.join(os.path.dirname(__file__), '..', 'references')
        self.assertTrue(os.path.exists(ref_dir))
        
        expected_files = [
            'macos_mlx_audio.md',
            'linux_windows_transformers.md',
            'privacy_security.md'
        ]
        
        for filename in expected_files:
            filepath = os.path.join(ref_dir, filename)
            self.assertTrue(os.path.exists(filepath), f"Missing reference file: {filename}")
    
    def test_privacy_security_content(self):
        """Test that privacy/security doc has required content."""
        doc_path = os.path.join(os.path.dirname(__file__), '..', 'references', 'privacy_security.md')
        with open(doc_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        self.assertIn('privacy', content.lower())
        self.assertIn('security', content.lower())
        # Check for GDPR or compliance mention
        has_gdpr_or_compliance = 'GDPR' in content or 'compliance' in content.lower()
        self.assertTrue(has_gdpr_or_compliance, "Should mention GDPR or compliance")


class TestPackageJson(unittest.TestCase):
    """Test package.json metadata."""
    
    def test_package_json_exists(self):
        """Test that package.json exists."""
        json_path = os.path.join(os.path.dirname(__file__), '..', 'package.json')
        self.assertTrue(os.path.exists(json_path))
    
    def test_package_json_valid(self):
        """Test that package.json is valid JSON."""
        import json
        json_path = os.path.join(os.path.dirname(__file__), '..', 'package.json')
        
        with open(json_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        self.assertIn('name', data)
        self.assertIn('version', data)
        self.assertIn('description', data)
        self.assertEqual(data['name'], 'local-tts')
    
    def test_version_matches(self):
        """Test that VERSION file matches package.json."""
        import json
        
        json_path = os.path.join(os.path.dirname(__file__), '..', 'package.json')
        version_path = os.path.join(os.path.dirname(__file__), '..', 'VERSION')
        
        with open(json_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        with open(version_path, 'r', encoding='utf-8') as f:
            version = f.read().strip()
        
        self.assertEqual(data['version'], version)


class TestScriptsExecutable(unittest.TestCase):
    """Test that scripts are executable."""
    
    def test_scripts_executable(self):
        """Test that scripts have executable permissions (Unix only)."""
        if os.name == 'nt':  # Windows
            self.skipTest("Executable permissions not applicable on Windows")
        
        scripts_dir = os.path.join(os.path.dirname(__file__), '..', 'scripts')
        for script in ['tts_macos.py', 'tts_linux.py']:
            script_path = os.path.join(scripts_dir, script)
            if os.path.exists(script_path):
                self.assertTrue(os.access(script_path, os.X_OK),
                               f"{script} should be executable")


if __name__ == '__main__':
    unittest.main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)