mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 22:30:40 -07:00
Add initial setup for Fumadocs documentation migration
- Created new directory structure for documentation under `/docs2`. - Added `.gitignore` to exclude build artifacts and dependencies. - Introduced `package.json`, `next.config.mjs`, and `postcss.config.mjs` for project configuration. - Implemented MDX components in `mdx-components.tsx` for rendering documentation. - Migrated existing documentation content and created new files for auto-updater and other features. - Established compatibility layer for Mintlify components in `mintlify-compat.tsx`. - Set up OpenAPI documentation in `openapi.json`. - Updated README and migration guide to reflect new structure and usage instructions. - Ensured all components and pages are ready for development and deployment with Fumadocs.
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user