第28章 Voicebox 生成历史
第28章:生成历史
本章节介绍 Voicebox 中生成历史追踪机制的工作原理。
概述
历史模块追踪所有生成的音频,提供过去生成记录的可搜索日志。每条记录都会存储文本、设置参数以及对音频文件的引用。
数据模型
生成数据表
```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=lambda: datetime.now(UTC)) ```
每次生成还可以拥有多个版本,即应用了不同效果链的处理变体。原始(清洁)版本及任意数量的处理版本都存储在单独的 `generation_versions` 表中。详情请参阅“效果流水线”。
文件存储
生成的音频文件存储于指定目录中:
核心功能
创建生成记录
TTS 生成音频后,系统会创建一条历史记录:
```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.now(UTC), ) db.add(db_generation) db.commit() return GenerationResponse.model_validate(db_generation) ```
列出生成记录
支持过滤和分页:
```python async def list_generations( query: HistoryQuery, db: Session, ) -> HistoryListResponse: # 构建包含配置文件名称关联的查询 q = db.query( DBGeneration, DBVoiceProfile.name.label('profile_name') ).join( DBVoiceProfile, DBGeneration.profile_id == DBVoiceProfile.id ) # 应用过滤器 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}%")) # 排序和分页 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) ```
获取统计数据
为仪表盘提供聚合统计数据:
```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 }, } ```
删除记录
删除某条生成记录会同时移除数据库记录及对应的音频文件:
```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 # 删除音频文件 audio_path = Path(generation.audio_path) if audio_path.exists(): audio_path.unlink() # 删除数据库记录 db.delete(generation) db.commit() return True ```
级联删除
删除某个配置文件时,其名下所有生成记录也会被一并删除:
```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) ```
导出/导入
导出生成记录
生成记录可导出为 ZIP 压缩包,内含: - `generation.json` - `audio.wav`
导入生成记录
导入流程如下:
1. 解压 ZIP 文件 2. 验证元数据和音频 3. 生成新的 ID 4. 将音频复制到 generations 目录 5. 创建数据库记录
API 接口
| 方法 | 端点 | 说明 | | :--- | :--- | :--- | | GET | /history | 获取带过滤条件的生成列表 | | GET | /history/stats | 获取聚合统计数据 | | GET | /history/{id} | 根据 ID 获取生成记录 | | DELETE | /history/{id} | 删除生成记录 | | GET | /history/{id}/export | 导出为 ZIP | | GET | /history/{id}/export-audio | 仅导出音频(?format=wav\|mp3;mp3 需 ffmpeg,否则返回 503) | | POST | /history/import | 从 ZIP 导入 |
查询参数
`GET /history?profile_id=uuid&search=hello&limit=50&offset=0`
| 参数 | 类型 | 默认值 | 说明 | | :--- | :--- | :--- | :--- | | profile_id | string | null | 按配置文件过滤 | | search | string | null | 在文本中搜索 | | limit | int | 50 | 每页结果数 | | offset | int | 0 | 分页偏移量 |
响应结构
```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 } ```
在故事中的使用
生成记录可被添加到故事中,以支持多声部叙事。故事系统通过 ID 引用生成记录:
```python class StoryItem(Base): generation_id = Column(String, ForeignKey("generations.id")) ```
这允许同一生成记录在多个故事中复用,而无需复制音频文件。
存储考量
磁盘占用
每次生成都会创建一个 WAV 文件。对于 24kHz 的 10 秒片段:
- 单声道 16-bit 模式下,每个文件约占 480KB
清理策略
建议实现以下功能:
- 自动清理旧记录 - 配置每个存储配额 - 归档压缩
相关链接: - 效果流水线:音频后期处理效果与生成版本化 - 故事与时间轴:Voicebox 中多声部时间轴编辑器的工作方式