Sonna
Developer

Generation History

How generation history tracking works in Sonna

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

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.

File Storage

Generated audio is stored in:

Core Functions

Creating a Generation Record

After TTS generates audio, a history entry is created:

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:

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:

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:

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:

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:

generation.json
audio.wav

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

MethodEndpointDescription
GET/historyList generations with filters
GET/history/statsGet aggregate statistics
GET/history/{id}Get generation by ID
DELETE/history/{id}Delete generation
GET/history/{id}/exportExport as ZIP
GET/history/{id}/export-audioExport audio only
POST/history/importImport from ZIP

Query Parameters

GET /history?profile_id=uuid&search=hello&limit=50&offset=0
ParameterTypeDefaultDescription
profile_idstringnullFilter by profile
searchstringnullSearch in text
limitint50Results per page
offsetint0Pagination offset

Response Schema

{
  "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:

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

On this page