Skip to main content

音声認識のベストプラクティス

音声認識アプリケーションの音声準備、接続の再利用、Java の同時実行、非同期ジョブ、エラー処理について説明します。

入力音声の準備

入力の形式、サンプルレート、チャンネル数、長さが対象モデルの音声仕様を満たすことを確認します。
  • 収録品質を優先し、背景ノイズとエコーを減らします。ノイズ抑制や AEC を有効にする前に、同じ代表的な手動書き起こし付き音声データで処理前後の認識結果を比較します。
  • 8 kHz の電話音声には対応するモデルとサンプルレートを選択します。
  • 音声の言語と方言に対応するモデルを選択します。
  • 専門用語の認識が不正確な場合は、認識精度の向上に従ってホットワードやコンテキスト拡張を設定します。
  • 音声形式の確認
  • エンコードとサンプルレートの変換
  • 音声の切り出し
  • 動画から音声を抽出
FFprobe でコンテナー形式、コーデック、サンプルレート、チャンネル数を確認します。
ffprobe -v error -show_entries format=format_name -show_entries stream=codec_name,sample_rate,channels -of default=noprint_wrappers=1 input.mp3

ファイルの入力方法を選択する

ファイルの入力方法は API によって異なります。たとえば、Qwen-Audio-ASR-Flash は音声ファイルの URL と Base64 Data URI をサポートします。ローカル音声を Base64 Data URI にエンコードして直接送信できます。エンコード方法と呼び出し例はFlash HTTP APIを参照してください。 URL でファイルを送信する場合、タスク処理中に認識サービスが音声をダウンロードできることを確認します。アクセス権限、URL の有効期限、パブリックネットワークからのアクセスを確認してください。 本番環境では OSS などの安定したストレージにファイルを保存し、URL を送信します。バッチ処理ではアップロード済みファイルを再利用し、重複アップロードを減らします。SDK のローカルファイルアップロード機能を使用する前に、対応インターフェースの制限を確認してください。一時ファイルの仕組みと適用条件は 一時ファイル URL の取得を参照してください。

リアルタイムタスクと接続の管理

Qwen-Audio-ASR-Message、Qwen-Audio-ASR-Streaming、Fun-ASR-Realtime、Paraformer の run-task / finish-task プロトコルは、接続の逐次再利用をサポートします。
  1. 一意の task_id で run-task を送信し、task-started を受信してから音声を送信します。
  2. 音声送信後、同じ task_id で finish-task を送信します。
  3. サーバーの task-finished イベントを待ち、現在のタスクの完了を確認します。
  4. 同じ接続で新しい task_id を使用して次のタスクを開始します。1 つの接続で複数のタスクを同時実行しないでください。
Qwen3-ASR-Realtime はセッションプロトコルを使用します。セッション終了後は接続を明示的に閉じます。接続と終了の手順は Qwen3-ASR-Realtime のインタラクションフローを参照してください。

アイドル接続と失敗した接続の処理

  • タスク完了後、約 60 秒間新しいタスクがないと接続は自動的に閉じます。長いアイドル期間後は接続を再確立します。
  • タスク失敗、ネットワーク切断、プロトコルエラーの後は旧接続を閉じ、新しい認識オブジェクトを作成します。プール内のオブジェクトは無効化し、利用可能なものとして返却しないでください。
  • アプリケーション終了時や認識の提供を停止するときは、プールとその接続を閉じます。

タスク中の長い無音区間の処理

実行中のタスクに無音音声を継続して送信する場合は、対応 API に従って heartbeat=true を設定し、必要なエンコードの無音データを送信し続けます。タスク終了後のアイドル接続は、前のセクションのルールで管理します。モデルのパラメーターは次のドキュメントを参照してください。 Message クライアントイベント | Streaming クライアントイベント | Fun-ASR クライアントイベント | Paraformer クライアントイベント

Java の同時実行の設定

