# stt_server.py — faster-whisper tabanlı YEREL konuşma tanıma sunucusu.
#
# Neden var? faster-whisper bir kütüphane; HTTP ucu yoktur. FIKO'nun konuşma tanıma
# motoru OpenAI-uyumlu /v1/audio/transcriptions bekler, biz de ONUN adresini veriyoruz:
#   STT_PROVIDER=openai
#   STT_URL=http://127.0.0.1:9000/v1/audio/transcriptions
#
# Kullanım:
#   python tools/stt_server.py 9000 --model models/faster-whisper-base
#
# Sağlık : GET  /health                    -> {"status":"ok","model":"..."}
# Tanıma : POST /v1/audio/transcriptions   (multipart gövde: file alanı)
#
# Model YEREL klasör de olabilir (models/faster-whisper-base); isim verilirse
# HuggingFace'den indirilir. Yerel kullanmak daha hızlı ve internet gerektirmez.

import argparse
import json
import os
import sys
import tempfile
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

# faster-whisper model adresleri: "tiny".."large-v3" veya yerel klasör yolu.
# Türkçe için "small" dengeli (CPU'da hızlı, ~460 MB). Daha hızlı/zayıf: "base" (~145 MB).
MODEL_DEFAULT = "small"
MAX_UPLOAD = 32 << 20  # 32 MiB üstü gövde reddedilir (bellek şişmesine karşı)


def safe_unlink(path: str) -> None:
    """Dosyayı silmeyi dener; dosya kilitliyse sessizce BIRAKIR.

    Neden? Windows'ta ses çözücü (PyAV) dosyayı işlediği sürece kilitli tutar; zorla
    silmek WinError 32 veriyor ve sunucu çöküyordu (canlı test bulgusu).
    Geçici dosya TEMP klasöründe kalır; işletim sistemi kendisi temizler.
    """
    for _ in range(3):
        try:
            os.unlink(path)
            return
        except FileNotFoundError:
            return
        except PermissionError:
            time.sleep(0.2)
    print(f"[stt] uyari: gecici dosya silinemedi, birakildi: {path}", file=sys.stderr, flush=True)


def extract_file(buf: bytes, boundary: str | None) -> bytes:
    """multipart gövdesinden ilk dosya alanının içeriğini döner.

    Neden şart? Go istemcimiz multipart/form-data gönderiyor; gövdenin TAMAMI
    (başlıklar + sınır işaretleri + dosya) ham ses DEĞİLDİR. İlk sürüm gövdeyi
    olduğu gibi diske yazdı ve faster-whisper "Invalid data found" hatası verdi
    (canlı test, 19. oturum).
    """
    if not boundary:
        return buf  # sınır yoksa gövde zaten ham sesdir
    delim = b"--" + boundary.encode("utf-8")
    head_end = buf.find(b"\r\n\r\n")
    if head_end < 0:
        return buf
    end = buf.find(b"\r\n" + delim, head_end)
    if end < 0:
        return buf
    return buf[head_end + 4 : end]


class Recognizer:
    """faster-whisper modelini bir kez yükler, istek başına çalıştırır."""

    def __init__(self, model_name: str, device: str, compute_type: str):
        from faster_whisper import WhisperModel  # geç import: hata mesajı daha okunur olsun

        self.name = model_name
        t0 = time.time()
        # Cihaz secimi: istenen cihaz kullanilamiyorsa CPU'ya DUS (sunucu cokmemez).
        # Neden? faster-whisper CUDA icin ayri kurulum ister; ortamda olmayabilir.
        # Otomatik: "auto" -> CUDA varsa cuda, yoksa cpu.
        if device == "auto":
            try:
                import ctranslate2

                device = "cuda" if ctranslate2.get_cuda_device_count() > 0 else "cpu"
            except Exception:  # noqa: BLE001 - CUDA surucusu yoksa CPU
                device = "cpu"
        # Bos compute_type cihaza gore secilir: GPU'da float16, CPU'da int8.
        # (Cihaz cozulmeden buraya gelinmez; aksi halde "auto" her zaman int8 alirdi.)
        if not compute_type:
            compute_type = "float16" if device == "cuda" else "int8"
        if device == "cuda":
            try:
                self.model = WhisperModel(model_name, device="cuda", compute_type=compute_type)
            except Exception as e:  # noqa: BLE001 - CUDA var ama model sigmadi (VRAM dolu)
                print(f"[stt] UYARI: cuda yuklenemedi ({e}); CPU'ya dusuluyor", flush=True)
                self.model = WhisperModel(model_name, device="cpu", compute_type="int8")
                device, compute_type = "cpu", "int8"
        else:
            self.model = WhisperModel(model_name, device=device, compute_type=compute_type)
        self.device = device
        print(
            f"[stt] model yuklendi: {model_name} ({device}/{compute_type}, {time.time() - t0:.1f}s)",
            flush=True,
        )

    def transcribe(self, path: str, language: str) -> str:
        # language="" OTOMATIK algilamadir. Zorlanan dil, baska dilde konusuldugunda
        # metni BOS donuyordu (canli test bulgusu) — bu yuzden varsayilan aciktir.
        #
        # condition_on_previous_text=False: TEKRAR/halusinasyon kaynagi. Ses klipleri
        # kisa oldugu icin onceki metni hatirlayip kendini tekrar etmeye calisiyor.
        # initial_prompt: Turkce metinde ("Istanbul, Turkce, birahne") dogru yazimi
        # ve noktalama bicimini modele ipucu verir.
        segments, info = self.model.transcribe(
            path,
            language=(language or None),
            beam_size=1,  # greedy: en hizli yol, kalite farki bu kadar kisa metinde ihmal edilebilir
            vad_filter=True,  # sessizlik kirp: tanimayi hizlandirir, bozuk metni azaltir
            condition_on_previous_text=False,
            initial_prompt="Bu bir Turkce konusma transkripsiyonudur.",
            no_speech_threshold=0.6,
        )
        text = " ".join(s.text.strip() for s in segments).strip()
        dur = getattr(info, "duration", 0.0)
        print(
            f"[stt] dil={info.language} olasilik={info.language_probability:.2f} "
            f"ses={dur:.1f}sn metin={text!r}",
            flush=True,
        )
        return text


