276 lines
7.0 KiB
Plaintext
276 lines
7.0 KiB
Plaintext
---
|
|
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
|