| from __future__ import annotations |
|
|
| from fastapi import APIRouter, HTTPException, UploadFile, File, Form |
| from loguru import logger |
|
|
| from api.src.core.config import settings |
| from api.src.inference.model_manager import ModelManager |
| from api.src.inference.voice_manager import VoiceManager |
| from api.src.services.audio_utils import validate_reference_audio |
| from api.src.structures.schemas import ( |
| VoiceDeleteResponse, |
| VoiceEncodeRequest, |
| VoiceEncodeResponse, |
| VoiceUploadResponse, |
| make_error, |
| ) |
|
|
| router = APIRouter(prefix="/v1/audio/voices", tags=["Voice Management"]) |
|
|
|
|
| @router.post("/upload", response_model=VoiceUploadResponse) |
| async def upload_voice( |
| voice_id: str = Form(...), |
| ref_text: str = Form(...), |
| audio: UploadFile = File(...), |
| language: str = Form("unknown"), |
| gender: str = Form("unknown"), |
| ) -> VoiceUploadResponse: |
| """Upload a custom voice reference (WAV + transcription text).""" |
| if not settings.allow_voice_upload: |
| raise HTTPException(status_code=403, detail=make_error("Voice upload is disabled")) |
|
|
| voice_manager = VoiceManager.get_instance() |
|
|
| |
| if not voice_id.isalnum() and not all(c.isalnum() or c in "-_" for c in voice_id): |
| raise HTTPException( |
| status_code=400, |
| detail=make_error("Voice ID must be alphanumeric (hyphens/underscores allowed)"), |
| ) |
|
|
| if voice_manager.voice_exists(voice_id): |
| raise HTTPException( |
| status_code=409, |
| detail=make_error(f"Voice '{voice_id}' already exists"), |
| ) |
|
|
| |
| wav_data = await audio.read() |
| try: |
| props = validate_reference_audio(wav_data) |
| except ValueError as e: |
| raise HTTPException(status_code=400, detail=make_error(str(e))) |
|
|
| voice_manager.upload_voice(voice_id, wav_data, ref_text, language=language, gender=gender) |
|
|
| return VoiceUploadResponse( |
| voice_id=voice_id, |
| status="uploaded", |
| message=f"Voice uploaded ({props['duration']:.1f}s, {props['sample_rate']}Hz)", |
| language=language, |
| gender=gender, |
| ) |
|
|
|
|
| @router.post("/{voice_id}/encode", response_model=VoiceEncodeResponse) |
| async def encode_voice(voice_id: str, request: VoiceEncodeRequest | None = None) -> VoiceEncodeResponse: |
| """Pre-encode a voice reference for a specific codec.""" |
| voice_manager = VoiceManager.get_instance() |
| model_manager = ModelManager.get_instance() |
|
|
| if not voice_manager.voice_exists(voice_id): |
| raise HTTPException(status_code=404, detail=make_error(f"Voice '{voice_id}' not found")) |
|
|
| |
| if not model_manager.loaded_models: |
| raise HTTPException( |
| status_code=400, |
| detail=make_error("No models loaded. Load a model first."), |
| ) |
|
|
| model_id = next(iter(model_manager.loaded_models)) |
| loaded = model_manager.loaded_models[model_id] |
| codec = (request.codec if request and request.codec else loaded.codec_id) |
|
|
| try: |
| await voice_manager.get_or_encode_ref_codes( |
| voice_id, codec, model_manager, model_id |
| ) |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=make_error(str(e), "server_error", 500)) |
|
|
| return VoiceEncodeResponse(voice_id=voice_id, codec=codec, status="encoded") |
|
|
|
|
| @router.delete("/{voice_id}", response_model=VoiceDeleteResponse) |
| async def delete_voice(voice_id: str) -> VoiceDeleteResponse: |
| """Delete a custom voice.""" |
| voice_manager = VoiceManager.get_instance() |
|
|
| try: |
| voice_manager.delete_voice(voice_id) |
| except ValueError as e: |
| raise HTTPException(status_code=400, detail=make_error(str(e))) |
|
|
| return VoiceDeleteResponse(voice_id=voice_id, status="deleted") |
|
|