Generation History

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:

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

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