実行中の各認識タスクに専用の Recognition オブジェクトを割り当て、正常終了後に返却します。以下の例は Commons Pool 2 で認識オブジェクトを管理します。プールは一度作成し、呼び出し間で再利用します。

依存関係の追加

先にAPI キーの取得とSDK のインストールを完了します。このセクションの完全なサンプルには DashScope Java SDK 2.18.0 以降が必要です。以下の依存関係は DashScope Java SDK 2.23.2 と Commons Pool 2.12.0 を使用します。
  • Maven
  • Gradle
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dashscope-sdk-java</artifactId>
    <version>2.23.2</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.12.0</version>
</dependency>

接続数、オブジェクト数、リクエスト容量の区別

次の 3 つの DASHSCOPE_ 環境変数は SDK が読み取ります。プロセス起動前に設定してください。RECOGNITION_OBJECTPOOL_SIZE は本文のサンプルコードが読み取ります。
環境変数既定値用途
DASHSCOPE_CONNECTION_POOL_SIZE32SDK の共有 OkHttp クライアントが保持するアイドル HTTP 接続数。
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS32SDK 非同期リクエストの同時スケジュール容量。超過したリクエストはスケジュールを待機します。
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST32ホスト単位の非同期リクエストのスケジュール容量。
RECOGNITION_OBJECTPOOL_SIZE500このサンプルが保持できる認識オブジェクトの総数。Commons Pool の maxTotal で制御します。
同時実行数は同時に実行中のタスク数、RPS は 1 秒あたりのリクエスト数です。アプリケーションの同時実行数に合わせてワーカースレッド数とオブジェクトプール容量を設定し、モデル API のレート制限に従ってタスク開始頻度を制御します。
以下は Paraformer リアルタイム認識アプリケーションの容量計画用の参考値です。サーバー単位の同時実行数は、同時に実行中の認識タスク数を示します。サーバーはこの認識アプリケーションのみを実行します。
  • HTTP アイドル接続容量は、ピーク同時実行数の 2 倍以上を起点に調整します。
  • 非同期リクエスト全体とホスト単位のスケジュール容量は、最初は接続容量と同じ値にし、実際の待ち行列に応じて調整します。
  • 認識オブジェクトプール容量は、ピーク同時実行数の 1.5~2 倍を起点にします。サンプルの既定容量 500 はアプリケーション設定です。
Alibaba Cloud サーバー参考タスク同時実行数認識プール容量HTTP アイドル接続容量
4 vCPU、8 GiB1005002000
8 vCPU、16 GiB2005002000
16 vCPU、32 GiB4005002000
これらの設定は、Paraformer リアルタイム認識アプリケーション専用のサーバーに適用されます。音声送信速度、クライアントリソース、アプリケーション負荷に応じて容量を調整します。モデル API の制限はレート制限を参照してください。

認識オブジェクトプールの実装

オブジェクトプールのライフサイクルは次のとおりです。maxTotal に達し、アイドルオブジェクトがない場合、この例の borrowObject は返却を待ちます。オブジェクトは必要に応じて作成され、認識リクエストの開始時に WebSocket 接続が確立されます。
  1. Recognition オブジェクトを借用し、認識を開始して音声を送信します。
  2. 終了コマンドを送信して完了を待ちます。非同期呼び出しではエラーコールバックも確認します。
  3. 正常完了後に returnObject を呼び出し、後続タスクが再利用できるようにします。
  4. 失敗や中断時は invalidateObject を呼び出します。ファクトリーの destroyObject が接続を閉じます。
  5. 全タスクの終了後にプールを閉じ、アイドルオブジェクトが保持する接続を解放します。
