#!/usr/bin/env python3
"""
概要:
    1リクエストにつき1つの推論プロセスを起動するOpenAI互換のQwen3-ASR APIです。
詳細説明:
    Flaskを使用してHTTPサーバーを構成し、音声ファイルのアップロードを受け付けます。
    GPUメモリの枯渇を防ぐため、スレッドロックを用いて推論プロセスを直列化します。
関連リンク:
    qwen_asr_flask_usage
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
import tempfile
import threading
from pathlib import Path

from flask import Flask, jsonify, request


MODEL = os.environ.get("QWEN_ASR_MODEL", "Qwen/Qwen3-ASR-1.7B")
RUNNER = Path(os.environ.get("QWEN_ASR_RUNNER", Path(__file__).with_name("qwen_asr_once.py")))
UPLOAD_DIR = Path(os.environ.get("QWEN_ASR_UPLOAD_DIR", "/var/tmp/qwen-asr-upload"))
TIMEOUT_SECONDS = int(os.environ.get("QWEN_ASR_TIMEOUT_SECONDS", "3600"))
INFERENCE_LOCK = threading.Lock()


def create_app() -> Flask:
    """
    概要:
        Flaskアプリケーションインスタンスを作成して設定します。
    詳細説明:
        ファイルのアップロードサイズの制限を設定し、アップロードディレクトリを作成します。
        ヘルスチェック、モデルリスト、音声文字起こしの各エンドポイントを内部で定義します。
    戻り値:
        :returns: 設定済みのFlaskアプリケーションインスタンス
        :rtype: flask.Flask
    """
    app = Flask(__name__)
    app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024 * 1024  # 1 GiB upload limit
    UPLOAD_DIR.mkdir(parents=True, exist_ok=True)

    @app.get("/health")
    def health():
        """
        概要:
            ヘルスチェック用のエンドポイントです。
        戻り値:
            :returns: 空文字列とHTTPステータスコード200のタプル
            :rtype: tuple
        """
        return ("", 200)

    @app.get("/v1/models")
    def models():
        """
        概要:
            利用可能なモデルのリストを返すエンドポイントです。
        戻り値:
            :returns: OpenAI互換のモデルリストを含むJSONレスポンス
            :rtype: flask.Response
        """
        return jsonify({"object": "list", "data": [{"id": MODEL, "object": "model", "owned_by": "qwen-asr-cli"}]})

    @app.post("/v1/audio/transcriptions")
    def transcriptions():
        """
        概要:
            音声ファイルを受け取り、文字起こし結果を返すエンドポイントです。
        詳細説明:
            アップロードされたファイルを一時ディレクトリに保存し、外部プロセスで推論スクリプトを実行します。
            複数のリクエストが同時に来た場合は、スレッドロックにより順番に処理されます。
        戻り値:
            :returns: 文字起こしされたテキストと検出された言語を含むJSONレスポンス、またはエラーメッセージ
            :rtype: tuple
        """
        uploaded = request.files.get("file")
        if uploaded is None or not uploaded.filename:
            return jsonify({"error": {"message": "multipart field 'file' is required"}}), 400
        requested_model = request.form.get("model", MODEL)
        if requested_model != MODEL:
            return jsonify({"error": {"message": f"only model {MODEL!r} is available"}}), 400

        suffix = Path(uploaded.filename).suffix.lower() or ".wav"
        with tempfile.NamedTemporaryFile(dir=UPLOAD_DIR, suffix=suffix, delete=False) as temporary:
            temporary_path = Path(temporary.name)
            uploaded.save(temporary)
        try:
            # One lock is intentional: concurrent requests must not each load a
            # 1.7B model on the same GPU. Later requests wait in the HTTP call.
            with INFERENCE_LOCK:
                completed = subprocess.run(
                    [sys.executable, str(RUNNER), "--audio", str(temporary_path), "--model", MODEL],
                    text=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    timeout=TIMEOUT_SECONDS,
                    check=False,
                )
            if completed.returncode != 0:
                message = (completed.stderr or completed.stdout or "Qwen-ASR runner failed").strip()
                return jsonify({"error": {"message": message[-4000:]}}), 500
            try:
                result = json.loads(completed.stdout.strip().splitlines()[-1])
            except (json.JSONDecodeError, IndexError) as exc:
                return jsonify({"error": {"message": f"invalid runner output: {completed.stdout[-1000:]}"}}), 500
            return jsonify({"text": result["text"], "language": result.get("language")})
        except subprocess.TimeoutExpired:
            return jsonify({"error": {"message": "ASR request timed out"}}), 504
        finally:
            temporary_path.unlink(missing_ok=True)

    return app


if __name__ == "__main__":
    create_app().run(host="0.0.0.0", port=8001, threaded=False)