mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 09:05:17 -07:00
Add post-processing audio effects system
Adds a full effects pipeline powered by Spotify's pedalboard library, enabling users to apply professional DSP effects (flanger, reverb, delay, compressor, pitch shift, filters, gain) to generated audio. Key features: - Effects chain editor with drag-and-drop reordering (dnd-kit) - Generation versions: clean copy always saved, processed versions created on top - Built-in presets (Robotic, Radio, Echo Chamber, Deep Voice) + custom user presets - Per-profile default effects chain (auto-applied to new generations) - Per-generation effects override from the generation form - Apply effects to existing generations from history (creates new version) - Ephemeral preview endpoint for auditioning effects without persisting - Dedicated Effects sidebar tab with preset management and live preview - Version switcher in history cards with expandable panel - Backward-compatible: existing generations backfilled as clean versions
This commit is contained in:
+53
-8
@@ -10,8 +10,8 @@ from pathlib import Path
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
|
||||
from .models import GenerationRequest, GenerationResponse, HistoryQuery, HistoryResponse, HistoryListResponse
|
||||
from .database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile
|
||||
from .models import GenerationRequest, GenerationResponse, HistoryQuery, HistoryResponse, HistoryListResponse, GenerationVersionResponse, EffectConfig
|
||||
from .database import Generation as DBGeneration, GenerationVersion as DBGenerationVersion, VoiceProfile as DBVoiceProfile
|
||||
from . import config
|
||||
|
||||
|
||||
@@ -20,6 +20,43 @@ def _get_generations_dir() -> Path:
|
||||
return config.get_generations_dir()
|
||||
|
||||
|
||||
def _get_versions_for_generation(generation_id: str, db: Session) -> tuple:
|
||||
"""Get versions list and active version ID for a generation."""
|
||||
import json
|
||||
versions_rows = (
|
||||
db.query(DBGenerationVersion)
|
||||
.filter_by(generation_id=generation_id)
|
||||
.order_by(DBGenerationVersion.created_at)
|
||||
.all()
|
||||
)
|
||||
if not versions_rows:
|
||||
return None, None
|
||||
|
||||
versions = []
|
||||
active_version_id = None
|
||||
for v in versions_rows:
|
||||
effects_chain = None
|
||||
if v.effects_chain:
|
||||
try:
|
||||
raw = json.loads(v.effects_chain)
|
||||
effects_chain = [EffectConfig(**e) for e in raw]
|
||||
except Exception:
|
||||
pass
|
||||
versions.append(GenerationVersionResponse(
|
||||
id=v.id,
|
||||
generation_id=v.generation_id,
|
||||
label=v.label,
|
||||
audio_path=v.audio_path,
|
||||
effects_chain=effects_chain,
|
||||
is_default=v.is_default,
|
||||
created_at=v.created_at,
|
||||
))
|
||||
if v.is_default:
|
||||
active_version_id = v.id
|
||||
|
||||
return versions, active_version_id
|
||||
|
||||
|
||||
async def create_generation(
|
||||
profile_id: str,
|
||||
text: str,
|
||||
@@ -170,6 +207,7 @@ async def list_generations(
|
||||
# Convert to HistoryResponse with profile_name
|
||||
items = []
|
||||
for generation, profile_name in results:
|
||||
versions, active_version_id = _get_versions_for_generation(generation.id, db)
|
||||
items.append(HistoryResponse(
|
||||
id=generation.id,
|
||||
profile_id=generation.profile_id,
|
||||
@@ -185,6 +223,8 @@ async def list_generations(
|
||||
status=generation.status or "completed",
|
||||
error=generation.error,
|
||||
created_at=generation.created_at,
|
||||
versions=versions,
|
||||
active_version_id=active_version_id,
|
||||
))
|
||||
|
||||
return HistoryListResponse(
|
||||
@@ -210,12 +250,17 @@ async def delete_generation(
|
||||
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 all version files and records
|
||||
from . import versions as versions_mod
|
||||
versions_mod.delete_versions_for_generation(generation_id, db)
|
||||
|
||||
# Delete main audio file (if not already removed by version cleanup)
|
||||
if generation.audio_path:
|
||||
audio_path = Path(generation.audio_path)
|
||||
if audio_path.exists():
|
||||
audio_path.unlink()
|
||||
|
||||
# Delete from database
|
||||
db.delete(generation)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user