Views
No views yet
qwen_transcribe.py Qwen3-Omniで区間ごとのJSON文字起こし(start/end/spk/text)
sortformer_diarize.py Sortformerでフレーム単位の生の話者確信度(sigmoid)を取得
acoustic_categorize.py Sortformerの確信度を5段階(超低/低/中/高/超高)にカテゴリ化し、
Qwenの各区間に対応付ける
gemma_fusion.py 上記を統合し、Gemma(OpenAI互換API)に最終判定させる
pipeline.py 上記4つをsubprocessで束ねるオーケストレータ (CLI)
server_v2.py ブラウザから音声をアップロードして実行できるUI (FastAPI)
index_v2.html server_v2.py が返すページqwen_transcribe.py はvLLM(transformers>=5.5.3が必要)、sortformer_diarize.py は
NeMo(nemo_toolkitがtransformers==4.57.6に固定インストールする)を使っており、
この2つは同じPython環境に共存できません(pip install nemo_toolkitが
transformersを4.57.6に強制ダウングレードし、vLLMを壊します)。そのため:qwen_transcribe.py / acoustic_categorize.py / gemma_fusion.py / pipeline.py /
server_v2.py は system python (vLLM環境) で実行sortformer_diarize.py だけは 別のvirtualenv (NeMo環境) で実行pipeline.pyとserver_v2.pyは、正しいインタプリタでそれぞれをsubprocessとして
呼び出すことでこの制約を吸収しています。1# 1. vLLM環境 (system python)
2pip install vllm==0.25.1 # transformers>=5.5.3 が入る
3pip install fastapi uvicorn python-multipart librosa
4
5# 2. NeMo環境 (別のvirtualenv、vLLM環境とは絶対に混ぜない)
6python3 -m venv /root/nemo_venv
7/root/nemo_venv/bin/pip install 'nemo_toolkit[asr]'
8
9# 3. Qwen3-Omniモデル本体 (INT8推奨。bf16でも動くがVRAM 70GB以上必要)
10hf download infodeliverailab/qwen-3-omni-vllm-int8 --local-dir /root/model
11# (Sortformerは初回実行時にHugging Faceから自動ダウンロードされる)GEMMA_URL / GEMMA_MODEL / GEMMA_API_KEY は環境変数で渡します
(OpenAI互換の/chat/completionsエンドポイントであれば任意のプロバイダで動きます。
開発時は bytecompute.ai の gemma-4-31B-it で検証しています):1export GEMMA_URL="https://your-openai-compatible-endpoint/v1"
2export GEMMA_MODEL="your-model-name"
3export GEMMA_API_KEY="your-api-key"gemma_fusion.pyだけがこれらを読みます。pipeline.py/server_v2.pyは
subprocessを起動する際にこの環境変数をそのまま継承させているだけです。)1python3 pipeline.py path/to/audio.wav \
2 --workdir /root/diar_work \
3 --model /root/model \
4 --api-key "$GEMMA_API_KEY"--workdir以下に各段階の中間ファイルが残ります(qwen_segments.json /
sortformer_probs.npy / annotated_segments.json / final_diarized.json)。
最終結果は標準出力にも表示されます。1python3 pipeline.py a.wav b.wav c.wav \
2 --workdir /root/diar_work \
3 --model /root/model \
4 --api-key "$GEMMA_API_KEY"pipeline.pyを呼ぶと、Qwen3-Omni(vLLMエンジン)とSortformerを
ファイルごとに毎回ロードし直すため、torch.compileのキャッシュが効いていても
1ファイルあたり70〜90秒程度の起動オーバーヘッドが発生します(実際の生成自体は
短い音声なら数秒)。バッチモードではQwen3-OmniとSortformerをそれぞれ1回だけ
ロードし、複数ファイルを処理してから次の段階に進みます(実測: 6ファイルで
逐次実行が約9分半 → バッチモードで約110秒)。--workdirはこの場合、
ファイルごとのサブディレクトリ(ファイル名から拡張子を除いたもの)を作る
親ディレクトリとして扱われます。1python3 qwen_transcribe.py audio.wav --model /root/model --output qwen_segments.json
2/root/nemo_venv/bin/python3 sortformer_diarize.py audio.wav --output sortformer_probs.npy
3python3 acoustic_categorize.py qwen_segments.json sortformer_probs.npy --output annotated_segments.json
4python3 gemma_fusion.py annotated_segments.json --output final_diarized.json --api-key "$GEMMA_API_KEY"GEMMA_API_KEY=... uvicorn server_v2:app --host 127.0.0.1 --port 8090--host 0.0.0.0にしないこと。 RunPod等のpublic proxyに認証なしで晒され、
顧客音声や有償APIキーが誰でも使える状態になります。ローカル(127.0.0.1)で
起動し、ssh -L 8090:127.0.0.1:8090 user@hostのようなSSHポートフォワード経由で
アクセスしてください。1[
2 {"start": 0.0, "end": 4.02, "final_spk": "spk0", "text": "...", "reason": "..."},
3 ...
4]final_spkはspk0, spk1, ... (声質クラスタベース、役職や性別に固定の意味は
ありません)。reasonはGemmaが判定根拠を一言で述べたものです。model.diarize()が返すspeaker_0/speaker_1のような離散クラスタラベルを
そのままGemmaに渡すと、ほぼそのままエコーされるだけで実質何も統合されません。
model.forward()が返す生のsigmoid確信度を5段階(超低/低/中/高/超高)に
バケット化して渡すことで、初めてGemmaがQwenの推定との食い違いを検討できます。[MM:SS]textのようなタグ形式で開始・終了の両方を出させようと
すると、終了時刻をテキスト生成前に「先読み」する必要が生じ、タイムラインが
実際より大きく圧縮される崩壊が起きました。{"start":..,"end":..,"spk":.., "text":..}ずつのJSONオブジェクトを求める形式にしたところ、この崩壊は
起きず、短い割り込み発言の検出率も上がりました(ただし出力に稀に同じ
キーが重複するなど、フォーマット遵守が完璧ではない点は残っています)。config.jsonの
thinker_config.audio_configにn_window/n_window_inferや
seconds_per_chunk=2といった値があり、これは実際にモデル内部の音声処理が
何らかの窓単位で行われることを示唆しますが、これが本当に文字起こしの
不整合(発話の途中で話者ラベルが不自然に変わる等)の原因になっているかは
未検証です。config内のフィールド名から推測しただけで、実際の
attentionメカニズムを確認したわけではないので、確定的な結論として
扱わないでください。