mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 09:05:17 -07:00
fix: store media paths relative to data dir
This commit is contained in:
@@ -22,6 +22,21 @@ if _custom_models_dir:
|
|||||||
_data_dir = Path("data").resolve()
|
_data_dir = Path("data").resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def _path_relative_to_any_data_dir(path: Path) -> Path | None:
|
||||||
|
"""Extract the path within a data dir from an absolute or relative path."""
|
||||||
|
parts = path.parts
|
||||||
|
for idx, part in enumerate(parts):
|
||||||
|
if part != "data":
|
||||||
|
continue
|
||||||
|
|
||||||
|
tail = parts[idx + 1 :]
|
||||||
|
if tail:
|
||||||
|
return Path(*tail)
|
||||||
|
return Path()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def set_data_dir(path: str | Path):
|
def set_data_dir(path: str | Path):
|
||||||
"""
|
"""
|
||||||
Set the data directory path.
|
Set the data directory path.
|
||||||
@@ -45,6 +60,38 @@ def get_data_dir() -> Path:
|
|||||||
return _data_dir
|
return _data_dir
|
||||||
|
|
||||||
|
|
||||||
|
def to_storage_path(path: str | Path) -> str:
|
||||||
|
"""Convert a filesystem path to a DB-safe path relative to the data dir."""
|
||||||
|
resolved_path = Path(path).resolve()
|
||||||
|
|
||||||
|
relative_to_any_data_dir = _path_relative_to_any_data_dir(resolved_path)
|
||||||
|
if relative_to_any_data_dir is not None:
|
||||||
|
return str(relative_to_any_data_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return str(resolved_path.relative_to(_data_dir))
|
||||||
|
except ValueError:
|
||||||
|
return str(resolved_path)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_storage_path(path: str | Path | None) -> Path | None:
|
||||||
|
"""Resolve a DB-stored path against the configured data dir."""
|
||||||
|
if path is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
stored_path = Path(path)
|
||||||
|
if stored_path.is_absolute():
|
||||||
|
rebased_path = _path_relative_to_any_data_dir(stored_path)
|
||||||
|
if rebased_path is not None:
|
||||||
|
candidate = (_data_dir / rebased_path).resolve()
|
||||||
|
if candidate.exists() or not stored_path.exists():
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return stored_path
|
||||||
|
|
||||||
|
return (_data_dir / stored_path).resolve()
|
||||||
|
|
||||||
|
|
||||||
def get_db_path() -> Path:
|
def get_db_path() -> Path:
|
||||||
"""Get database file path."""
|
"""Get database file path."""
|
||||||
return _data_dir / "voicebox.db"
|
return _data_dir / "voicebox.db"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def run_migrations(engine) -> None:
|
|||||||
_migrate_generations(engine, inspector, tables)
|
_migrate_generations(engine, inspector, tables)
|
||||||
_migrate_effect_presets(engine, inspector, tables)
|
_migrate_effect_presets(engine, inspector, tables)
|
||||||
_migrate_generation_versions(engine, inspector, tables)
|
_migrate_generation_versions(engine, inspector, tables)
|
||||||
_resolve_relative_paths(engine, tables)
|
_normalize_storage_paths(engine, tables)
|
||||||
|
|
||||||
|
|
||||||
# -- helpers ---------------------------------------------------------------
|
# -- helpers ---------------------------------------------------------------
|
||||||
@@ -182,24 +182,11 @@ def _migrate_generation_versions(engine, inspector, tables: set[str]) -> None:
|
|||||||
_add_column(engine, "generation_versions", "source_version_id VARCHAR", "source_version_id")
|
_add_column(engine, "generation_versions", "source_version_id VARCHAR", "source_version_id")
|
||||||
|
|
||||||
|
|
||||||
def _resolve_relative_paths(engine, tables: set[str]) -> None:
|
def _normalize_storage_paths(engine, tables: set[str]) -> None:
|
||||||
"""Resolve any relative file paths in the database to absolute paths.
|
"""Normalize stored file paths to be relative to the configured data dir."""
|
||||||
|
|
||||||
Earlier versions stored paths relative to CWD (e.g. "data/generations/abc.wav").
|
|
||||||
These break when the production binary's CWD differs from the data directory.
|
|
||||||
This migration converts them to absolute paths using the configured data dir.
|
|
||||||
Idempotent: absolute paths are left untouched.
|
|
||||||
|
|
||||||
Strategy: paths like "data/generations/abc.wav" are rebased onto the
|
|
||||||
configured data directory. If the path starts with "data/", strip that
|
|
||||||
prefix and prepend get_data_dir(). Otherwise, join the relative path
|
|
||||||
directly under get_data_dir().
|
|
||||||
directly under get_data_dir(). If the rebased path still does not exist,
|
|
||||||
fall back to resolving relative to CWD.
|
|
||||||
"""
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..config import get_data_dir
|
from ..config import get_data_dir, to_storage_path, resolve_storage_path
|
||||||
|
|
||||||
data_dir = get_data_dir()
|
data_dir = get_data_dir()
|
||||||
|
|
||||||
@@ -222,21 +209,18 @@ def _resolve_relative_paths(engine, tables: set[str]) -> None:
|
|||||||
if not path_val:
|
if not path_val:
|
||||||
continue
|
continue
|
||||||
p = Path(path_val)
|
p = Path(path_val)
|
||||||
if p.is_absolute():
|
resolved = resolve_storage_path(p)
|
||||||
|
if resolved is None:
|
||||||
continue
|
continue
|
||||||
parts = p.parts
|
|
||||||
if parts and parts[0] == "data":
|
|
||||||
rebased = (data_dir / Path(*parts[1:])).resolve()
|
|
||||||
else:
|
|
||||||
rebased = (data_dir / p).resolve()
|
|
||||||
|
|
||||||
resolved = rebased if rebased.exists() else p.resolve()
|
normalized = to_storage_path(resolved)
|
||||||
if resolved.exists():
|
|
||||||
|
if normalized != path_val:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
text(f"UPDATE {table} SET {column} = :path WHERE id = :id"),
|
text(f"UPDATE {table} SET {column} = :path WHERE id = :id"),
|
||||||
{"path": str(resolved), "id": row_id},
|
{"path": normalized, "id": row_id},
|
||||||
)
|
)
|
||||||
total_fixed += 1
|
total_fixed += 1
|
||||||
if total_fixed > 0:
|
if total_fixed > 0:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info("Resolved %d relative file paths to absolute", total_fixed)
|
logger.info("Normalized %d stored file paths", total_fixed)
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
|
||||||
|
from .. import config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -25,7 +26,8 @@ def backfill_generation_versions(SessionLocal, Generation, GenerationVersion) ->
|
|||||||
for gen in generations:
|
for gen in generations:
|
||||||
if gen.id in existing_version_gen_ids:
|
if gen.id in existing_version_gen_ids:
|
||||||
continue
|
continue
|
||||||
if not Path(gen.audio_path).exists():
|
resolved_audio_path = config.resolve_storage_path(gen.audio_path)
|
||||||
|
if resolved_audio_path is None or not resolved_audio_path.exists():
|
||||||
continue
|
continue
|
||||||
version = GenerationVersion(
|
version = GenerationVersion(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
"""Audio file serving endpoints."""
|
"""Audio file serving endpoints."""
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .. import models
|
from .. import config, models
|
||||||
from ..services import history
|
from ..services import history
|
||||||
from ..database import get_db
|
from ..database import get_db
|
||||||
|
|
||||||
@@ -22,8 +20,8 @@ async def get_version_audio(version_id: str, db: Session = Depends(get_db)):
|
|||||||
if not version:
|
if not version:
|
||||||
raise HTTPException(status_code=404, detail="Version not found")
|
raise HTTPException(status_code=404, detail="Version not found")
|
||||||
|
|
||||||
audio_path = Path(version.audio_path)
|
audio_path = config.resolve_storage_path(version.audio_path)
|
||||||
if not audio_path.exists():
|
if audio_path is None or not audio_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Audio file not found")
|
raise HTTPException(status_code=404, detail="Audio file not found")
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
@@ -40,8 +38,8 @@ async def get_audio(generation_id: str, db: Session = Depends(get_db)):
|
|||||||
if not generation:
|
if not generation:
|
||||||
raise HTTPException(status_code=404, detail="Generation not found")
|
raise HTTPException(status_code=404, detail="Generation not found")
|
||||||
|
|
||||||
audio_path = Path(generation.audio_path)
|
audio_path = config.resolve_storage_path(generation.audio_path)
|
||||||
if not audio_path.exists():
|
if audio_path is None or not audio_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Audio file not found")
|
raise HTTPException(status_code=404, detail="Audio file not found")
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
@@ -60,8 +58,8 @@ async def get_sample_audio(sample_id: str, db: Session = Depends(get_db)):
|
|||||||
if not sample:
|
if not sample:
|
||||||
raise HTTPException(status_code=404, detail="Sample not found")
|
raise HTTPException(status_code=404, detail="Sample not found")
|
||||||
|
|
||||||
audio_path = Path(sample.audio_path)
|
audio_path = config.resolve_storage_path(sample.audio_path)
|
||||||
if not audio_path.exists():
|
if audio_path is None or not audio_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Audio file not found")
|
raise HTTPException(status_code=404, detail="Audio file not found")
|
||||||
|
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import io
|
import io
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
@@ -41,10 +40,11 @@ async def preview_effects(
|
|||||||
all_versions = versions_mod.list_versions(generation_id, db)
|
all_versions = versions_mod.list_versions(generation_id, db)
|
||||||
clean_version = next((v for v in all_versions if v.effects_chain is None), None)
|
clean_version = next((v for v in all_versions if v.effects_chain is None), None)
|
||||||
source_path = clean_version.audio_path if clean_version else gen.audio_path
|
source_path = clean_version.audio_path if clean_version else gen.audio_path
|
||||||
if not source_path or not Path(source_path).exists():
|
resolved_source_path = config.resolve_storage_path(source_path)
|
||||||
|
if resolved_source_path is None or not resolved_source_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Source audio file not found")
|
raise HTTPException(status_code=404, detail="Source audio file not found")
|
||||||
|
|
||||||
audio, sample_rate = await asyncio.to_thread(load_audio, source_path)
|
audio, sample_rate = await asyncio.to_thread(load_audio, str(resolved_source_path))
|
||||||
processed = await asyncio.to_thread(apply_effects, audio, sample_rate, chain_dicts)
|
processed = await asyncio.to_thread(apply_effects, audio, sample_rate, chain_dicts)
|
||||||
|
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
@@ -193,10 +193,11 @@ async def apply_effects_to_generation(
|
|||||||
source_path = clean_version.audio_path
|
source_path = clean_version.audio_path
|
||||||
source_version_id = clean_version.id
|
source_version_id = clean_version.id
|
||||||
|
|
||||||
if not source_path or not Path(source_path).exists():
|
resolved_source_path = config.resolve_storage_path(source_path)
|
||||||
|
if resolved_source_path is None or not resolved_source_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Source audio file not found")
|
raise HTTPException(status_code=404, detail="Source audio file not found")
|
||||||
|
|
||||||
audio, sample_rate = await asyncio.to_thread(load_audio, source_path)
|
audio, sample_rate = await asyncio.to_thread(load_audio, str(resolved_source_path))
|
||||||
processed_audio = await asyncio.to_thread(apply_effects, audio, sample_rate, chain_dicts)
|
processed_audio = await asyncio.to_thread(apply_effects, audio, sample_rate, chain_dicts)
|
||||||
|
|
||||||
version_id = str(uuid.uuid4())
|
version_id = str(uuid.uuid4())
|
||||||
@@ -208,7 +209,7 @@ async def apply_effects_to_generation(
|
|||||||
version = versions_mod.create_version(
|
version = versions_mod.create_version(
|
||||||
generation_id=generation_id,
|
generation_id=generation_id,
|
||||||
label=label,
|
label=label,
|
||||||
audio_path=str(processed_path),
|
audio_path=config.to_storage_path(processed_path),
|
||||||
db=db,
|
db=db,
|
||||||
effects_chain=chain_dicts,
|
effects_chain=chain_dicts,
|
||||||
is_default=data.set_as_default,
|
is_default=data.set_as_default,
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
"""Generation history endpoints."""
|
"""Generation history endpoints."""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .. import models
|
from .. import config, models
|
||||||
from ..services import export_import, history
|
from ..services import export_import, history
|
||||||
from ..app import safe_content_disposition
|
from ..app import safe_content_disposition
|
||||||
from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db
|
from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db
|
||||||
@@ -162,8 +161,8 @@ async def export_generation_audio(
|
|||||||
if not generation.audio_path:
|
if not generation.audio_path:
|
||||||
raise HTTPException(status_code=404, detail="Generation has no audio file")
|
raise HTTPException(status_code=404, detail="Generation has no audio file")
|
||||||
|
|
||||||
audio_path = Path(generation.audio_path)
|
audio_path = config.resolve_storage_path(generation.audio_path)
|
||||||
if not audio_path.is_file():
|
if audio_path is None or not audio_path.is_file():
|
||||||
raise HTTPException(status_code=404, detail="Audio file not found")
|
raise HTTPException(status_code=404, detail="Audio file not found")
|
||||||
|
|
||||||
safe_text = "".join(c for c in generation.text[:30] if c.isalnum() or c in (" ", "-", "_")).strip()
|
safe_text = "".join(c for c in generation.text[:30] if c.isalnum() or c in (" ", "-", "_")).strip()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
|||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse, StreamingResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .. import models
|
from .. import config, models
|
||||||
from ..app import safe_content_disposition
|
from ..app import safe_content_disposition
|
||||||
from ..database import VoiceProfile as DBVoiceProfile, get_db
|
from ..database import VoiceProfile as DBVoiceProfile, get_db
|
||||||
from ..services import channels, export_import, profiles
|
from ..services import channels, export_import, profiles
|
||||||
@@ -258,8 +258,8 @@ async def get_profile_avatar(
|
|||||||
if not profile.avatar_path:
|
if not profile.avatar_path:
|
||||||
raise HTTPException(status_code=404, detail="No avatar found for this profile")
|
raise HTTPException(status_code=404, detail="No avatar found for this profile")
|
||||||
|
|
||||||
avatar_path = Path(profile.avatar_path)
|
avatar_path = config.resolve_storage_path(profile.avatar_path)
|
||||||
if not avatar_path.exists():
|
if avatar_path is None or not avatar_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Avatar file not found")
|
raise HTTPException(status_code=404, detail="Avatar file not found")
|
||||||
|
|
||||||
return FileResponse(avatar_path)
|
return FileResponse(avatar_path)
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ def export_profile_to_zip(profile_id: str, db: Session) -> bytes:
|
|||||||
# Check if profile has avatar
|
# Check if profile has avatar
|
||||||
has_avatar = False
|
has_avatar = False
|
||||||
if profile.avatar_path:
|
if profile.avatar_path:
|
||||||
avatar_path = Path(profile.avatar_path)
|
avatar_path = config.resolve_storage_path(profile.avatar_path)
|
||||||
if avatar_path.exists():
|
if avatar_path is not None and avatar_path.exists():
|
||||||
has_avatar = True
|
has_avatar = True
|
||||||
# Add avatar to ZIP root with original extension
|
# Add avatar to ZIP root with original extension
|
||||||
avatar_ext = avatar_path.suffix
|
avatar_ext = avatar_path.suffix
|
||||||
@@ -98,7 +98,9 @@ def export_profile_to_zip(profile_id: str, db: Session) -> bytes:
|
|||||||
|
|
||||||
for sample in samples:
|
for sample in samples:
|
||||||
# Get filename from audio_path (should be {sample_id}.wav)
|
# Get filename from audio_path (should be {sample_id}.wav)
|
||||||
audio_path = Path(sample.audio_path)
|
audio_path = config.resolve_storage_path(sample.audio_path)
|
||||||
|
if audio_path is None:
|
||||||
|
raise ValueError(f"Audio file not found: {sample.audio_path}")
|
||||||
filename = audio_path.name
|
filename = audio_path.name
|
||||||
|
|
||||||
# Read audio file
|
# Read audio file
|
||||||
@@ -279,7 +281,7 @@ def export_generation_to_zip(generation_id: str, db: Session) -> bytes:
|
|||||||
# Build version manifest entries
|
# Build version manifest entries
|
||||||
version_entries = []
|
version_entries = []
|
||||||
for v in versions:
|
for v in versions:
|
||||||
v_path = Path(v.audio_path)
|
v_path = config.resolve_storage_path(v.audio_path)
|
||||||
effects_chain = None
|
effects_chain = None
|
||||||
if v.effects_chain:
|
if v.effects_chain:
|
||||||
effects_chain = json.loads(v.effects_chain)
|
effects_chain = json.loads(v.effects_chain)
|
||||||
@@ -314,14 +316,14 @@ def export_generation_to_zip(generation_id: str, db: Session) -> bytes:
|
|||||||
|
|
||||||
# Add all version audio files
|
# Add all version audio files
|
||||||
for v in versions:
|
for v in versions:
|
||||||
v_path = Path(v.audio_path)
|
v_path = config.resolve_storage_path(v.audio_path)
|
||||||
if v_path.exists():
|
if v_path is not None and v_path.exists():
|
||||||
zip_file.write(v_path, f"audio/{v_path.name}")
|
zip_file.write(v_path, f"audio/{v_path.name}")
|
||||||
|
|
||||||
# Fallback: if no versions exist, include the generation's main audio
|
# Fallback: if no versions exist, include the generation's main audio
|
||||||
if not versions:
|
if not versions:
|
||||||
audio_path = Path(generation.audio_path)
|
audio_path = config.resolve_storage_path(generation.audio_path)
|
||||||
if audio_path.exists():
|
if audio_path is not None and audio_path.exists():
|
||||||
zip_file.write(audio_path, f"audio/{audio_path.name}")
|
zip_file.write(audio_path, f"audio/{audio_path.name}")
|
||||||
|
|
||||||
zip_buffer.seek(0)
|
zip_buffer.seek(0)
|
||||||
@@ -426,7 +428,7 @@ async def import_generation_from_zip(file_bytes: bytes, db: Session) -> dict:
|
|||||||
profile_id=profile_id,
|
profile_id=profile_id,
|
||||||
text=generation_data["text"],
|
text=generation_data["text"],
|
||||||
language=generation_data["language"],
|
language=generation_data["language"],
|
||||||
audio_path=str(audio_dest),
|
audio_path=config.to_storage_path(audio_dest),
|
||||||
duration=generation_data["duration"],
|
duration=generation_data["duration"],
|
||||||
seed=generation_data.get("seed"),
|
seed=generation_data.get("seed"),
|
||||||
instruct=generation_data.get("instruct"),
|
instruct=generation_data.get("instruct"),
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ def _save_generate(
|
|||||||
versions_mod.create_version(
|
versions_mod.create_version(
|
||||||
generation_id=generation_id,
|
generation_id=generation_id,
|
||||||
label="original",
|
label="original",
|
||||||
audio_path=str(clean_audio_path),
|
audio_path=config.to_storage_path(clean_audio_path),
|
||||||
db=db,
|
db=db,
|
||||||
effects_chain=None,
|
effects_chain=None,
|
||||||
is_default=not has_effects,
|
is_default=not has_effects,
|
||||||
@@ -174,6 +174,8 @@ def _save_generate(
|
|||||||
if has_effects:
|
if has_effects:
|
||||||
from ..utils.effects import apply_effects, validate_effects_chain
|
from ..utils.effects import apply_effects, validate_effects_chain
|
||||||
|
|
||||||
|
assert effects_chain is not None
|
||||||
|
|
||||||
error_msg = validate_effects_chain(effects_chain)
|
error_msg = validate_effects_chain(effects_chain)
|
||||||
if error_msg:
|
if error_msg:
|
||||||
import logging
|
import logging
|
||||||
@@ -189,13 +191,13 @@ def _save_generate(
|
|||||||
versions_mod.create_version(
|
versions_mod.create_version(
|
||||||
generation_id=generation_id,
|
generation_id=generation_id,
|
||||||
label="version-2",
|
label="version-2",
|
||||||
audio_path=str(processed_path),
|
audio_path=config.to_storage_path(processed_path),
|
||||||
db=db,
|
db=db,
|
||||||
effects_chain=effects_chain,
|
effects_chain=effects_chain,
|
||||||
is_default=True,
|
is_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return final_audio_path
|
return config.to_storage_path(final_audio_path)
|
||||||
|
|
||||||
|
|
||||||
def _save_retry(
|
def _save_retry(
|
||||||
@@ -211,7 +213,7 @@ def _save_retry(
|
|||||||
"""
|
"""
|
||||||
audio_path = config.get_generations_dir() / f"{generation_id}.wav"
|
audio_path = config.get_generations_dir() / f"{generation_id}.wav"
|
||||||
save_audio(audio, str(audio_path), sample_rate)
|
save_audio(audio, str(audio_path), sample_rate)
|
||||||
return str(audio_path)
|
return config.to_storage_path(audio_path)
|
||||||
|
|
||||||
|
|
||||||
def _save_regenerate(
|
def _save_regenerate(
|
||||||
@@ -244,10 +246,10 @@ def _save_regenerate(
|
|||||||
versions_mod.create_version(
|
versions_mod.create_version(
|
||||||
generation_id=generation_id,
|
generation_id=generation_id,
|
||||||
label=label,
|
label=label,
|
||||||
audio_path=str(audio_path),
|
audio_path=config.to_storage_path(audio_path),
|
||||||
db=db,
|
db=db,
|
||||||
effects_chain=None,
|
effects_chain=None,
|
||||||
is_default=True,
|
is_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return str(audio_path)
|
return config.to_storage_path(audio_path)
|
||||||
|
|||||||
@@ -253,8 +253,8 @@ async def delete_generation(
|
|||||||
|
|
||||||
# Delete main audio file (if not already removed by version cleanup)
|
# Delete main audio file (if not already removed by version cleanup)
|
||||||
if generation.audio_path:
|
if generation.audio_path:
|
||||||
audio_path = Path(generation.audio_path)
|
audio_path = config.resolve_storage_path(generation.audio_path)
|
||||||
if audio_path.exists():
|
if audio_path is not None and audio_path.exists():
|
||||||
audio_path.unlink()
|
audio_path.unlink()
|
||||||
|
|
||||||
# Delete from database
|
# Delete from database
|
||||||
@@ -283,8 +283,8 @@ async def delete_generations_by_profile(
|
|||||||
count = 0
|
count = 0
|
||||||
for generation in generations:
|
for generation in generations:
|
||||||
# Delete audio file
|
# Delete audio file
|
||||||
audio_path = Path(generation.audio_path)
|
audio_path = config.resolve_storage_path(generation.audio_path)
|
||||||
if audio_path.exists():
|
if audio_path is not None and audio_path.exists():
|
||||||
audio_path.unlink()
|
audio_path.unlink()
|
||||||
|
|
||||||
# Delete from database
|
# Delete from database
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ async def add_profile_sample(
|
|||||||
db_sample = DBProfileSample(
|
db_sample = DBProfileSample(
|
||||||
id=sample_id,
|
id=sample_id,
|
||||||
profile_id=profile_id,
|
profile_id=profile_id,
|
||||||
audio_path=str(dest_path),
|
audio_path=config.to_storage_path(dest_path),
|
||||||
reference_text=reference_text,
|
reference_text=reference_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -441,8 +441,8 @@ async def delete_profile_sample(
|
|||||||
# Store profile_id before deleting
|
# Store profile_id before deleting
|
||||||
profile_id = sample.profile_id
|
profile_id = sample.profile_id
|
||||||
|
|
||||||
audio_path = Path(sample.audio_path)
|
audio_path = config.resolve_storage_path(sample.audio_path)
|
||||||
if audio_path.exists():
|
if audio_path is not None and audio_path.exists():
|
||||||
audio_path.unlink()
|
audio_path.unlink()
|
||||||
|
|
||||||
db.delete(sample)
|
db.delete(sample)
|
||||||
@@ -556,14 +556,22 @@ async def create_voice_prompt_for_profile(
|
|||||||
|
|
||||||
if len(samples) == 1:
|
if len(samples) == 1:
|
||||||
sample = samples[0]
|
sample = samples[0]
|
||||||
|
sample_audio_path = config.resolve_storage_path(sample.audio_path)
|
||||||
|
if sample_audio_path is None:
|
||||||
|
raise ValueError(f"Sample audio not found for profile {profile_id}")
|
||||||
voice_prompt, _ = await tts_model.create_voice_prompt(
|
voice_prompt, _ = await tts_model.create_voice_prompt(
|
||||||
sample.audio_path,
|
str(sample_audio_path),
|
||||||
sample.reference_text,
|
sample.reference_text,
|
||||||
use_cache=use_cache,
|
use_cache=use_cache,
|
||||||
)
|
)
|
||||||
return voice_prompt
|
return voice_prompt
|
||||||
|
|
||||||
audio_paths = [s.audio_path for s in samples]
|
audio_paths = []
|
||||||
|
for sample in samples:
|
||||||
|
sample_audio_path = config.resolve_storage_path(sample.audio_path)
|
||||||
|
if sample_audio_path is None:
|
||||||
|
raise ValueError(f"Sample audio not found for profile {profile_id}")
|
||||||
|
audio_paths.append(str(sample_audio_path))
|
||||||
reference_texts = [s.reference_text for s in samples]
|
reference_texts = [s.reference_text for s in samples]
|
||||||
|
|
||||||
combined_audio, combined_text = await tts_model.combine_voice_prompts(
|
combined_audio, combined_text = await tts_model.combine_voice_prompts(
|
||||||
@@ -617,8 +625,8 @@ async def upload_avatar(
|
|||||||
raise ValueError(error_msg)
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
if profile.avatar_path:
|
if profile.avatar_path:
|
||||||
old_avatar = Path(profile.avatar_path)
|
old_avatar = config.resolve_storage_path(profile.avatar_path)
|
||||||
if old_avatar.exists():
|
if old_avatar is not None and old_avatar.exists():
|
||||||
old_avatar.unlink()
|
old_avatar.unlink()
|
||||||
|
|
||||||
# Determine file extension from uploaded file
|
# Determine file extension from uploaded file
|
||||||
@@ -639,7 +647,7 @@ async def upload_avatar(
|
|||||||
|
|
||||||
process_avatar(image_path, str(output_path))
|
process_avatar(image_path, str(output_path))
|
||||||
|
|
||||||
profile.avatar_path = str(output_path)
|
profile.avatar_path = config.to_storage_path(output_path)
|
||||||
profile.updated_at = datetime.utcnow()
|
profile.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -666,8 +674,8 @@ async def delete_avatar(
|
|||||||
if not profile or not profile.avatar_path:
|
if not profile or not profile.avatar_path:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
avatar_path = Path(profile.avatar_path)
|
avatar_path = config.resolve_storage_path(profile.avatar_path)
|
||||||
if avatar_path.exists():
|
if avatar_path is not None and avatar_path.exists():
|
||||||
avatar_path.unlink()
|
avatar_path.unlink()
|
||||||
|
|
||||||
profile.avatar_path = None
|
profile.avatar_path = None
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
from .. import config
|
||||||
from ..models import (
|
from ..models import (
|
||||||
StoryCreate,
|
StoryCreate,
|
||||||
StoryResponse,
|
StoryResponse,
|
||||||
@@ -826,8 +827,8 @@ async def export_story_audio(
|
|||||||
if version:
|
if version:
|
||||||
resolved_audio_path = version.audio_path
|
resolved_audio_path = version.audio_path
|
||||||
|
|
||||||
audio_path = Path(resolved_audio_path)
|
audio_path = config.resolve_storage_path(resolved_audio_path)
|
||||||
if not audio_path.exists():
|
if audio_path is None or not audio_path.exists():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -158,8 +158,8 @@ def delete_version(version_id: str, db: Session) -> bool:
|
|||||||
gen_id = version.generation_id
|
gen_id = version.generation_id
|
||||||
|
|
||||||
# Delete audio file
|
# Delete audio file
|
||||||
audio_path = Path(version.audio_path)
|
audio_path = config.resolve_storage_path(version.audio_path)
|
||||||
if audio_path.exists():
|
if audio_path is not None and audio_path.exists():
|
||||||
audio_path.unlink()
|
audio_path.unlink()
|
||||||
|
|
||||||
db.delete(version)
|
db.delete(version)
|
||||||
@@ -193,8 +193,8 @@ def delete_versions_for_generation(generation_id: str, db: Session) -> int:
|
|||||||
)
|
)
|
||||||
count = 0
|
count = 0
|
||||||
for v in versions:
|
for v in versions:
|
||||||
audio_path = Path(v.audio_path)
|
audio_path = config.resolve_storage_path(v.audio_path)
|
||||||
if audio_path.exists():
|
if audio_path is not None and audio_path.exists():
|
||||||
audio_path.unlink()
|
audio_path.unlink()
|
||||||
db.delete(v)
|
db.delete(v)
|
||||||
count += 1
|
count += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user