---
title: "Generation History"
description: "How generation history tracking works in Voicebox"
---

## Overview

The history module tracks all generated audio, providing a searchable record of past generations. Each generation stores the text, settings, and a reference to the audio file.

## Data Model

### Generation Table

```python
class Generation(Base):
    __tablename__ = "generations"

    id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
    profile_id = Column(String, ForeignKey("profiles.id"), nullable=False)
    text = Column(Text, nullable=False)
    language = Column(String, default="en")
    audio_path = Column(String, nullable=True)
    duration = Column(Float, nullable=True)
    seed = Column(Integer)
    instruct = Column(Text)
    engine = Column(String, default="qwen")
    model_size = Column(String, nullable=True)
    status = Column(String, default="completed")  # pending | completed | failed
    error = Column(Text, nullable=True)
    is_favorited = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)
```

Each generation can also have multiple **generation versions** — processed variants with different effects chains applied. The original (`clean`) version plus any number of processed versions live in a separate `generation_versions` table. See [Effects Pipeline](/developer/effects-pipeline).

## File Storage

Generated audio is stored in:

<Files>
  <Folder name="data" defaultOpen>
    <Folder name="generations">
      <File name="{generation_id}.wav" />
    </Folder>
  </Folder>
</Files>

## Core Functions

### Creating a Generation Record

After TTS generates audio, a history entry is created:

```python
async def create_generation(
    profile_id: str,
    text: str,
    language: str,
    audio_path: str,
    duration: float,
    seed: Optional[int],
    db: Session,
    instruct: Optional[str] = None,
) -> GenerationResponse:
    db_generation = DBGeneration(
        id=str(uuid.uuid4()),
        profile_id=profile_id,
        text=text,
        language=language,
        audio_path=audio_path,
        duration=duration,
        seed=seed,
        instruct=instruct,
        created_at=datetime.utcnow(),
    )
    
    db.add(db_generation)
    db.commit()
    
    return GenerationResponse.model_validate(db_generation)
```

### Listing Generations

Supports filtering and pagination:

```python
async def list_generations(
    query: HistoryQuery,
    db: Session,
) -> HistoryListResponse:
    # Build query with profile name join
    q = db.query(
        DBGeneration,
        DBVoiceProfile.name.label('profile_name')
    ).join(
        DBVoiceProfile,
        DBGeneration.profile_id == DBVoiceProfile.id
    )
    
    # Apply filters
    if query.profile_id:
        q = q.filter(DBGeneration.profile_id == query.profile_id)
    
    if query.search:
        q = q.filter(DBGeneration.text.like(f"%{query.search}%"))
    
    # Order and paginate
    total = q.count()
    q = q.order_by(DBGeneration.created_at.desc())
    q = q.offset(query.offset).limit(query.limit)
    
    return HistoryListResponse(items=results, total=total)
```

### Getting Statistics

Aggregate statistics for the dashboard:

```python
async def get_generation_stats(db: Session) -> dict:
    total = db.query(func.count(DBGeneration.id)).scalar()
    total_duration = db.query(func.sum(DBGeneration.duration)).scalar()
    
    by_profile = db.query(
        DBGeneration.profile_id,
        func.count(DBGeneration.id).label('count')
    ).group_by(DBGeneration.profile_id).all()
    
    return {
        "total_generations": total,
        "total_duration_seconds": total_duration,
        "generations_by_profile": {
            profile_id: count for profile_id, count in by_profile
        },
    }
```

## Deletion

Deleting a generation removes both the database record and audio file:

```python
async def delete_generation(generation_id: str, db: Session) -> bool:
    generation = db.query(DBGeneration).filter_by(id=generation_id).first()
    if not generation:
        return False
    
    # Delete audio file
    audio_path = Path(generation.audio_path)
    if audio_path.exists():
        audio_path.unlink()
    
    # Delete database record
    db.delete(generation)
    db.commit()
    
    return True
```

### Cascade Delete

When deleting a profile, all its generations are also deleted:

```python
async def delete_generations_by_profile(profile_id: str, db: Session) -> int:
    generations = db.query(DBGeneration).filter_by(profile_id=profile_id).all()
    
    for generation in generations:
        Path(generation.audio_path).unlink(missing_ok=True)
        db.delete(generation)
    
    db.commit()
    return len(generations)
```

## Export/Import

### Exporting a Generation

Generations can be exported as ZIP archives:

<Files>
  <Folder name="generation_export.zip" defaultOpen>
    <File name="generation.json" />
    <File name="audio.wav" />
  </Folder>
</Files>

### Importing a Generation

The import process:

1. Extract ZIP archive
2. Validate metadata and audio
3. Create new generation ID
4. Copy audio to generations directory
5. Create database record

## API Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/history` | List generations with filters |
| GET | `/history/stats` | Get aggregate statistics |
| GET | `/history/{id}` | Get generation by ID |
| DELETE | `/history/{id}` | Delete generation |
| GET | `/history/{id}/export` | Export as ZIP |
| GET | `/history/{id}/export-audio` | Export audio only |
| POST | `/history/import` | Import from ZIP |

### Query Parameters

```
GET /history?profile_id=uuid&search=hello&limit=50&offset=0
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `profile_id` | string | null | Filter by profile |
| `search` | string | null | Search in text |
| `limit` | int | 50 | Results per page |
| `offset` | int | 0 | Pagination offset |

### Response Schema

```json
{
  "items": [
    {
      "id": "uuid",
      "profile_id": "uuid",
      "profile_name": "My Voice",
      "text": "Hello world",
      "language": "en",
      "audio_path": "/path/to/audio.wav",
      "duration": 1.5,
      "seed": 42,
      "instruct": null,
      "engine": "qwen",
      "model_size": "1.7B",
      "status": "completed",
      "error": null,
      "is_favorited": false,
      "created_at": "2026-04-18T10:30:00Z"
    }
  ],
  "total": 150
}
```

## Usage in Stories

Generations can be added to stories for multi-voice narratives. The story system references generations by ID:

```python
class StoryItem(Base):
    generation_id = Column(String, ForeignKey("generations.id"))
```

This allows the same generation to be reused across multiple stories without duplicating audio files.

## Storage Considerations

### Disk Usage

Each generation creates a WAV file. For a 10-second clip at 24kHz:
- ~480KB per file (mono, 16-bit)

### Cleanup Strategy

Consider implementing:
- Automatic cleanup of old generations
- Storage quota per profile
- Compression for archival
