File size: 3,967 Bytes
35bb6f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97fe226
35bb6f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
from __future__ import annotations

import subprocess
import sys
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
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.temp_manager import cleanup_temp

# Configure loguru
logger.remove()
logger.add(
    sys.stderr,
    level=settings.log_level,
    format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan> - <level>{message}</level>",
)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application startup and shutdown events."""
    logger.info("NeuTTS-FastAPI starting up...")

    # Initialize voice manager
    voice_manager = VoiceManager.get_instance()
    voice_manager.scan_voices()

    # GPU startup diagnostics
    try:
        import torch

        torch_cuda_ok = torch.cuda.is_available()
        if not torch_cuda_ok:
            try:
                result = subprocess.run(
                    ["nvidia-smi", "--query-gpu=name,driver_version", "--format=csv,noheader,nounits"],
                    capture_output=True, text=True, timeout=5,
                )
                if result.returncode == 0 and result.stdout.strip():
                    from api.src.routers.debug import _build_gpu_fix_instructions
                    line = result.stdout.strip().split("\n")[0]
                    parts = [p.strip() for p in line.split(",")]
                    gpu_name = parts[0]
                    driver_ver = parts[1] if len(parts) > 1 else "unknown"
                    fix = _build_gpu_fix_instructions(
                        gpu_name, driver_ver, torch.__version__, torch.version.cuda,
                    )
                    logger.warning(f"GPU detected but unusable! Running on CPU only.\n{fix}")
            except (FileNotFoundError, subprocess.TimeoutExpired):
                pass
    except ImportError:
        pass

    # Load default models
    model_manager = ModelManager.get_instance()
    await model_manager.startup()

    logger.info(
        f"Ready! Models: {list(model_manager.loaded_models.keys())}, "
        f"Voices: {list(voice_manager.voices.keys())}"
    )

    yield

    # Shutdown
    logger.info("Shutting down...")
    await model_manager.shutdown()
    cleanup_temp()
    logger.info("Shutdown complete")


app = FastAPI(
    title="NeuTTS-FastAPI",
    description="OpenAI-compatible Text-to-Speech API powered by NeuTTS",
    version="0.1.0",
    lifespan=lifespan,
)

# CORS
if settings.cors_enabled:
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.cors_origins_list,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

# Register routers
from api.src.routers import (
    debug,
    health,
    model_management,
    openai_compatible,
    voice_management,
    websocket,
)

# model_management first so /v1/models/registry is matched before /v1/models/{model_id}
app.include_router(model_management.router)
app.include_router(openai_compatible.router)
app.include_router(voice_management.router)
app.include_router(websocket.router)
app.include_router(health.router)
app.include_router(debug.router)

# Serve Web UI
_static_dir = Path(__file__).parent / "static"


@app.get("/", include_in_schema=False)
async def web_ui():
    return FileResponse(_static_dir / "index.html")


app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(
        "api.src.main:app",
        host=settings.host,
        port=settings.port,
        log_level=settings.log_level.lower(),
    )