qwen_asr_flask.py ダウンロード/コピー

qwen_asr_flask.py をダウンロード

qwen_asr_flask.py
qwen_asr_flask.py
  1#!/usr/bin/env python3
  2"""
  3概要:
  4    1リクエストにつき1つの推論プロセスを起動するOpenAI互換のQwen3-ASR APIです。
  5詳細説明:
  6    Flaskを使用してHTTPサーバーを構成し、音声ファイルのアップロードを受け付けます。
  7    GPUメモリの枯渇を防ぐため、スレッドロックを用いて推論プロセスを直列化します。
  8関連リンク:
  9    qwen_asr_flask_usage
 10"""
 11
 12from __future__ import annotations
 13
 14import json
 15import os
 16import subprocess
 17import sys
 18import tempfile
 19import threading
 20from pathlib import Path
 21
 22from flask import Flask, jsonify, request
 23
 24
 25MODEL = os.environ.get("QWEN_ASR_MODEL", "Qwen/Qwen3-ASR-1.7B")
 26RUNNER = Path(os.environ.get("QWEN_ASR_RUNNER", Path(__file__).with_name("qwen_asr_once.py")))
 27UPLOAD_DIR = Path(os.environ.get("QWEN_ASR_UPLOAD_DIR", "/var/tmp/qwen-asr-upload"))
 28TIMEOUT_SECONDS = int(os.environ.get("QWEN_ASR_TIMEOUT_SECONDS", "3600"))
 29INFERENCE_LOCK = threading.Lock()
 30
 31
 32def create_app() -> Flask:
 33    """
 34    概要:
 35        Flaskアプリケーションインスタンスを作成して設定します。
 36    詳細説明:
 37        ファイルのアップロードサイズの制限を設定し、アップロードディレクトリを作成します。
 38        ヘルスチェック、モデルリスト、音声文字起こしの各エンドポイントを内部で定義します。
 39    戻り値:
 40        :returns: 設定済みのFlaskアプリケーションインスタンス
 41        :rtype: flask.Flask
 42    """
 43    app = Flask(__name__)
 44    app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024 * 1024  # 1 GiB upload limit
 45    UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
 46
 47    @app.get("/health")
 48    def health():
 49        """
 50        概要:
 51            ヘルスチェック用のエンドポイントです。
 52        戻り値:
 53            :returns: 空文字列とHTTPステータスコード200のタプル
 54            :rtype: tuple
 55        """
 56        return ("", 200)
 57
 58    @app.get("/v1/models")
 59    def models():
 60        """
 61        概要:
 62            利用可能なモデルのリストを返すエンドポイントです。
 63        戻り値:
 64            :returns: OpenAI互換のモデルリストを含むJSONレスポンス
 65            :rtype: flask.Response
 66        """
 67        return jsonify({"object": "list", "data": [{"id": MODEL, "object": "model", "owned_by": "qwen-asr-cli"}]})
 68
 69    @app.post("/v1/audio/transcriptions")
 70    def transcriptions():
 71        """
 72        概要:
 73            音声ファイルを受け取り、文字起こし結果を返すエンドポイントです。
 74        詳細説明:
 75            アップロードされたファイルを一時ディレクトリに保存し、外部プロセスで推論スクリプトを実行します。
 76            複数のリクエストが同時に来た場合は、スレッドロックにより順番に処理されます。
 77        戻り値:
 78            :returns: 文字起こしされたテキストと検出された言語を含むJSONレスポンス、またはエラーメッセージ
 79            :rtype: tuple
 80        """
 81        uploaded = request.files.get("file")
 82        if uploaded is None or not uploaded.filename:
 83            return jsonify({"error": {"message": "multipart field 'file' is required"}}), 400
 84        requested_model = request.form.get("model", MODEL)
 85        if requested_model != MODEL:
 86            return jsonify({"error": {"message": f"only model {MODEL!r} is available"}}), 400
 87
 88        suffix = Path(uploaded.filename).suffix.lower() or ".wav"
 89        with tempfile.NamedTemporaryFile(dir=UPLOAD_DIR, suffix=suffix, delete=False) as temporary:
 90            temporary_path = Path(temporary.name)
 91            uploaded.save(temporary)
 92        try:
 93            # One lock is intentional: concurrent requests must not each load a
 94            # 1.7B model on the same GPU. Later requests wait in the HTTP call.
 95            with INFERENCE_LOCK:
 96                completed = subprocess.run(
 97                    [sys.executable, str(RUNNER), "--audio", str(temporary_path), "--model", MODEL],
 98                    text=True,
 99                    stdout=subprocess.PIPE,
100                    stderr=subprocess.PIPE,
101                    timeout=TIMEOUT_SECONDS,
102                    check=False,
103                )
104            if completed.returncode != 0:
105                message = (completed.stderr or completed.stdout or "Qwen-ASR runner failed").strip()
106                return jsonify({"error": {"message": message[-4000:]}}), 500
107            try:
108                result = json.loads(completed.stdout.strip().splitlines()[-1])
109            except (json.JSONDecodeError, IndexError) as exc:
110                return jsonify({"error": {"message": f"invalid runner output: {completed.stdout[-1000:]}"}}), 500
111            return jsonify({"text": result["text"], "language": result.get("language")})
112        except subprocess.TimeoutExpired:
113            return jsonify({"error": {"message": "ASR request timed out"}}), 504
114        finally:
115            temporary_path.unlink(missing_ok=True)
116
117    return app
118
119
120if __name__ == "__main__":
121    create_app().run(host="0.0.0.0", port=8001, threaded=False)