--- 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) profile_id = Column(String, ForeignKey("profiles.id")) text = Column(Text, nullable=False) language = Column(String, default="en") audio_path = Column(String, nullable=False) duration = Column(Float, nullable=False) seed = Column(Integer) instruct = Column(Text) created_at = Column(DateTime) ``` ## File Storage Generated audio is stored in: ``` data/ └── generations/ └── {generation_id}.wav ``` ## 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: ``` generation_export.zip ├── generation.json # Metadata └── audio.wav # Audio file ``` ### 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, "created_at": "2024-01-15T10: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