REC: Recognizer | None = None


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def log_message(self, fmt, *args):  # gürültüyü azalt
        pass

    def _json(self, code: int, body: dict):
        raw = json.dumps(body, ensure_ascii=False).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(raw)))
        self.end_headers()
        self.wfile.write(raw)

    def do_GET(self):
        if self.path.rstrip("/").endswith("/health"):
            self._json(200, {"status": "ok", "model": REC.name if REC else None})
        else:
            self._json(404, {"error": "yok"})

    def do_POST(self):
        if not self.path.endswith("/audio/transcriptions"):
            self._json(404, {"error": "yok"})
            return
        if REC is None:
            self._json(503, {"error": "model yuklenmedi"})
            return

        length = int(self.headers.get("Content-Length", 0))
        if length <= 0:
            self._json(400, {"error": "bos istek"})
            return
        if length > MAX_UPLOAD:
            self._json(413, {"error": "ses cok buyuk"})
            return

        try:
            boundary = None
            ctype = self.headers.get("Content-Type", "")
            if "boundary=" in ctype:
                boundary = ctype.split("boundary=", 1)[1].strip().strip('"')

            buf = bytearray()
            remaining = length
            while remaining > 0:
                chunk = self.rfile.read(min(65536, remaining))
                if not chunk:
                    break
                buf += chunk
                remaining -= len(chunk)

            audio = extract_file(bytes(buf), boundary)
            if not audio:
                self._json(400, {"error": "ses alani bulunamadi"})
                return

            tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".webm")
            try:
                with open(tmp.name, "wb") as fh:
                    fh.write(audio)
                text = REC.transcribe(tmp.name, self.server.language)
                self._json(200, {"text": text})
            finally:
                safe_unlink(tmp.name)
        except Exception as e:  # noqa: BLE001 — hatayı istemciye bildirmek üstüne
            print(f"[stt] HATA: {e}", file=sys.stderr, flush=True)
            self._json(500, {"error": str(e)})


class Server(ThreadingHTTPServer):
    language = ""


def main() -> int:
    ap = argparse.ArgumentParser(description="FIKO yerel konusma tanima sunucusu")
    ap.add_argument("port", nargs="?", type=int, default=9000)
    ap.add_argument("--model", default=os.environ.get("FIKO_STT_MODEL", MODEL_DEFAULT))
    # Cihaz: "auto" (varsayilan) CUDA varsa GPU'yu kullanir, yoksa CPU'ya duser.
    # DİKKAT: sohbet modeli (Ollama) + TTS zaten GPU'daysa, konusma tanimayi da
    # GPU'ya koymak VRAM'i tuketir ve MOTORLAR BIRBIRINI KOVAR (olcum: tek cumle
    # TTS 1,6 sn yerine 131 sn surdu). Bu yuzden VARSAYILAN CPU/int8'dir.
    # Daha fazla GPU isteyen: --device cuda --compute-type float16
    ap.add_argument("--device", default=os.environ.get("FIKO_STT_DEVICE", "cpu"))
    ap.add_argument("--compute-type", default=os.environ.get("FIKO_STT_COMPUTE", ""))
    ap.add_argument(
        "--language",
        default=os.environ.get("FIKO_STT_LANGUAGE", ""),
        help='dil kodu (örn. "tr"). Boş bırakılırsa OTOMATİK algılanır (önerilen).',
    )
    args = ap.parse_args()

    global REC
    try:
        REC = Recognizer(args.model, args.device, args.compute_type)
    except ImportError:
        print("HATA: faster-whisper kurulu degil.  pip install faster-whisper", file=sys.stderr)
        return 1
    except Exception as e:  # noqa: BLE001
        print(f"HATA: model yuklenemedi ({e}). Yerel klasor verin: --model models/...", file=sys.stderr)
        return 1

    Server.language = args.language
    httpd = Server(("127.0.0.1", args.port), Handler)
    print(f"[stt] hazir: http://127.0.0.1:{args.port}/v1/audio/transcriptions", flush=True)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    return 0


if __name__ == "__main__":
    raise SystemExit(main())