diff --git a/backend/app.py b/backend/app.py index f8bbbcd1..69cee59b 100644 --- a/backend/app.py +++ b/backend/app.py @@ -15,9 +15,10 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from urllib.parse import quote -from . import __version__, database, tts, transcribe, config +from . import __version__, database +from .services import tts, transcribe from .database import get_db -from .platform_detect import get_backend_type +from .utils.platform_detect import get_backend_type from .utils.progress import get_progress_manager from .services.task_queue import create_background_task, init_queue from .routes import register_routers diff --git a/backend/backends/__init__.py b/backend/backends/__init__.py index 3ebbb985..cc35eabe 100644 --- a/backend/backends/__init__.py +++ b/backend/backends/__init__.py @@ -11,7 +11,7 @@ from typing import Protocol, Optional, Tuple, List from typing_extensions import runtime_checkable import numpy as np -from ..platform_detect import get_backend_type +from ..utils.platform_detect import get_backend_type LANGUAGE_CODE_TO_NAME = { "zh": "chinese", @@ -375,7 +375,7 @@ async def ensure_model_cached_or_raise(engine: str, model_size: str = "default") def unload_model_by_config(config: ModelConfig) -> bool: """Unload a model given its config. Returns True if it was loaded, False otherwise.""" from . import get_tts_backend_for_engine - from .. import tts, transcribe + from ..services import tts, transcribe if config.engine == "whisper": whisper_model = transcribe.get_whisper_model() @@ -403,7 +403,7 @@ def unload_model_by_config(config: ModelConfig) -> bool: def check_model_loaded(config: ModelConfig) -> bool: """Check if a model is currently loaded.""" from . import get_tts_backend_for_engine - from .. import tts, transcribe + from ..services import tts, transcribe try: if config.engine == "whisper": @@ -424,7 +424,7 @@ def check_model_loaded(config: ModelConfig) -> bool: def get_model_load_func(config: ModelConfig): """Return a callable that loads/downloads the model.""" from . import get_tts_backend_for_engine - from .. import tts, transcribe + from ..services import tts, transcribe if config.engine == "whisper": return lambda: transcribe.get_whisper_model().load_model(config.model_size) diff --git a/backend/build_binary.py b/backend/build_binary.py index 43de991b..6f8b1736 100644 --- a/backend/build_binary.py +++ b/backend/build_binary.py @@ -55,11 +55,11 @@ def build_server(cuda=False): '--hidden-import', 'backend.config', '--hidden-import', 'backend.database', '--hidden-import', 'backend.models', - '--hidden-import', 'backend.profiles', - '--hidden-import', 'backend.history', - '--hidden-import', 'backend.tts', - '--hidden-import', 'backend.transcribe', - '--hidden-import', 'backend.platform_detect', + '--hidden-import', 'backend.services.profiles', + '--hidden-import', 'backend.services.history', + '--hidden-import', 'backend.services.tts', + '--hidden-import', 'backend.services.transcribe', + '--hidden-import', 'backend.utils.platform_detect', '--hidden-import', 'backend.backends', '--hidden-import', 'backend.backends.pytorch_backend', '--hidden-import', 'backend.utils.audio', @@ -68,9 +68,9 @@ def build_server(cuda=False): '--hidden-import', 'backend.utils.hf_progress', '--hidden-import', 'backend.utils.validation', '--hidden-import', 'backend.services.cuda', - '--hidden-import', 'backend.effects', + '--hidden-import', 'backend.services.effects', '--hidden-import', 'backend.utils.effects', - '--hidden-import', 'backend.versions', + '--hidden-import', 'backend.services.versions', '--hidden-import', 'pedalboard', '--hidden-import', 'chatterbox', '--hidden-import', 'chatterbox.tts_turbo', diff --git a/backend/routes/audio.py b/backend/routes/audio.py index 12e774ef..682d7aae 100644 --- a/backend/routes/audio.py +++ b/backend/routes/audio.py @@ -6,7 +6,8 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse from sqlalchemy.orm import Session -from .. import history, models +from .. import models +from ..services import history from ..database import get_db router = APIRouter() @@ -15,7 +16,7 @@ router = APIRouter() @router.get("/audio/version/{version_id}") async def get_version_audio(version_id: str, db: Session = Depends(get_db)): """Serve audio for a specific version.""" - from .. import versions as versions_mod + from ..services import versions as versions_mod version = versions_mod.get_version(version_id, db) if not version: diff --git a/backend/routes/channels.py b/backend/routes/channels.py index 56391624..c13162fb 100644 --- a/backend/routes/channels.py +++ b/backend/routes/channels.py @@ -3,7 +3,8 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session -from .. import channels, models +from .. import models +from ..services import channels from ..database import get_db router = APIRouter() diff --git a/backend/routes/effects.py b/backend/routes/effects.py index 8e1f6272..8139176d 100644 --- a/backend/routes/effects.py +++ b/backend/routes/effects.py @@ -9,7 +9,8 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session -from .. import config, history, models +from .. import config, models +from ..services import history from ..database import Generation as DBGeneration, get_db router = APIRouter() @@ -28,7 +29,7 @@ async def preview_effects( if (gen.status or "completed") != "completed": raise HTTPException(status_code=400, detail="Generation is not completed") - from .. import versions as versions_mod + from ..services import versions as versions_mod from ..utils.effects import apply_effects, validate_effects_chain from ..utils.audio import load_audio @@ -73,7 +74,7 @@ async def get_available_effects(): @router.get("/effects/presets", response_model=list[models.EffectPresetResponse]) async def list_effect_presets(db: Session = Depends(get_db)): """List all effect presets (built-in + user-created).""" - from .. import effects as effects_mod + from ..services import effects as effects_mod return effects_mod.list_presets(db) @@ -81,7 +82,7 @@ async def list_effect_presets(db: Session = Depends(get_db)): @router.get("/effects/presets/{preset_id}", response_model=models.EffectPresetResponse) async def get_effect_preset(preset_id: str, db: Session = Depends(get_db)): """Get a specific effect preset.""" - from .. import effects as effects_mod + from ..services import effects as effects_mod preset = effects_mod.get_preset(preset_id, db) if not preset: @@ -95,7 +96,7 @@ async def create_effect_preset( db: Session = Depends(get_db), ): """Create a new effect preset.""" - from .. import effects as effects_mod + from ..services import effects as effects_mod try: return effects_mod.create_preset(data, db) @@ -110,7 +111,7 @@ async def update_effect_preset( db: Session = Depends(get_db), ): """Update an effect preset.""" - from .. import effects as effects_mod + from ..services import effects as effects_mod try: result = effects_mod.update_preset(preset_id, data, db) @@ -124,7 +125,7 @@ async def update_effect_preset( @router.delete("/effects/presets/{preset_id}") async def delete_effect_preset(preset_id: str, db: Session = Depends(get_db)): """Delete a user effect preset.""" - from .. import effects as effects_mod + from ..services import effects as effects_mod try: if not effects_mod.delete_preset(preset_id, db): @@ -147,7 +148,7 @@ async def list_generation_versions( if not gen: raise HTTPException(status_code=404, detail="Generation not found") - from .. import versions as versions_mod + from ..services import versions as versions_mod return versions_mod.list_versions(generation_id, db) @@ -168,7 +169,7 @@ async def apply_effects_to_generation( if (gen.status or "completed") != "completed": raise HTTPException(status_code=400, detail="Generation is not completed") - from .. import versions as versions_mod + from ..services import versions as versions_mod from ..utils.effects import apply_effects, validate_effects_chain from ..utils.audio import load_audio, save_audio @@ -227,7 +228,7 @@ async def set_default_version( db: Session = Depends(get_db), ): """Set a specific version as the default for a generation.""" - from .. import versions as versions_mod + from ..services import versions as versions_mod version = versions_mod.get_version(version_id, db) if not version or version.generation_id != generation_id: @@ -246,7 +247,7 @@ async def delete_generation_version( db: Session = Depends(get_db), ): """Delete a version. Cannot delete the last remaining version.""" - from .. import versions as versions_mod + from ..services import versions as versions_mod version = versions_mod.get_version(version_id, db) if not version or version.generation_id != generation_id: diff --git a/backend/routes/generations.py b/backend/routes/generations.py index 32051796..d9f4ef29 100644 --- a/backend/routes/generations.py +++ b/backend/routes/generations.py @@ -7,7 +7,8 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session -from .. import history, models, profiles, tts +from .. import models +from ..services import history, profiles, tts from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db from ..services.generation import run_generation from ..services.task_queue import enqueue_generation diff --git a/backend/routes/health.py b/backend/routes/health.py index 181aaebf..e48d5689 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -8,9 +8,10 @@ import torch from fastapi import APIRouter, Depends from sqlalchemy.orm import Session -from .. import config, models, tts +from .. import config, models +from ..services import tts from ..database import get_db -from ..platform_detect import get_backend_type +from ..utils.platform_detect import get_backend_type router = APIRouter() diff --git a/backend/routes/history.py b/backend/routes/history.py index e4c2d47c..5ae438b2 100644 --- a/backend/routes/history.py +++ b/backend/routes/history.py @@ -7,7 +7,8 @@ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse, StreamingResponse from sqlalchemy.orm import Session -from .. import export_import, history, models +from .. import models +from ..services import export_import, history from ..app import safe_content_disposition from ..database import Generation as DBGeneration, VoiceProfile as DBVoiceProfile, get_db diff --git a/backend/routes/models.py b/backend/routes/models.py index c17d0f15..5706c7ad 100644 --- a/backend/routes/models.py +++ b/backend/routes/models.py @@ -9,7 +9,7 @@ from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from .. import models -from ..platform_detect import get_backend_type +from ..utils.platform_detect import get_backend_type from ..services.task_queue import create_background_task from ..utils.progress import get_progress_manager from ..utils.tasks import get_task_manager @@ -50,7 +50,7 @@ def _copy_with_progress(src: Path, dst: Path, progress_manager, copied_so_far: i @router.post("/models/load") async def load_model(model_size: str = "1.7B"): """Manually load TTS model.""" - from .. import tts + from ..services import tts try: tts_model = tts.get_tts_model() @@ -63,7 +63,7 @@ async def load_model(model_size: str = "1.7B"): @router.post("/models/unload") async def unload_model(): """Unload the default Qwen TTS model to free memory.""" - from .. import tts + from ..services import tts try: tts.unload_tts_model() diff --git a/backend/routes/profiles.py b/backend/routes/profiles.py index 320dfc50..6b7b4509 100644 --- a/backend/routes/profiles.py +++ b/backend/routes/profiles.py @@ -9,10 +9,11 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile from fastapi.responses import FileResponse, StreamingResponse from sqlalchemy.orm import Session -from .. import channels, config, export_import, models, profiles +from .. import config, models from ..app import safe_content_disposition from ..database import VoiceProfile as DBVoiceProfile, get_db -from ..profiles import _profile_to_response +from ..services import channels, export_import, profiles +from ..services.profiles import _profile_to_response router = APIRouter() diff --git a/backend/routes/stories.py b/backend/routes/stories.py index 0c5296ba..74af7a50 100644 --- a/backend/routes/stories.py +++ b/backend/routes/stories.py @@ -6,7 +6,8 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session -from .. import database, models, stories +from .. import database, models +from ..services import stories from ..app import safe_content_disposition from ..database import get_db diff --git a/backend/routes/transcription.py b/backend/routes/transcription.py index 8edafef6..b5008c82 100644 --- a/backend/routes/transcription.py +++ b/backend/routes/transcription.py @@ -6,7 +6,8 @@ from pathlib import Path from fastapi import APIRouter, File, Form, HTTPException, UploadFile -from .. import models, transcribe +from .. import models +from ..services import transcribe from ..services.task_queue import create_background_task from ..utils.tasks import get_task_manager diff --git a/backend/channels.py b/backend/services/channels.py similarity index 99% rename from backend/channels.py rename to backend/services/channels.py index 146c003d..f7d9d788 100644 --- a/backend/channels.py +++ b/backend/services/channels.py @@ -7,14 +7,14 @@ from datetime import datetime import uuid from sqlalchemy.orm import Session -from .models import ( +from ..models import ( AudioChannelCreate, AudioChannelUpdate, AudioChannelResponse, ChannelVoiceAssignment, ProfileChannelAssignment, ) -from .database import ( +from ..database import ( AudioChannel as DBAudioChannel, ChannelDeviceMapping as DBChannelDeviceMapping, ProfileChannelMapping as DBProfileChannelMapping, diff --git a/backend/effects.py b/backend/services/effects.py similarity index 96% rename from backend/effects.py rename to backend/services/effects.py index 79130785..29ca5918 100644 --- a/backend/effects.py +++ b/backend/services/effects.py @@ -11,8 +11,8 @@ from typing import List, Optional from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError -from .database import EffectPreset as DBEffectPreset -from .models import EffectPresetResponse, EffectPresetCreate, EffectPresetUpdate, EffectConfig +from ..database import EffectPreset as DBEffectPreset +from ..models import EffectPresetResponse, EffectPresetCreate, EffectPresetUpdate, EffectConfig def _preset_response(p: DBEffectPreset) -> EffectPresetResponse: diff --git a/backend/export_import.py b/backend/services/export_import.py similarity index 98% rename from backend/export_import.py rename to backend/services/export_import.py index 58acd400..93252f50 100644 --- a/backend/export_import.py +++ b/backend/services/export_import.py @@ -12,11 +12,11 @@ from pathlib import Path from typing import Optional from sqlalchemy.orm import Session -from .models import VoiceProfileResponse -from .database import VoiceProfile as DBVoiceProfile, ProfileSample as DBProfileSample, Generation as DBGeneration, GenerationVersion as DBGenerationVersion +from ..models import VoiceProfileResponse +from ..database import VoiceProfile as DBVoiceProfile, ProfileSample as DBProfileSample, Generation as DBGeneration, GenerationVersion as DBGenerationVersion from .profiles import create_profile, add_profile_sample -from .models import VoiceProfileCreate -from . import config +from ..models import VoiceProfileCreate +from .. import config def _get_unique_profile_name(name: str, db: Session) -> str: @@ -346,7 +346,7 @@ async def import_generation_from_zip(file_bytes: bytes, db: Session) -> dict: import tempfile import shutil from datetime import datetime - from . import config + from .. import config zip_buffer = io.BytesIO(file_bytes) diff --git a/backend/services/generation.py b/backend/services/generation.py index 6f85cd0c..04e07014 100644 --- a/backend/services/generation.py +++ b/backend/services/generation.py @@ -19,7 +19,8 @@ from __future__ import annotations import traceback from typing import Literal, Optional -from .. import config, history, profiles +from .. import config +from . import history, profiles from ..database import get_db from ..utils.tasks import get_task_manager @@ -151,7 +152,7 @@ def _save_generate( Returns the final audio path (processed if effects were applied, otherwise clean). """ - from .. import versions as versions_mod + from . import versions as versions_mod clean_audio_path = config.get_generations_dir() / f"{generation_id}.wav" save_audio(audio, str(clean_audio_path), sample_rate) @@ -221,7 +222,7 @@ def _save_regenerate( Returns the audio path. """ - from .. import versions as versions_mod + from . import versions as versions_mod suffix = version_id[:8] if version_id else generation_id[:8] audio_path = config.get_generations_dir() / f"{generation_id}_{suffix}.wav" diff --git a/backend/history.py b/backend/services/history.py similarity index 96% rename from backend/history.py rename to backend/services/history.py index c2c9197a..8f45d48f 100644 --- a/backend/history.py +++ b/backend/services/history.py @@ -10,9 +10,9 @@ from pathlib import Path 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 . import config +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 def _get_versions_for_generation(generation_id: str, db: Session) -> tuple: diff --git a/backend/profiles.py b/backend/services/profiles.py similarity index 97% rename from backend/profiles.py rename to backend/services/profiles.py index ab8941c4..46180f3c 100644 --- a/backend/profiles.py +++ b/backend/services/profiles.py @@ -10,23 +10,23 @@ from pathlib import Path from sqlalchemy.orm import Session from sqlalchemy import func, select -from .models import ( +from ..models import ( VoiceProfileCreate, VoiceProfileResponse, ProfileSampleCreate, ProfileSampleResponse, ) -from .database import ( +from ..database import ( VoiceProfile as DBVoiceProfile, ProfileSample as DBProfileSample, Generation as DBGeneration, ) -from .models import EffectConfig -from .utils.audio import validate_reference_audio, load_audio, save_audio -from .utils.images import validate_image, process_avatar -from .utils.cache import _get_cache_dir, clear_profile_cache +from ..models import EffectConfig +from ..utils.audio import validate_reference_audio, load_audio, save_audio +from ..utils.images import validate_image, process_avatar +from ..utils.cache import _get_cache_dir, clear_profile_cache from .tts import get_tts_model -from . import config +from .. import config import json as _json @@ -389,7 +389,7 @@ async def create_voice_prompt_for_profile( Returns: Voice prompt dictionary """ - from .backends import get_tts_backend_for_engine + from ..backends import get_tts_backend_for_engine samples = db.query(DBProfileSample).filter_by(profile_id=profile_id).all() diff --git a/backend/stories.py b/backend/services/stories.py similarity index 99% rename from backend/stories.py rename to backend/services/stories.py index cb4dad65..ac8e22bd 100644 --- a/backend/stories.py +++ b/backend/services/stories.py @@ -10,7 +10,7 @@ from pathlib import Path from sqlalchemy.orm import Session from sqlalchemy import func -from .models import ( +from ..models import ( StoryCreate, StoryResponse, StoryDetailResponse, @@ -22,14 +22,14 @@ from .models import ( StoryItemSplit, StoryItemVersionUpdate, ) -from .database import ( +from ..database import ( Story as DBStory, StoryItem as DBStoryItem, Generation as DBGeneration, VoiceProfile as DBVoiceProfile, ) from .history import _get_versions_for_generation -from .utils.audio import load_audio, save_audio +from ..utils.audio import load_audio, save_audio import numpy as np @@ -754,7 +754,7 @@ async def set_story_item_version( # Validate version_id belongs to this generation if provided if data.version_id: - from .database import GenerationVersion as DBGenerationVersion + from ..database import GenerationVersion as DBGenerationVersion version = ( db.query(DBGenerationVersion) @@ -820,7 +820,7 @@ async def export_story_audio( # Resolve audio path: use pinned version if set, otherwise generation default resolved_audio_path = generation.audio_path if getattr(item, "version_id", None): - from .database import GenerationVersion as DBGenerationVersion + from ..database import GenerationVersion as DBGenerationVersion version = db.query(DBGenerationVersion).filter_by(id=item.version_id).first() if version: diff --git a/backend/transcribe.py b/backend/services/transcribe.py similarity index 89% rename from backend/transcribe.py rename to backend/services/transcribe.py index 4d735fce..e400dbd5 100644 --- a/backend/transcribe.py +++ b/backend/services/transcribe.py @@ -3,7 +3,7 @@ STT (Speech-to-Text) module - delegates to backend abstraction layer. """ from typing import Optional -from .backends import get_stt_backend, STTBackend +from ..backends import get_stt_backend, STTBackend def get_whisper_model() -> STTBackend: diff --git a/backend/tts.py b/backend/services/tts.py similarity index 93% rename from backend/tts.py rename to backend/services/tts.py index 453f2f5c..d4f90ff3 100644 --- a/backend/tts.py +++ b/backend/services/tts.py @@ -7,7 +7,7 @@ import numpy as np import io import soundfile as sf -from .backends import get_tts_backend, TTSBackend +from ..backends import get_tts_backend, TTSBackend def get_tts_model() -> TTSBackend: diff --git a/backend/versions.py b/backend/services/versions.py similarity index 98% rename from backend/versions.py rename to backend/services/versions.py index 381114d7..1743a25c 100644 --- a/backend/versions.py +++ b/backend/services/versions.py @@ -14,12 +14,12 @@ from typing import List, Optional from sqlalchemy.orm import Session -from .database import ( +from ..database import ( GenerationVersion as DBGenerationVersion, Generation as DBGeneration, ) -from .models import GenerationVersionResponse, EffectConfig -from . import config +from ..models import GenerationVersionResponse, EffectConfig +from .. import config def _version_response(v: DBGenerationVersion) -> GenerationVersionResponse: diff --git a/backend/platform_detect.py b/backend/utils/platform_detect.py similarity index 100% rename from backend/platform_detect.py rename to backend/utils/platform_detect.py