This guide covers the development setup and workflow for TTS Studio.
- Project Structure
- Environment Setup
- Development Workflow
- Code Quality Tools
- Testing
- Git Workflow
- Troubleshooting
TTS Studio uses a monorepo structure with hexagonal architecture:
tts-studio/
├── apps/
│ ├── core/ # Python library (hexagonal architecture)
│ │ ├── src/
│ │ │ ├── domain/ # Business logic (NO external dependencies)
│ │ │ ├── app/ # Use cases (orchestration)
│ │ │ ├── infra/ # Adapters (implementations)
│ │ │ └── api/ # Python API (entry point)
│ │ ├── tests/
│ │ │ ├── domain/ # Domain tests (mocks only)
│ │ │ ├── app/ # Application tests (mocked ports)
│ │ │ ├── infra/ # Infrastructure tests (real adapters)
│ │ │ ├── integration/ # End-to-end tests
│ │ │ └── pbt/ # Property-based tests
│ │ ├── setup.py
│ │ ├── pyproject.toml
│ │ └── requirements.txt
│ └── desktop/ # Tauri desktop app (coming soon)
├── config/ # Configuration files
├── data/ # Data directory (gitignored)
├── docs/ # Documentation
└── examples/ # Usage examples
Domain Layer (apps/core/src/domain/):
- Pure business logic
- NO external dependencies
- Defines ports (interfaces)
- Contains models, services, exceptions
Application Layer (apps/core/src/app/):
- Use cases (orchestration)
- DTOs (data transfer objects)
- Uses ports, NOT adapters
Infrastructure Layer (apps/core/src/infra/):
- Adapters (implementations)
- TTS engines (Qwen3, XTTS, etc.)
- Audio processing (librosa)
- Storage (files, databases)
API Layer (apps/core/src/api/):
- Entry points
- Dependency injection
- Python API for Tauri backend
For more details, see HEXAGONAL_ARCHITECTURE.md.
- Python 3.10 (recommended)
- Git
- pip (comes with Python)
We use Python's built-in venv for dependency isolation because:
- Isolation: Keeps project dependencies separate from system Python
- Reproducibility: Ensures consistent environments across developers
- No conflicts: Prevents version conflicts with other Python projects
- Lightweight: Native to Python, no additional tools needed
- Standard: Industry standard for Python projects
The easiest way to set up your development environment:
./setup.shThis script will:
- Check Python 3.10 is installed
- Create a virtual environment in
venv/ - Activate the virtual environment
- Upgrade pip to the latest version
- Install all development dependencies
- Install pre-commit hooks
If you prefer to set up manually or need more control:
cd apps/core# Using Python 3.10 (recommended)
python3.10 -m venv venv
# Or using default python3
python3 -m venv venvOn macOS/Linux:
source venv/bin/activateOn Windows:
venv\Scripts\activateYou should see (venv) in your terminal prompt.
pip install --upgrade pip# Install from requirements.txt
pip install -r requirements.txt
# Install project in editable mode
pip install -e .# Navigate back to repo root
cd ../..
# Install hooks
pre-commit install
pre-commit install --hook-type commit-msg
pre-commit install --hook-type pre-push# Check Python version
python --version
# Check installed packages
pip list
# Test pre-commit
pre-commit run --all-files
# Run tests
cd apps/core
pytest tests/ -v-
Activate virtual environment
source venv/bin/activate -
Pull latest changes
git pull origin master
-
Create feature branch
git checkout -b feat/your-feature-name
-
Make your changes
- Write code
- Write tests
- Update documentation
-
Run quality checks
make pre-commit # or pre-commit run --all-files -
Run tests
make test # or pytest
-
Commit changes
git add . git commit -m "feat: add your feature"
-
Push and create PR
git push origin feat/your-feature-name
We provide a Makefile with common commands:
make help # Show all available commands
make setup # Run automated setup
make test # Run tests with coverage
make test-fast # Run tests without coverage
make lint # Run linter
make format # Format code
make type-check # Run type checker
make pre-commit # Run all pre-commit hooks
make clean # Clean generated filesBlack automatically formats your code to a consistent style.
# Format all code
black src/ tests/
# Check without modifying
black --check src/ tests/
# Format specific file
black src/voice_clone/audio.pyConfiguration: See [tool.black] in pyproject.toml
Ruff is a fast Python linter that catches common errors and style issues.
# Lint all code
ruff check src/ tests/
# Auto-fix issues
ruff check --fix src/ tests/
# Lint specific file
ruff check src/voice_clone/audio.pyConfiguration: See [tool.ruff] in pyproject.toml
MyPy performs static type checking to catch type-related bugs.
# Type check all code
mypy src/
# Type check specific file
mypy src/voice_clone/audio.pyConfiguration: See [tool.mypy] in pyproject.toml
Pre-commit hooks run automatically before commits and pushes:
On every commit:
- Black formatting
- Ruff linting
- MyPy type checking
- Trailing whitespace removal
- End-of-file fixing
- YAML validation
- Large file detection
- Merge conflict detection
- Private key detection
On commit message:
- Conventional Commits validation
On push:
- Full test suite with coverage
# Run all hooks manually
pre-commit run --all-files
# Run specific hook
pre-commit run black --all-files
# Update hooks to latest versions
pre-commit autoupdate
# Skip hooks (not recommended)
git commit --no-verifyTests are organized by hexagonal architecture layers:
apps/core/tests/
├── domain/ # Domain tests (NO infrastructure)
│ ├── models/ # Test entities and value objects
│ └── services/ # Test domain services with mocks
├── app/ # Application tests (mocked ports)
│ └── use_cases/ # Test use cases with mocked adapters
├── infra/ # Infrastructure tests (real adapters)
│ ├── engines/ # Test TTS engine adapters
│ ├── audio/ # Test audio processing adapters
│ └── persistence/ # Test storage adapters
├── integration/ # End-to-end tests
│ ├── test_end_to_end.py
│ └── test_hexagonal_architecture.py
└── pbt/ # Property-based tests
├── test_domain_properties.py
└── test_use_case_properties.py
# Navigate to core library
cd apps/core
# Run all tests
pytest
# Run with coverage
pytest --cov=src --cov-report=term-missing
# Run specific layer
pytest tests/domain/ # Domain tests only
pytest tests/app/ # Application tests only
pytest tests/infra/ # Infrastructure tests only
pytest tests/integration/ # Integration tests only
pytest tests/pbt/ # Property-based tests only
# Run specific test file
pytest tests/domain/models/test_voice_profile.py
# Run specific test
pytest tests/domain/models/test_voice_profile.py::test_voice_profile_creation
# Run with verbose output
pytest -v
# Run and stop on first failure
pytest -x
# Run tests matching pattern
pytest -k "test_voice"Test business logic without infrastructure:
# tests/domain/services/test_voice_cloning.py
from unittest.mock import Mock
from domain.ports.audio_processor import AudioProcessor
from domain.services.voice_cloning import VoiceCloningService
def test_create_profile_from_samples():
"""Test domain service with mocked port."""
# Mock the audio processor port
mock_processor = Mock(spec=AudioProcessor)
mock_processor.validate_sample.return_value = True
mock_processor.process_sample.return_value = AudioSample(...)
# Test domain service
service = VoiceCloningService(mock_processor)
profile = service.create_profile_from_samples("test", [Path("sample.wav")])
assert profile.name == "test"
assert len(profile.samples) == 1Test use cases with mocked adapters:
# tests/app/use_cases/test_create_voice_profile.py
from unittest.mock import Mock
from app.use_cases.create_voice_profile import CreateVoiceProfileUseCase
def test_create_voice_profile_use_case():
"""Test use case with mocked ports."""
mock_processor = Mock(spec=AudioProcessor)
mock_repository = Mock(spec=ProfileRepository)
uc = CreateVoiceProfileUseCase(mock_processor, mock_repository)
result = uc.execute("test", [Path("sample.wav")])
assert result.name == "test"
mock_repository.save.assert_called_once()Test adapters with real implementations:
# tests/infra/engines/test_qwen3_adapter.py
from infra.engines.qwen3.adapter import Qwen3Adapter
def test_qwen3_adapter_generates_audio():
"""Test adapter with real Qwen3."""
config = {'model_name': 'Qwen/Qwen3-TTS-12Hz-1.7B-Base'}
adapter = Qwen3Adapter(config)
output = adapter.generate_audio(
text="Hello world",
profile_id="test_profile",
output_path=Path("test_output.wav")
)
assert output.exists()
assert output.stat().st_size > 0Test complete workflows:
# tests/integration/test_end_to_end.py
from api.studio import TTSStudio
def test_create_profile_and_generate_audio():
"""Test complete workflow."""
studio = TTSStudio()
# Create profile
profile_result = studio.create_voice_profile(
name='test_voice',
sample_paths=['data/samples/sample1.wav']
)
assert profile_result['status'] == 'success'
# Generate audio
audio_result = studio.generate_audio(
profile_id='test_voice',
text='Hello world',
output_path='output.wav'
)
assert audio_result['status'] == 'success'Test properties that should always hold:
# tests/pbt/test_domain_properties.py
from hypothesis import given, strategies as st
from domain.models.voice_profile import VoiceProfile
@given(st.text(min_size=1, max_size=50))
def test_voice_profile_name_preserved(name):
"""Test that profile name is always preserved."""
profile = VoiceProfile(id=name, name=name, samples=[])
assert profile.name == name
@given(st.lists(st.floats(min_value=0.1, max_value=30.0), min_size=1, max_size=10))
def test_voice_profile_duration_sum(durations):
"""Test that total duration equals sum of sample durations."""
samples = [AudioSample(duration=d, ...) for d in durations]
profile = VoiceProfile(id="test", name="test", samples=samples)
assert abs(profile.total_duration - sum(durations)) < 0.01- Minimum coverage: 80%
- Pre-push hook will fail if coverage is below threshold
- View coverage report:
pytest --cov=src --cov-report=html && open htmlcov/index.html
- Domain tests: Use mocks for all ports, test pure business logic
- Application tests: Mock infrastructure, test orchestration
- Infrastructure tests: Use real implementations, test integration
- Integration tests: Test complete workflows end-to-end
- Property-based tests: Test invariants that should always hold
- Test naming: Use descriptive names that explain what is being tested
- Test isolation: Each test should be independent
- Test data: Use fixtures for common test data
- Test coverage: Aim for >80% coverage, but focus on critical paths
feat/feature-name- New featuresfix/bug-description- Bug fixesdocs/what-changed- Documentation updatesrefactor/what-changed- Code refactoringtest/what-added- Test additions
We use Conventional Commits:
Format:
type(scope): description
[optional body]
[optional footer]
Types:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasksperf: Performance improvementsci: CI/CD changesbuild: Build system changes
Examples:
git commit -m "feat: add voice cloning functionality"
git commit -m "fix: resolve audio processing bug in stereo files"
git commit -m "docs: update installation instructions for macOS"
git commit -m "test: add unit tests for audio loader"Multi-line commits:
git commit -m "feat: add voice cloning functionality
- Implement audio sample processing
- Add Qwen3-TTS model integration
- Create CLI interface for cloning
Closes #123"- Create feature branch from
master - Make changes and commit using conventional commits
- Push branch to remote
- Create Pull Request on GitHub
- Wait for CI checks to pass
- Request review from maintainers
- Address review feedback
- Merge when approved
Problem: command not found: python3.10
Solution: Install Python 3.10 using pyenv or your system package manager:
# Using pyenv
pyenv install 3.10
pyenv local 3.10
# Using Homebrew (macOS)
brew install python@3.10Problem: Virtual environment not activating
Solution: Make sure you're in the project directory and run:
source venv/bin/activateProblem: Wrong Python version in venv
Solution: Delete and recreate the virtual environment:
rm -rf venv
python3.10 -m venv venv
source venv/bin/activate
pip install -r requirements.txtProblem: Pre-commit hooks not running
Solution: Reinstall hooks:
pre-commit uninstall
pre-commit install
pre-commit install --hook-type commit-msg
pre-commit install --hook-type pre-pushProblem: Hook installation fails with core.hooksPath error
Solution: Unset the Git config:
git config --unset-all core.hooksPath
pre-commit installProblem: Hooks are too slow
Solution: Pre-commit caches environments. First run is slow, subsequent runs are fast.
Problem: Tests fail with import errors
Solution: Install project in editable mode:
pip install -e .Problem: Coverage below threshold
Solution: Add more tests or adjust threshold in pyproject.toml
Problem: Package conflicts
Solution: Recreate virtual environment:
deactivate
rm -rf venv
./setup.sh- Always use virtual environment - Never install packages globally
- Commit often - Small, focused commits are better
- Write tests first - TDD helps design better code
- Run pre-commit before pushing - Catch issues early
- Keep dependencies minimal - Only add what you need
- Document as you go - Update docs with code changes
- Review your own PR - Catch obvious issues before review