import os from contextlib import AsyncExitStack, asynccontextmanager from fastapi import Request from fastapi.responses import RedirectResponse, Response from access_log_filter import install_access_log_filter from editor_app import app as editor_app from ktts_app import app as tts_app from musicgen_app import app as music_app from mcp_server import RouteSource, create_mcp_server from renderer_app import app from whisper_app import app as whisper_app install_access_log_filter() def _is_api_docs_path(path: str) -> bool: return ( path.endswith("/openapi.json") or path.endswith("/docs") or "/docs/" in path or path.endswith("/redoc") ) @app.middleware("http") async def protect_api_docs(request: Request, call_next): docs_enabled = os.getenv("ENABLE_API_DOCS", "false").lower() in { "1", "true", "yes", } if not docs_enabled and _is_api_docs_path(request.url.path): return Response(status_code=404) return await call_next(request) CHILD_APPS = ( ("/tts", tts_app, "tts"), ("/music", music_app, "music"), ("/whisper", whisper_app, "whisper"), ("/editor", editor_app, "editor"), ) renderer_lifespan = app.router.lifespan_context @app.get("/", include_in_schema=False) async def studio_home(): return RedirectResponse(url="/dashboard/", status_code=302) @app.get("/apps", tags=["studio"]) async def studio_apps(): return { "renderer": "/dashboard", "tts": "/tts/", "music": "/music/", "whisper": "/whisper/", "editor": "/editor/", "mcp": "/mcp/", "gradio_mcp": "/dashboard/gradio_api/mcp/", "mcp_tools": len(mcp_tool_catalog), } for path, child_app, name in CHILD_APPS: app.mount(path, child_app, name=name) mcp, mcp_tool_catalog = create_mcp_server( app, ( RouteSource("renderer", app), RouteSource("tts", tts_app, "/tts"), RouteSource("music", music_app, "/music"), RouteSource("whisper", whisper_app, "/whisper"), RouteSource("editor", editor_app, "/editor"), ), ) mcp_http_app = mcp.streamable_http_app() @app.get("/mcp-health", include_in_schema=False) async def mcp_health(): return { "status": "ok", "custom_mcp_url": "/mcp/", "gradio_mcp_url": "/dashboard/gradio_api/mcp/", "custom_tool_count": len(mcp_tool_catalog), "transports": ["streamable-http"], } @asynccontextmanager async def studio_lifespan(root_app): async with AsyncExitStack() as stack: await stack.enter_async_context(renderer_lifespan(root_app)) for _, child_app, _ in CHILD_APPS: await stack.enter_async_context(child_app.router.lifespan_context(child_app)) await stack.enter_async_context(mcp.session_manager.run()) yield app.router.lifespan_context = studio_lifespan app.mount("/mcp", mcp_http_app, name="mcp")