fix and return full

import os
import sys
import json
import copy
import logging
import torch
import yt_dlp
import threading
import uvicorn

from fastapi import FastAPI, HTTPException, Request, UploadFile, File, Form
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from typing import Optional

import spaces
from audio_separator.separator import Separator
from assets.i18n.i18n import I18nAuto
from assets.presence.discord_presence import RPCManager, track_presence


# Setup logging
logging.basicConfig(level=logging.INFO)

# Constants and paths
now_dir = os.getcwd()
sys.path.append(now_dir)

models_dir = "./models"
out_dir = "./outputs"
temp_dir = "./temp"
os.makedirs(out_dir, exist_ok=True)
os.makedirs(temp_dir, exist_ok=True)

extensions = (".wav", ".flac", ".mp3", ".ogg", ".opus", ".m4a", ".aiff", ".ac3")

config_file = os.path.join(now_dir, "assets", "config.json")
default_settings_file = os.path.join(now_dir, "assets", "default_settings.json")
custom_settings_file = os.path.join(now_dir, "assets", "custom_settings.json")

device = "cuda" if torch.cuda.is_available() else "cpu"
use_autocast = device == "cuda"

# VR ARCH model (Server A)
VRARCH_MODEL = "UVR-DeEcho-DeReverb.pth"
VRARCH_MODEL_PATH = os.path.join(models_dir, VRARCH_MODEL)
if not os.path.exists(VRARCH_MODEL_PATH):
    raise FileNotFoundError(f"Model '{VRARCH_MODEL}' not found in '{models_dir}'")


# MDX23C models list (from Server B)
mdx23c_models = [
    'MDX23C_D1581.ckpt',
    'MDX23C-8KFFT-InstVoc_HQ.ckpt',
    'MDX23C-8KFFT-InstVoc_HQ_2.ckpt',
    'MDX23C-De-Reverb-aufr33-jarredou.ckpt',
    'MDX23C-DrumSep-aufr33-jarredou.ckpt'
]

# Internationalization instance
i18n = I18nAuto()

# --- Shutdown auth token, configure this to a secure value ---
SHUTDOWN_AUTH_TOKEN = "your-secret-token"  # Replace accordingly

# Global reference to uvicorn.Server for shutdown
server: uvicorn.Server = None


def load_settings_from_file(filepath):
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            return json.load(f)
    except Exception:
        return None


def read_main_config():
    try:
        with open(config_file, "r", encoding="utf8") as f:
            return json.load(f)
    except Exception:
        return {}


def get_initial_settings():
    main_config = read_main_config()
    load_custom = main_config.get('load_custom_settings', False)

    settings_to_load = {}
    default_settings = load_settings_from_file(default_settings_file)

    if load_custom:
        custom_settings = load_settings_from_file(custom_settings_file)
        if custom_settings:
            settings_to_load = copy.deepcopy(default_settings)
            for section, params in custom_settings.items():
                if section in settings_to_load:
                    for key, value in params.items():
                        settings_to_load[section][key] = value
                else:
                    settings_to_load[section] = params
        else:
            settings_to_load = default_settings
    else:
        settings_to_load = default_settings

    return settings_to_load or {}


initial_settings = get_initial_settings()


# FastAPI app
app = FastAPI(title="Combined UVR VR ARCH and MDX23C Separation Server")

# Mount static files under /outputs for client downloads (Server B)
app.mount("/outputs", StaticFiles(directory=out_dir), name="outputs")


# --------- Server A part (VR ARCH) ---------

class SeparationRequest(BaseModel):
    audio_path: str  # local file path to input audio
    output_format: str = "wav"
    window_size: int = 512
    aggression: int = 5
    tta: bool = True
    post_process: bool = False
    post_process_threshold: float = 0.2
    high_end_process: bool = False
    batch_size: int = 1
    normalization_threshold: float = 0.9
    amplification_threshold: float = 0.7
    single_stem: Optional[str] = ""  # empty or stem name