コードを RecognitionPoolExample.java として保存します。16 kHz、16 ビット、モノラルの PCM ファイル asr-example.pcm を用意し、DASHSCOPE_API_KEY を設定します。コード内のワークスペースとアドレスを対象リージョンの設定に置き換えてください(エンドポイントを参照)。この例は 3 つのワーカースレッドで 9 つの認識タスクを処理します。
import com.alibaba.dashscope.audio.asr.recognition.Recognition;
import com.alibaba.dashscope.audio.asr.recognition.RecognitionParam;
import com.alibaba.dashscope.audio.asr.recognition.RecognitionResult;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.utils.Constants;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

public class RecognitionPoolExample {
    private static final int WORKERS = 3;
    private static final int TASKS = 9;
    private static final int COMPLETION_TIMEOUT_SECONDS = 60;

    public static void main(String[] args) {
        int exitCode = 0;
        try {
            run(args);
        } catch (Exception e) {
            e.printStackTrace();
            exitCode = 1;
        }
        // Exit this standalone example after all owned resources are closed.
        System.exit(exitCode);
    }

    private static void run(String[] args) throws Exception {
        String apiKey = System.getenv("DASHSCOPE_API_KEY");
        if (apiKey == null || apiKey.isEmpty()) {
            throw new IllegalArgumentException("Set DASHSCOPE_API_KEY first");
        }
        // Replace {WorkspaceId} with the workspace ID for the endpoint region.
        Constants.baseWebsocketApiUrl =
                "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference";
        Path audio = Paths.get(args.length == 0 ? "asr-example.pcm" : args[0]);
        if (!Files.isReadable(audio)) {
            throw new IllegalArgumentException("Audio file is not readable: " + audio);
        }
        int capacity = Integer.parseInt(System.getenv().getOrDefault(
                "RECOGNITION_OBJECTPOOL_SIZE", "500"));
        if (capacity <= 0) {
            throw new IllegalArgumentException("Pool size must be positive");
        }
        GenericObjectPoolConfig<Recognition> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(capacity);
        config.setMaxIdle(capacity);
        config.setBlockWhenExhausted(true);
        config.setMinIdle(0);

        GenericObjectPool<Recognition> pool =
                new GenericObjectPool<>(new RecognitionFactory(), config);
        ExecutorService executor = Executors.newFixedThreadPool(WORKERS);
        ExecutorService completionExecutor = Executors.newFixedThreadPool(WORKERS);
        try {
            List<Callable<Void>> tasks = new ArrayList<>();
            for (int i = 0; i < TASKS; i++) {
                tasks.add(() -> {
                    recognize(pool, audio, apiKey, completionExecutor);
                    return null;
                });
            }
            // Wait for every task, then propagate any task failure.
            List<Future<Void>> results = executor.invokeAll(tasks);
            for (Future<Void> result : results) {
                result.get();
            }
        } finally {
            executor.shutdown();
            completionExecutor.shutdownNow();
            try {
                if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
                    executor.shutdownNow();
                }
                if (!completionExecutor.awaitTermination(1, TimeUnit.MINUTES)) {
                    completionExecutor.shutdownNow();
                }
            } finally {
                pool.close();
                System.out.printf("Pool closed: created=%d, destroyed=%d, active=%d, idle=%d%n",
                        pool.getCreatedCount(), pool.getDestroyedCount(),
                        pool.getNumActive(), pool.getNumIdle());
            }
        }
    }

    private static void recognize(GenericObjectPool<Recognition> pool,
                                  Path audio, String apiKey,
                                  ExecutorService completionExecutor) throws Exception {
        // Example timeout; adjust it to the application's queueing policy.
        Recognition recognizer = pool.borrowObject(Duration.ofSeconds(30));
        boolean reusable = false;
        AtomicReference<Exception> failure = new AtomicReference<>();
        CountDownLatch completed = new CountDownLatch(1);
        try (InputStream input = Files.newInputStream(audio)) {
            RecognitionParam param = RecognitionParam.builder()
                    .model("qwen-audio-3.1-asr-flash-streaming")
                    .apiKey(apiKey).format("pcm").sampleRate(16000).build();
            ResultCallback<RecognitionResult> callback = new ResultCallback<RecognitionResult>() {
                @Override
                public void onEvent(RecognitionResult result) {
                    if (result.isSentenceEnd() && result.getSentence() != null) {
                        System.out.println(result.getSentence().getText());
                    }
                }

                @Override
                public void onComplete() {
                    completed.countDown();
                }

                @Override
                public void onError(Exception error) {
                    failure.set(error);
                    completed.countDown();
                }
            };
            recognizer.call(param, callback);
            // 100 ms of PCM16 mono audio at 16 kHz is 3,200 bytes.
            byte[] buffer = new byte[3200];
            int count;
            while ((count = input.read(buffer)) != -1) {
                if (failure.get() != null) {
                    throw failure.get();
                }
                recognizer.sendAudioFrame(ByteBuffer.wrap(buffer, 0, count));
                Thread.sleep(100);
                buffer = new byte[3200];
            }
            // stop() blocks in the SDK, so apply the deadline to the stop call itself.
            long deadline = System.nanoTime()
                    + TimeUnit.SECONDS.toNanos(COMPLETION_TIMEOUT_SECONDS);
            Future<?> stopTask = completionExecutor.submit(recognizer::stop);
            try {
                stopTask.get(Math.max(0, deadline - System.nanoTime()), TimeUnit.NANOSECONDS);
                long remaining = Math.max(0, deadline - System.nanoTime());
                if (!completed.await(remaining, TimeUnit.NANOSECONDS)) {
                    throw new TimeoutException("Timed out waiting for task completion");
                }
            } finally {
                stopTask.cancel(true);
            }
            if (failure.get() != null) {
                throw failure.get();
            }
            reusable = true;
            System.out.printf("Completed: request_id=%s, first_packet_ms=%d, last_packet_ms=%d%n",
                    recognizer.getLastRequestId(), recognizer.getFirstPackageDelay(),
                    recognizer.getLastPackageDelay());
        } finally {
            if (reusable) {
                pool.returnObject(recognizer);
            } else {
                // destroyObject closes the connection before discarding the object.
                pool.invalidateObject(recognizer);
            }
        }
    }

    private static class RecognitionFactory extends BasePooledObjectFactory<Recognition> {
        @Override
        public Recognition create() {
            return new Recognition();
        }

        @Override
        public PooledObject<Recognition> wrap(Recognition recognizer) {
            return new DefaultPooledObject<>(recognizer);
        }

        @Override
        public void destroyObject(PooledObject<Recognition> object) {
            if (object.getObject().getDuplexApi() != null) {
                object.getObject().getDuplexApi().close(1000, "pool cleanup");
            }
        }
    }
}
この例では、オブジェクトの借用待機上限は 30 秒、タスク終了の待機上限は 60 秒です。必要に応じて調整します。ライブ収録では、許容される開始遅延に合わせて待機上限を設定し、キュー内のタスク数を制限します。音声ソースを変更しても、完了確認、失敗時の無効化、リソース解放を維持します。

