mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-04 09:35:16 -07:00
delete_generations_by_profile (and the version sweep under it) committed on their own, so a failure between the generation sweep and the profile row left the profile listed with its history already gone. Both helpers take a commit flag now, and delete_profile passes commit=False so the single db.commit() at its end publishes the entire cascade. Other callers keep the default and are unchanged.
231 lines
6.9 KiB
Python
231 lines
6.9 KiB
Python
"""
|
|
Generation versions management module.
|
|
|
|
Each generation can have multiple audio versions: a clean (unprocessed)
|
|
version and any number of processed versions with different effects chains.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from ..database import (
|
|
GenerationVersion as DBGenerationVersion,
|
|
Generation as DBGeneration,
|
|
)
|
|
from ..models import GenerationVersionResponse, EffectConfig
|
|
from .. import config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _version_response(v: DBGenerationVersion) -> GenerationVersionResponse:
|
|
"""Convert a DB version row to a Pydantic response."""
|
|
effects_chain = None
|
|
if v.effects_chain:
|
|
raw = json.loads(v.effects_chain)
|
|
effects_chain = [EffectConfig(**e) for e in raw]
|
|
return GenerationVersionResponse(
|
|
id=v.id,
|
|
generation_id=v.generation_id,
|
|
label=v.label,
|
|
audio_path=v.audio_path,
|
|
effects_chain=effects_chain,
|
|
source_version_id=v.source_version_id,
|
|
is_default=v.is_default,
|
|
created_at=v.created_at,
|
|
)
|
|
|
|
|
|
def list_versions(generation_id: str, db: Session) -> List[GenerationVersionResponse]:
|
|
"""List all versions for a generation."""
|
|
versions = (
|
|
db.query(DBGenerationVersion)
|
|
.filter_by(generation_id=generation_id)
|
|
.order_by(DBGenerationVersion.created_at)
|
|
.all()
|
|
)
|
|
return [_version_response(v) for v in versions]
|
|
|
|
|
|
def get_version(version_id: str, db: Session) -> Optional[GenerationVersionResponse]:
|
|
"""Get a specific version by ID."""
|
|
v = db.query(DBGenerationVersion).filter_by(id=version_id).first()
|
|
if not v:
|
|
return None
|
|
return _version_response(v)
|
|
|
|
|
|
def get_default_version(generation_id: str, db: Session) -> Optional[GenerationVersionResponse]:
|
|
"""Get the default version for a generation."""
|
|
v = (
|
|
db.query(DBGenerationVersion)
|
|
.filter_by(generation_id=generation_id, is_default=True)
|
|
.first()
|
|
)
|
|
if not v:
|
|
# Fallback: return the first version
|
|
v = (
|
|
db.query(DBGenerationVersion)
|
|
.filter_by(generation_id=generation_id)
|
|
.order_by(DBGenerationVersion.created_at)
|
|
.first()
|
|
)
|
|
if not v:
|
|
return None
|
|
return _version_response(v)
|
|
|
|
|
|
def create_version(
|
|
generation_id: str,
|
|
label: str,
|
|
audio_path: str,
|
|
db: Session,
|
|
effects_chain: Optional[List[dict]] = None,
|
|
is_default: bool = False,
|
|
source_version_id: Optional[str] = None,
|
|
) -> GenerationVersionResponse:
|
|
"""Create a new version for a generation.
|
|
|
|
If ``is_default`` is True, all other versions for this generation
|
|
are un-defaulted first.
|
|
"""
|
|
if is_default:
|
|
_clear_defaults(generation_id, db)
|
|
|
|
version = DBGenerationVersion(
|
|
id=str(uuid.uuid4()),
|
|
generation_id=generation_id,
|
|
label=label,
|
|
audio_path=audio_path,
|
|
effects_chain=json.dumps(effects_chain) if effects_chain else None,
|
|
source_version_id=source_version_id,
|
|
is_default=is_default,
|
|
)
|
|
db.add(version)
|
|
db.commit()
|
|
db.refresh(version)
|
|
|
|
# If this version is the default, update the generation's audio_path
|
|
if is_default:
|
|
gen = db.query(DBGeneration).filter_by(id=generation_id).first()
|
|
if gen:
|
|
gen.audio_path = audio_path
|
|
db.commit()
|
|
|
|
return _version_response(version)
|
|
|
|
|
|
def set_default_version(version_id: str, db: Session) -> Optional[GenerationVersionResponse]:
|
|
"""Set a version as the default for its generation."""
|
|
version = db.query(DBGenerationVersion).filter_by(id=version_id).first()
|
|
if not version:
|
|
return None
|
|
|
|
_clear_defaults(version.generation_id, db)
|
|
version.is_default = True
|
|
db.commit()
|
|
db.refresh(version)
|
|
|
|
# Update generation's audio_path to point to this version
|
|
gen = db.query(DBGeneration).filter_by(id=version.generation_id).first()
|
|
if gen:
|
|
gen.audio_path = version.audio_path
|
|
db.commit()
|
|
|
|
return _version_response(version)
|
|
|
|
|
|
def delete_version(version_id: str, db: Session) -> bool:
|
|
"""Delete a version. Cannot delete the last remaining version."""
|
|
version = db.query(DBGenerationVersion).filter_by(id=version_id).first()
|
|
if not version:
|
|
return False
|
|
|
|
# Don't allow deleting the last version
|
|
count = (
|
|
db.query(DBGenerationVersion)
|
|
.filter_by(generation_id=version.generation_id)
|
|
.count()
|
|
)
|
|
if count <= 1:
|
|
return False
|
|
|
|
was_default = version.is_default
|
|
gen_id = version.generation_id
|
|
|
|
# Delete audio file
|
|
audio_path = config.resolve_storage_path(version.audio_path)
|
|
if audio_path is not None and audio_path.exists():
|
|
audio_path.unlink()
|
|
|
|
db.delete(version)
|
|
db.commit()
|
|
|
|
# If this was the default, promote the first remaining version
|
|
if was_default:
|
|
first = (
|
|
db.query(DBGenerationVersion)
|
|
.filter_by(generation_id=gen_id)
|
|
.order_by(DBGenerationVersion.created_at)
|
|
.first()
|
|
)
|
|
if first:
|
|
first.is_default = True
|
|
db.commit()
|
|
gen = db.query(DBGeneration).filter_by(id=gen_id).first()
|
|
if gen:
|
|
gen.audio_path = first.audio_path
|
|
db.commit()
|
|
|
|
return True
|
|
|
|
|
|
def delete_versions_for_generation(generation_id: str, db: Session, commit: bool = True) -> int:
|
|
"""Delete all versions for a generation (used when deleting a generation).
|
|
|
|
This runs as part of a wider cascade — deleting one generation, sweeping
|
|
failed ones, or deleting a whole profile. A version file the OS won't let
|
|
us remove (locked by playback on Windows) must not abort that cascade and
|
|
strand the caller half-deleted, so the row goes regardless and the leaked
|
|
file is logged. ``delete_version`` keeps raising, because a single
|
|
user-initiated delete should fail loudly.
|
|
|
|
Pass ``commit=False`` when the caller owns the transaction and will commit
|
|
the whole cascade itself.
|
|
"""
|
|
versions = (
|
|
db.query(DBGenerationVersion)
|
|
.filter_by(generation_id=generation_id)
|
|
.all()
|
|
)
|
|
count = 0
|
|
for v in versions:
|
|
audio_path = config.resolve_storage_path(v.audio_path)
|
|
if audio_path is not None and audio_path.exists():
|
|
try:
|
|
audio_path.unlink()
|
|
except OSError:
|
|
logger.warning(
|
|
"Could not delete version audio %s; removing the row anyway", audio_path
|
|
)
|
|
db.delete(v)
|
|
count += 1
|
|
if count > 0 and commit:
|
|
db.commit()
|
|
return count
|
|
|
|
|
|
def _clear_defaults(generation_id: str, db: Session) -> None:
|
|
"""Clear the is_default flag on all versions for a generation."""
|
|
db.query(DBGenerationVersion).filter_by(
|
|
generation_id=generation_id, is_default=True
|
|
).update({"is_default": False})
|
|
db.flush()
|