@app.post("/separate")
def separate_audio(req: SeparationRequest):
    """Perform music separation using VR ARCH model UVR-DeEcho-DeReverb.pth"""
    audio_path = req.audio_path

    if not os.path.isfile(audio_path):
        raise HTTPException(status_code=400, detail=f"Audio file '{audio_path}' does not exist")

    # Ensure output dir exists
    os.makedirs(out_dir, exist_ok=True)

    try:
        logging.info("Initializing Separator for VR ARCH model...")
        separator = Separator(
            log_level=logging.WARNING,
            model_file_dir=models_dir,
            output_dir=out_dir,
            output_format=req.output_format,
            use_autocast=use_autocast,
            normalization_threshold=req.normalization_threshold,
            amplification_threshold=req.amplification_threshold,
            output_single_stem=req.single_stem,
            vr_params={
                "batch_size": req.batch_size,
                "window_size": req.window_size,
                "aggression": req.aggression,
                "enable_tta": req.tta,
                "enable_post_process": req.post_process,
                "post_process_threshold": req.post_process_threshold,
                "high_end_process": req.high_end_process,
            }
        )

        logging.info(f"Loading model {VRARCH_MODEL}...")
        separator.load_model(model_filename=VRARCH_MODEL)

        logging.info(f"Starting separation on audio: {audio_path} ...")
        separation = separator.separate(audio_path)

        stems_paths = [os.path.abspath(os.path.join(out_dir, fname)) for fname in separation]

        if req.single_stem and req.single_stem.strip():
            # Output single stem only
            logging.info(f"Returning single stem at: {stems_paths[0]}")
            return {"stem1": stems_paths[0], "stem2": None}
        else:
            # Return first two stems if available
            stem1 = stems_paths[0] if len(stems_paths) > 0 else None
            stem2 = stems_paths[1] if len(stems_paths) > 1 else None
            logging.info(f"Returning stems at: {stem1}, {stem2}")
            return {"stem1": stem1, "stem2": stem2}

    except Exception as e:
        logging.error(f"Error during VR ARCH separation: {e}", exc_info=True)
        raise HTTPException(status_code=500, detail=f"Separation failed: {e}")


@app.get("/")
def root():
    try:
        return {"message": "Combined UVR VR ARCH and MDX23C Separation Server - Use POST /separate or /mdx23c/separate to process audio files."}
    except Exception as ex:
        return {"error": str(ex)}


# --------- Server B part (MDX23C) ---------

@track_presence("Performing MDXC Separation")
@spaces.GPU(duration=60)
def mdxc_separator(audio_path, model, out_format, segment_size, override_seg_size, overlap, batch_size, norm_thresh, amp_thresh, single_stem):
    model_path = os.path.join(models_dir, model)
    try:
        if not os.path.exists(model_path):
            raise FileNotFoundError(f"Model '{model}' not found in '{models_dir}'")

        separator = Separator(
            log_level=logging.WARNING,
            model_file_dir=models_dir,
            output_dir=out_dir,
            output_format=out_format,
            use_autocast=use_autocast,
            normalization_threshold=norm_thresh,
            amplification_threshold=amp_thresh,
            output_single_stem=single_stem,
            mdxc_params={
                "segment_size": segment_size,
                "override_model_segment_size": override_seg_size,
                "batch_size": batch_size,
                "overlap": overlap,
            }
        )

        separator.load_model(model_filename=model)
        separation = separator.separate(audio_path)
        stems = [os.path.join(out_dir, file_name) for file_name in separation]
        if single_stem.strip():
            return stems[0], None
        return stems[0], stems[1]

    except Exception as e:
        raise RuntimeError(f"MDX23C separation failed: {e}") from e