ウォームアップとレイテンシの測定

実際の認識リクエストを送信してウォームアップし、後続タスクで再利用できる WebSocket 接続を確立します。
  1. 測定前に本番と同じ同時実行数で呼び出します。1~2 分を開始時の参考とし、負荷に合わせて調整します。
  2. 再利用可能な接続ができてから安定稼働時のレイテンシを収集します。コールドスタートと安定稼働時のデータは分けて集計します。
  3. アイドル中に接続が閉じると、次の呼び出しで再接続が必要になる場合があります。新たな性能測定の前に必要に応じて再度ウォームアップします。
SDK の最初のパケットと最後のパケットのレイテンシに加え、アプリケーション全体の処理時間を記録し、それぞれの測定開始点と終了点を明確にします。キュー待ち、接続確立、音声送信、サービス処理の影響は指標によって異なるため、個別に確認します。

非同期文字起こしジョブの管理

同時実行数の多いバッチ文字起こしでは、ポーリングを減らすため、タスク完了のコールバックを使用します。設定とイベント形式については、非同期タスクのコールバックの設定をご参照ください。 完了通知を受信したら、data.task_id で業務タスクを特定し、data.task_status を確認します。タスクが成功していても通知に必要な結果が含まれない場合は、対象モデルの HTTP API で結果を取得し、文字起こしファイルをダウンロードして保存します。

