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:
- Extract ZIP archive
- Validate metadata and audio
- Create new generation ID
- Copy audio to generations directory
- 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