def download_audio(url, output_dir="ytdl"):
    os.makedirs(output_dir, exist_ok=True)
    ydl_opts = {
        'format': 'bestaudio/best',
        'postprocessors': [{
            'key': 'FFmpegExtractAudio',
            'preferredcodec': 'wav',
            'preferredquality': '32',
        }],
        'outtmpl': os.path.join(output_dir, '%(title)s.%(ext)s'),
        'postprocessor_args': [
            '-acodec', 'pcm_f32le'
        ],
        'quiet': True,
        'no_warnings': True,
    }
    try:
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            info = ydl.extract_info(url, download=False)
            video_title = info['title']
            ydl.download([url])
            file_path = os.path.join(output_dir, f"{video_title}.wav")
            if os.path.exists(file_path):
                return os.path.abspath(file_path)
            else:
                raise Exception("Audio conversion failed or output file missing.")
    except Exception as e:
        raise Exception(f"Error extracting audio with yt-dlp: {str(e)}")


@app.post("/mdx23c/separate")
async def separate_mdx23c(
    request: Request,
    audio: UploadFile = File(...),
    model: str = Form(...),
    output_format: str = Form(...),
    segment_size: int = Form(...),
    override_segment_size: bool = Form(...),
    overlap: int = Form(...),
    batch_size: int = Form(...),
    normalization_threshold: float = Form(...),
    amplification_threshold: float = Form(...),
    single_stem: str = Form("")
):
    # Save uploaded file temporarily
    input_path = os.path.join(temp_dir, audio.filename)
    try:
        with open(input_path, "wb") as f:
            f.write(await audio.read())

        stem1_path, stem2_path = mdxc_separator(
            audio_path=input_path,
            model=model,
            out_format=output_format,
            segment_size=segment_size,
            override_seg_size=override_segment_size,
            overlap=overlap,
            batch_size=batch_size,
            norm_thresh=normalization_threshold,
            amp_thresh=amplification_threshold,
            single_stem=single_stem,
        )

        # Construct URLs to output files (served as static files)
        base_url = str(request.base_url).rstrip("/")
        response = {"stem1": f"{base_url}/outputs/{os.path.basename(stem1_path)}"}
        if stem2_path:
            response["stem2"] = f"{base_url}/outputs/{os.path.basename(stem2_path)}"}

        return JSONResponse(content=response)

    except Exception as e:
        logging.error(f"Error during MDX23C separation: {e}", exc_info=True)
        return JSONResponse(content={"error": str(e)}, status_code=500)
    finally:
        try:
            if os.path.exists(input_path):
                os.remove(input_path)  # Clean temp file
        except Exception:
            pass


@app.get("/mdx23c/models")
def list_models():
    return {"models": mdx23c_models}


@app.get("/mdx23c/download_audio")
def download_audio_endpoint(url: str):
    try:
        audio_path = download_audio(url)
        return {"audio_path": audio_path}
    except Exception as e:
        return JSONResponse(content={"error": str(e)}, status_code=500)


@app.post("/shutdown")
async def shutdown(request: Request):
    """
    Shutdown endpoint secured by `X-Auth-Token` header.
    Triggers uvicorn server shutdown.
    """
    global server
    token = request.headers.get("X-Auth-Token")
    if token != SHUTDOWN_AUTH_TOKEN:
        raise HTTPException(status_code=401, detail="Unauthorized")

    if server is None:
        raise HTTPException(status_code=503, detail="Server instance not found")

    def stop_server():
        logging.info("Shutdown initiated via /shutdown endpoint.")
        server.should_exit = True

    threading.Thread(target=stop_server).start()
    return {"status": "shutting down"}


def main():
    global server

    port = 8123  # default port
    # If passed as cmd argument, parse and override port
    if "--port" in sys.argv:
        try:
            idx = sys.argv.index("--port")
            if idx + 1 < len(sys.argv):
                port = int(sys.argv[idx + 1])
        except Exception:
            logging.warning("Invalid port argument - using default 8123")

    config = uvicorn.Config(app, host="0.0.0.0", port=port, log_level="info")
    server = uvicorn.Server(config)
    server.run()


if __name__ == "__main__":
    main()

#SyntaxError: unmatched '}'