mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-04 09:35:16 -07:00
fix(profiles): delete a profile's generations instead of orphaning them
This commit is contained in:
committed by
capy-ai-staging[bot]
parent
4f54e16824
commit
4750b47b04
+24
-10
@@ -11,10 +11,23 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
|
||||
from ..models import GenerationRequest, GenerationResponse, HistoryQuery, HistoryResponse, HistoryListResponse, GenerationVersionResponse, EffectConfig
|
||||
from ..database import Generation as DBGeneration, GenerationVersion as DBGenerationVersion, VoiceProfile as DBVoiceProfile
|
||||
from ..database import Generation as DBGeneration, GenerationVersion as DBGenerationVersion, StoryItem as DBStoryItem, VoiceProfile as DBVoiceProfile
|
||||
from .. import config
|
||||
|
||||
|
||||
def _delete_generation_children(generation_id: str, db: Session) -> None:
|
||||
"""Remove the rows that reference a generation, plus any version audio files.
|
||||
|
||||
Story items and versions both point at the generation by a non-null FK.
|
||||
The story detail query inner-joins generations, so a leftover story item
|
||||
vanishes from the timeline while staying in the table forever.
|
||||
"""
|
||||
from . import versions as versions_mod
|
||||
|
||||
db.query(DBStoryItem).filter_by(generation_id=generation_id).delete()
|
||||
versions_mod.delete_versions_for_generation(generation_id, db)
|
||||
|
||||
|
||||
def _get_versions_for_generations(generation_ids: list[str], db: Session) -> dict:
|
||||
"""Fetch versions for many generations in a single query.
|
||||
|
||||
@@ -280,8 +293,7 @@ async def delete_generation(
|
||||
return False
|
||||
|
||||
# Delete all version files and records
|
||||
from . import versions as versions_mod
|
||||
versions_mod.delete_versions_for_generation(generation_id, db)
|
||||
_delete_generation_children(generation_id, db)
|
||||
|
||||
# Delete main audio file (if not already removed by version cleanup)
|
||||
if generation.audio_path:
|
||||
@@ -307,13 +319,11 @@ async def delete_failed_generations(db: Session) -> int:
|
||||
Returns:
|
||||
Number of generations deleted.
|
||||
"""
|
||||
from . import versions as versions_mod
|
||||
|
||||
failed = db.query(DBGeneration).filter(DBGeneration.status == "failed").all()
|
||||
count = 0
|
||||
for generation in failed:
|
||||
# Clean up version files/rows first.
|
||||
versions_mod.delete_versions_for_generation(generation.id, db)
|
||||
_delete_generation_children(generation.id, db)
|
||||
|
||||
# Remove the main audio file if it somehow made it to disk.
|
||||
if generation.audio_path:
|
||||
@@ -352,14 +362,18 @@ async def delete_generations_by_profile(
|
||||
count = 0
|
||||
for generation in generations:
|
||||
# Delete associated version files and rows first
|
||||
from . import versions as versions_mod
|
||||
versions_mod.delete_versions_for_generation(generation.id, db)
|
||||
_delete_generation_children(generation.id, db)
|
||||
|
||||
# Delete audio file
|
||||
audio_path = config.resolve_storage_path(generation.audio_path)
|
||||
if audio_path is not None and audio_path.exists():
|
||||
audio_path.unlink()
|
||||
|
||||
try:
|
||||
audio_path.unlink()
|
||||
except OSError:
|
||||
# A file locked by playback shouldn't abort the whole sweep
|
||||
# and leave the profile half-deleted.
|
||||
pass
|
||||
|
||||
# Delete from database
|
||||
db.delete(generation)
|
||||
count += 1
|
||||
|
||||
@@ -11,7 +11,14 @@ from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import config
|
||||
from ..database import Generation as DBGeneration, ProfileSample as DBProfileSample, VoiceProfile as DBVoiceProfile
|
||||
from ..database import (
|
||||
CaptureSettings as DBCaptureSettings,
|
||||
Generation as DBGeneration,
|
||||
MCPClientBinding as DBMCPClientBinding,
|
||||
ProfileChannelMapping as DBProfileChannelMapping,
|
||||
ProfileSample as DBProfileSample,
|
||||
VoiceProfile as DBVoiceProfile,
|
||||
)
|
||||
from ..models import (
|
||||
EffectConfig,
|
||||
ProfileSampleResponse,
|
||||
@@ -21,6 +28,7 @@ from ..models import (
|
||||
from ..utils.audio import save_audio, validate_and_load_reference_audio
|
||||
from ..utils.cache import _get_cache_dir, clear_profile_cache
|
||||
from ..utils.images import process_avatar, validate_image
|
||||
from . import history
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -429,7 +437,23 @@ async def delete_profile(
|
||||
if not profile:
|
||||
return False
|
||||
|
||||
# Generations carry a non-null FK to the profile and the history query
|
||||
# inner-joins profiles, so anything left behind here becomes a row the UI
|
||||
# can never show and a .wav in data/generations the user can never reclaim.
|
||||
deleted_generations = await history.delete_generations_by_profile(profile_id, db)
|
||||
if deleted_generations:
|
||||
logger.info("Deleted %d generations belonging to profile %s", deleted_generations, profile_id)
|
||||
|
||||
db.query(DBProfileSample).filter_by(profile_id=profile_id).delete()
|
||||
db.query(DBProfileChannelMapping).filter_by(profile_id=profile_id).delete()
|
||||
|
||||
# Nullable pointers at the profile — resolve_profile() already tolerates a
|
||||
# dangling id, but leaving one behind makes the UI show an empty selection
|
||||
# that the user can't clear.
|
||||
db.query(DBMCPClientBinding).filter_by(profile_id=profile_id).update({"profile_id": None})
|
||||
db.query(DBCaptureSettings).filter_by(default_playback_voice_id=profile_id).update(
|
||||
{"default_playback_voice_id": None}
|
||||
)
|
||||
|
||||
db.delete(profile)
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user