送信、クエリ、アップロードの頻度を制御する

  • タスク送信:対象モデルのレート制限に従って送信頻度を制御します。
  • 状態クエリ:クエリ API のデフォルト制限は、RAM ユーザーを含む Alibaba Cloud メインアカウント単位で 20 QPS で、最大 100 QPS(引き上げの申請は Alibaba Cloud の担当営業にお問い合わせください) まで引き上げられます。ポーリングする場合は 2~5 秒の間隔を起点とし、合計クエリ頻度が制限内に収まるようタスク数に応じて調整します。クエリ、一括クエリ、キャンセルは非同期タスクの管理を参照してください。
  • 一時ファイルのアップロード:アップロード認証情報 API の制限は、Alibaba Cloud メインアカウントとモデルの組み合わせごとに 100 QPS で、引き上げはできません。本番環境や高い同時実行数では OSS などの安定したストレージを使用します。一時アップロードの手順と制限は一時ファイル URL の取得を参照してください。

認識結果の速やかな保存

タスク成功後は速やかに結果ファイルをダウンロードして保存します。たとえば、Qwen-Audio-ASR-Filetrans HTTP APIの transcription_url の有効期間は 24 時間です。有効期限内に結果をダウンロードして永続保存します。
  • レスポンスに results 配列が含まれる場合は、各 subtask_status を確認してから、成功したファイルの文字起こし JSON をダウンロードします。アプリケーションに必要なタイムスタンプ、話者情報などのフィールドを保存します。
  • タスクとファイルの対応関係、最終状態を保存します。ダウンロード失敗時もタスク記録を残し、URL の有効期間内に再試行します。

エラー処理と再試行

エラーの種類に応じて再試行の可否を判断します。リクエスト ID、タスク ID、モデル、リージョン、失敗した段階を記録し、入力や権限の問題と一時的なサービス障害を区別します。
問題対応
入力ファイルまたはパラメーターのエラーURL、エンコード、サンプルレート、チャンネル数、パラメーターの範囲を確認し、修正後に再送信します。
認証またはエンドポイント設定のエラーAPI キー、ワークスペース、エンドポイントのリージョンを確認し、エラーメッセージに従って修正します。
レート制限リクエスト頻度やタスク開始頻度を下げ、レート制限に従います。間隔を置かない再試行ループを避けます。
ネットワークのタイムアウト、切断、一時的なサーバーエラー(5xx)クエリやダウンロードの失敗には、試行回数と合計待機時間に上限を設けたバックオフ再試行を使用します。非同期タスクの送信がタイムアウトした場合、task_id を取得済みなら状態を確認します。未取得なら、重複処理と重複課金に対する業務上の許容度に応じて再送信を判断します。
非同期タスクまたはファイル処理の失敗タスク単位とファイル単位のエラーを残し、原因を確認してから入力修正や再送信を判断します。
ジッターを加えた指数バックオフを使用し、再試行回数と合計待機時間を制限します。たとえば最大 3 回を起点とし、アプリケーションの遅延予算に合わせて調整します。リアルタイム接続が切断されたら、旧接続を閉じて新しい ID でタスクを開始します。最後に確認できた最終結果を基準に音声位置を記録し、必要に応じてアプリケーションのバッファーから未確認音声を再送します。元の音声位置に基づいて結果を結合し、重複を排除します。

関連ドキュメント