mcpField
A
asr-transcribe-to-text

asr-transcribe-to-text

github.com/daymadeクリエイティブ・メディア1.5k1.2k 回インストール今日更新
リポジトリ

asr-transcribe-to-text について

asr-transcribe-to-textは、音声や動画から話者ラベル付きのテキストを作成するスキルです。既存の正規トランスクリプトを先に確認し、クラウドまたはローカルのASR経路を選び、メディアの前処理や話者分離も行えます。

主な機能

  1. 01ローカルファイルとメディアURLに対応
  2. 02処理前に既存トランスクリプトを確認
  3. 03Feishu MinutesとローカルASR経路に対応
  4. 04音声入力の前処理、結合、検証
  5. 05話者ラベル付きテキストとCSVの生成
  6. 06タイムスタンプなしのテキストモード

活用シーン

  1. 01正規トランスクリプトがない録音の文字起こし
  2. 02文字起こし前の録音セグメント結合
  3. 03オフラインやプライバシー重視のローカルASR
  4. 04会議録音の話者タイムライン作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/daymade/claude-code-skills --skill asr-transcribe-to-text

インストール後、Claude Code で/asr-transcribe-to-textを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.gitignore
references/
batch_vad_coverage.md
decoupled_speaker_alignment.md
execution_location_and_comparison.md
local_mlx_guide.md
overlap_merge_strategy.md
review_workbench.md
speaker_diarization.md
voiceprint_speaker_id.md
whisper_word_timestamps.md
whispercpp_gpu_runtime.md
scripts/
align_speakers.py
build_whispercpp_gpu.py
diarize_speakers.py
diarize_speakers.py.lock
fuse_whispercpp_diarization.py
generate_audit_html.py
measure_asr.py
overlap_merge_transcribe.py
prepare_asr_input.py
resolve_media_input.py
scripts/review-workbench/
prepare_project.py
server.py
scripts/review-workbench/static/
app.js
index.html
styles.css
scripts/review-workbench/tests/
test_client_integrity.cjs
test_frontend.cjs
test_playback.cjs
test_prepare_project.py

詳細

ASR からテキストへの文字起こし

音声/動画を話者ラベル付きテキストに文字起こしします。ローカル実行には、明確に分かれた2つのルートがあります。長時間の録音や無人処理の録音には、チェックポイント付き whisper.cpp と Silero VAD ブロックを使用し、その後 pyannote の話者情報を後から統合します。短時間/中程度の録音には、Qwen3-ASR + mlx-whisper のアライメントルートを使用できます。どちらのルートでも、話者ダイアライゼーションの区切りで ASR 入力を分割することはありません。話者の割り当ては、連続した文脈での ASR の後に行います。

ASR の前にルートを決める:文字起こしは実行結果であり、実行そのものではありません

文字起こしを開始する前に、対象プロジェクトの文字起こしカタログ、外部ソースインデックス、宣言済みの過去作業の参照先を確認し、ソース ID、日付、タイトル、エンティティ語を使って、既存の正規の文字起こしを探します。人手で確認済みの現行文字起こしが見つかった場合は、ユーザーが新たな独立比較を明示的に求めていない限り、そこで作業を終了します。元の音声が存在することは、すでにあるテキストを再生成する理由にはなりません。

正規の文字起こしが存在しない場合:

  1. クラウド処理が許可されている通常の会議/DJI 録音には、Feishu Minutes を標準の優先ルートとして使用します(先に小さな M4A に前処理します)。
  2. ユーザーがオフライン/プライバシー対応を求めている場合、Feishu が利用できない、または失敗した場合、あるいはタスクで独立した品質比較が明示的に必要な場合は、ローカル ASR を使用します。 2b. 独立した2つ目の話者タイムラインには、stepfun-asr スキルの asr_file.py を介して StepFun の非同期ファイルエンドポイントを使用します。ここで利用できるベンダーの話者ダイアライゼーションルートのうち、ローカルの pyannote とコードもモデルも共有しないのはこれだけです。そのため、両者の一致は、同じツールが自分自身と一致しているだけではなく、実際の裏付けとなります。2026-09-19 に10分の映画で測定したところ、両者は重要なターンを同じように分割しましたが、Feishu Minutes は誤った話者に割り当てました。制約として、公開 URL を取得する方式であり、base64 は拒否されるため、ステージングが必要です(aliyun-multi-account.md を参照)。帰属が重要な根拠となる場合に使用し、定常的な文字起こしには使用しないでください。
  3. 会議以外のメディア、またはローカル/リモート ASR の明示的なリクエストでは、以下の音声配置ルールに従って実行場所を選択します。

2つのルートを実行したように見せるためだけに、ローカル ASR を実行しないでください。

ローカル実行とリモート実行のどちらを選ぶか決める前に、実行場所と比較を読んでください。まず既存のテキストと検証済みチェックポイントを使い、ソースの配置、処理の許可、実際の GPU/モデルの識別情報、エンドツーエンドの品質を示す根拠を比較します。測定値がない場合は、音声の近くで利用可能なルートを優先し、転送を避けられることを明記してください。より高速だとは主張しないでください。

設定は ${CLAUDE_PLUGIN_DATA}/config.json に保存されます。

話者ラベルがデフォルトです。 各実行では [start-end] SPEAKER_xx: text

  • CSV が生成されます。プレーンテキストのみの出力は(--no-diarization)でオプトアウトします。独白、 ポッドキャスト、または要約だけが必要な場合に使用してください。Step 3 を参照してください。これによりすべてのタイムスタンプも削除されます。プレーンテキストの経路では、セッションのテキストだけが返され、それ以外は何も返されません。字幕や、特定の時点にジャンプできるアーカイブなど、独白に行単位のタイムスタンプが必要な場合は、完全なパイプラインを実行し、SPEAKER_00 ラベルは無視します。

話者ダイアライゼーションの初回セットアップ: pyannote はアクセス制限付きの HuggingFace モデルであり、一度トークンが必要です(下記の ## Speaker Diarization & Identification を参照)。トークンなしで初回実行すると、セットアップ手順が表示されて失敗します。セットアップ後は、すべての機能が恒久的に利用可能になり、自動検出されます。

Step 0:プラットフォームを検出して設定を読み込む

cat "${CLAUDE_PLUGIN_DATA}/config.json" 2>/dev/null

設定が存在する場合は、値を読み込んで Step 1 に進みます。

設定が存在しない場合は、まずプラットフォームを自動検出します。

python3 -c "
import sys, platform
is_mac_arm = sys.platform == 'darwin' and platform.machine() in ('arm64', 'aarch64')
print(f'Platform: {sys.platform} {platform.machine()}')
print(f'Apple Silicon: {is_mac_arm}')
if is_mac_arm:
    print('RECOMMEND: local-mlx')
else:
    print('RECOMMEND: remote-api')
"

次に、プラットフォームに応じたデフォルト値を使って AskUserQuestion を使用します。

macOS Apple Silicon の場合(推奨:ローカル):

ASR setup — your Mac supports local MLX; choose from data location and the execution SOP.

Q1: Transcription mode?
  A) Local MLX — runs on your Mac's GPU, no API key needed
  B) Remote API — send audio to a server (vLLM, Tailscale workstation, etc.)

Q2: Does your network have an HTTP proxy that might intercept traffic?
  A) Yes — bypass proxy for ASR traffic (Recommended if using Shadowrocket/Clash)
  B) No — direct connection

その他のプラットフォームの場合(推奨:リモート):

ASR setup — local MLX requires macOS Apple Silicon. Using remote API mode.

Q1: ASR Endpoint URL?
  A) https://asr.example.com/v1/audio/transcriptions (Self-hosted remote ASR)
  B) http://localhost:8002/v1/audio/transcriptions (Local ASR server)
  C) Custom URL

Q2: Proxy bypass needed?
  A) Yes (Recommended for Shadowrocket/Clash/corporate proxy)
  B) No

設定を保存します。

mkdir -p "${CLAUDE_PLUGIN_DATA}"
python3 -c "
import json
config = {
    'mode': 'MODE',           # 'local-mlx' or 'remote-api'
    'model': 'MODEL_ID',      # local: 'mlx-community/Qwen3-ASR-1.7B-8bit', remote: 'Qwen/Qwen3-ASR-1.7B'
    'max_tokens': 8192,       # local only; PER 20-minute chunk, not per recording
    'endpoint': 'URL',        # remote only
    'noproxy': True,
    'max_timeout': 900        # remote only
    # 'diarization_declined': True  # set only after the user explicitly declines
    #   the pyannote setup in Step 3 — every run then warns + goes plain-text
    #   until an HF token appears (auto-detected)
}
with open('${CLAUDE_PLUGIN_DATA}/config.json', 'w') as f:
    json.dump(config, f, indent=2)
print('Config saved.')
"

ステップ 1:入力を解決する

ローカルファイル、メディアの直接 URL、ウェブページまたはポッドキャストのエピソードページを受け付けます。

  • ウェブページまたはポッドキャストページの URL:まず、ページに既存の文字起こしがあるか確認します。公式またはプラットフォーム提供の文字起こしは、ユーザーのアカウントから直接アクセスできる場合にのみ使用してください。文字起こしのエンドポイントにログイントークンが必要で、それが利用できない場合は、その旨を明確に伝え、音声 URL からの ASR に切り替えてください。
  • ローカルファイル、メディアの直接 URL、またはページ URL からのフォールバック:同梱のリゾルバーを実行します。一般的なページメタデータ(og:audio、メディアタグ、JSON-LD、RSS 形式の enclosure リンク)からメディアを抽出し、アトミックな一時ファイル置換で URL をダウンロードします。存在する場合はリモートの Content-Length を検証し、SHA-256 を計算して、ffprobe で結果を検証します。
uv run ${CLAUDE_SKILL_DIR}/scripts/resolve_media_input.py \
  INPUT_FILE_OR_URL [INPUT_FILE_OR_URL2 ...] \
  --output-dir OUTPUT_DIR \
  --manifest OUTPUT_DIR/media_manifest.json

疑わしいダウンロードや重要度の高いダウンロードでは、文字起こしの前にファイル全体を ffmpeg でデコードするため、--decode-check を追加してください。

uv run ${CLAUDE_SKILL_DIR}/scripts/resolve_media_input.py \
  "https://www.xiaoyuzhoufm.com/episode/EPISODE_ID" \
  --output-dir OUTPUT_DIR \
  --manifest OUTPUT_DIR/media_manifest.json \
  --decode-check

想定される出力:

Downloaded ... bytes in ...s -> OUTPUT_DIR/episode-title.m4a
OUTPUT_DIR/episode-title.m4a

後続の手順では、表示されたローカルパスを INPUT_AUDIO として使用します。実行環境で置換後のパスではなく ${CLAUDE_SKILL_DIR} がそのまま表示される場合は、このドキュメントの末尾にある「トラブルシューティング」の項目に従ってスキルディレクトリを解決してください。

サードパーティの公開ポッドキャストや著作権で保護されたメディアの場合、文字起こしはユーザー個人の分析用としてローカルファイルに保存してください。長い文字起こし全体をチャットに貼り付けず、パス、プレビュー、要約、または短い抜粋を提示してください。

ステップ 2:音声を抽出する(入力が動画の場合)

動画ファイル(mp4、mov、mkv、avi、webm)の場合、16kHz のモノラル WAV として抽出します。

ffmpeg -i INPUT_VIDEO -vn -acodec pcm_s16le -ar 16000 -ac 1 OUTPUT.wav -y

音声ファイル(wav、mp3、m4a、flac、ogg)はそのまま使用できます。長さを取得します。

ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 INPUT_FILE

クリーンアップ:文字起こしが成功した後、ディスク容量を節約するため、抽出した WAV ファイルを削除します。

前処理:セグメントの結合と従量制アップロードのサイズ削減(任意)

次のいずれかに該当する場合は、文字起こしの前に実行します。

  • 録音が複数セグメントに分割されたダンプである場合 — ボディマイクやフィールドレコーダーは、セッションを固定長のファイルに分割します(例:TX02_MIC024_....wav、TX02_MIC025_....wav。TX01/TX02 = DJI MIC MINI 2S の内部録音 — デバイス一覧とレコーダー→Feishu-Minutes の経路については、meeting-ingest スキルの meeting-ingest/references/architecture.md §①-L0 を参照)。セッションの順序を保つため、一度だけ結合します。明示的な長時間音声ランナー(ステップ 3 のパス L)が、その後の安定したソース時間ブロックを管理します。デバイスごとのセグメントを個別に文字起こしすると、これらのチャンク内のセグメントをまたぐコンテキストが失われます。
  • 音声を従量制の ASR(Feishu Minutes、分単位のクォータがあるサービスなど)に送信する場合 — ピッチを保ったまま高速化すると、請求対象の時間を直接短縮できます。最新の ASR では問題ありません。Feishu Minutes では、2026-07-16 にユーザーが 1.3 倍速で認識品質に体感できる差がないことを確認しました。また、公開されている Whisper のベンチマークでは、2.0 倍速になるまで WER は大きく低下しません(1.5 倍速以下は安全圏で、1.5 倍速時の WER 増加は約 3%、2 倍速超では実用不可)。

同梱のスクリプトを使用します。ファイルを結合し、16 kHz モノラルに正規化し、必要に応じて高速化します。また、ffmpeg の終了コードを信用せず、出力を自動検証します。

uv run ${CLAUDE_SKILL_DIR}/scripts/prepare_asr_input.py SEG1.wav SEG2.wav -o merged.wav   # merge only
uv run ${CLAUDE_SKILL_DIR}/scripts/prepare_asr_input.py SEG*.wav -o upload.m4a --speed 1.3  # merge + quota-saving speedup

想定される出力:

Merge order:
  1. SEG1.wav  [pcm_s24le 48000Hz ch=1 1800.14s]
  2. SEG2.wav  [pcm_s24le 48000Hz ch=1 1800.15s]
[OK] duration: 4946.19s vs expected 4946.18s (delta +0.00s)
[OK] boundary 1 @ 1384.7s: max_volume -15.5 dB
[info] overall: mean_volume -38.3 dB, max_volume 0.0 dB
Wrote upload.m4a
  • ファイル名に YYYYMMDD_HHMMSS のタイムスタンプが含まれているファイルがすべての場合、セグメントはそのタイムスタンプ順に並びます(レコーダーのダンプでは通常含まれています)。そうでない場合は、指定された順序が維持され、その旨の注記が表示されます。文字起こしの前に、表示された結合順序を目視で確認してください。

  • 自動検証:出力時間は Σセグメントの合計時間 ÷ 速度と一致する必要があります(±1.5 秒。そうでない場合は必ず FAIL)。各接合部では 10 秒間の音量をスポットチェックします(境界で無音になる場合は、順序が誤っているかセグメントが欠落しています)。比較用に、全体のラウドネスもソースとともに表示されます。

  • 高速化には、ピッチを保つ atempo 方式の時間伸縮を使用します。ピッチが変わり、ASR の精度と話者ダイアライゼーションの声紋の両方に悪影響を与えるサンプルレートのごまかしは、絶対に使用しないでください。

  • 送信先に応じて出力形式を選択します — コーデックはファイル拡張子に従います。

    送信先形式理由
    ローカル MLX パイプライン(パス A).wav または .m4aどちらもパイプラインに直接渡せます(m4a は 2026-07-18 に検証済み。3 分の音声を問題なく文字起こしできました)。M4A は約 5 分の 1 のサイズです。2 時間 49 分の結合音声では、WAV が 324 MB であるのに対し M4A は 63 MB で、再生時間は秒単位で同じです
    従量制アップロード(Feishu Minutes、分単位のクォータ).m4a + --speed 1.3AAC 48k は ASR で音声品質の違いが認識できない水準で、同等の音声品質の mp3 より約 30%小さくなります。高速化により、請求対象の時間を約 23%短縮できます
    セルフホスト型 vLLM エンドポイント(パス B).oggMP3 が拒否される場合でも受け入れられ、WAV より約 8 分の 1 のサイズです。これにより、長時間の録音をサーバーの 25 MB リクエスト上限内に収められます。パス B の制限セクションを参照してください
    可逆アーカイブ.flacWAV の約 50%のサイズで、ビット単位で完全に保持されます
    上記の形式が送信先で拒否される場合のみ.mp3互換性のための代替形式です
  • ステップ 4 の検証に文字起こしが合格するまで、元のファイルを保持してください。

オプション:文字起こしのために Feishu Minutes にアップロードする

前処理の後、ユーザーが Feishu Minutes を希望する場合、または通常の会議/DJI 録音に正規の文字起こしがなく、クラウド処理が許可されている場合は、この経路を使用します。失敗したローカル実行の後にのみ使う代替手段ではなく、通常の会議音声の処理経路です。

トリガーフレーズ:传到妙记 / 上传到飞书妙记 / 让妙记转写 / この音声から議事録を作成 / Feishu minutes にアップロード。

アップロード前に、求められている成果を把握します:

  • アップロードのみ — ユーザーが Minute のリンクだけ、またはメディアのアップロードだけを明示的に希望しています。minute_url の作成が最終状態です。
  • 文字起こしのみ — ユーザーは文字起こし/要約を希望していますが、プロジェクトへの登録は希望していません。アップロード後、文字起こしの準備ができるまで、現行バージョンに対応する lark-minutes の手順に従います。
  • プロジェクトへの納品 — ユーザーは修正、振り分け、ナレッジベースファイル、プロジェクトインデックス、または Git の引き継ぎを希望しています。全体の処理は meeting-ingest が担当します。このスキルはその前処理を担うだけです。アップロード前にそのオーケストレーターへ切り替え、納品の受領が確認できるまで実行を続けます。
  • 後続処理が未指定 — リクエストで前処理/アップロードが指示されているものの(例:「上传到飞书妙记,先转成适合 ASR 的格式」)、Minute 作成後に何をするかが指定されていません。文字起こしやプロジェクト関連の処理範囲を勝手に決めず、指示されたアップロードを実行してから、永続トークン/URL とともに outcome_pending を返します。後から文字起こしまたはナレッジベースのリクエストがあった場合は、同じ Minute から再開します。最初の推測に依存したり、最初からやり直したりすることはありません。

ユーザーの最初の文節に「上传到妙记」とあるだけで、アップロードのみと判断してはいけません。同じリクエストまたはプロジェクトのコンテキストに明示された後続処理の内容を優先します。

制約:

  • プロキシ禁止:すべての lark-cli 呼び出しで LARK_CLI_NO_PROXY=1 を使用してください。
  • プロファイルは 1 つのみ:アクティブな Feishu プロファイルのみを使用してください。テナントプロファイルを順に試したり、テナントルーティングを呼び出したりしないでください。
  • ローカル ASR の重複実行はデフォルトで禁止:この分岐では Feishu が文字起こしを担当します。ローカル ASR は、オフラインまたは比較目的が明示されている場合にのみ実行します。
  • 最新の Feishu ガイダンスを読み込む:lark-cli-router を使用してから、バージョンに対応する lark-minutes のアップロード/詳細手順を読みます。このスキルに記載された古い停止条件を、現在の CLI の契約に上書きして適用しないでください。

手順:

  1. 可能であれば、上記のセクションですでに前処理された音声を使用します。Feishu は .m4a、.mp3、.wav、MP4/MOV コンテナ内の .aac を受け付けます。前処理の「Metered upload」行は、すでにこの形式に調整されています。ファイルは 6 GB 未満、再生時間は 6 時間未満にしてください。これらは Feishu のアップロード上限です。

  2. ユーザーとして Drive にアップロードします:

    LARK_CLI_NO_PROXY=1 lark-cli drive +upload \
      --file '<preprocessed-media-path>' \
      --name '<basename>' \
      --as user \
      --format json
    

    結果から file_token を記録します。パス検証またはマルチパート処理の失敗でコマンドがエラーになった場合は、むやみに再試行しないでください。形式またはサイズの戦略を変更して、もう一度だけ試してから、正確なエラー内容を報告します。

  3. その Drive ファイルから Minute を作成します:

    LARK_CLI_NO_PROXY=1 lark-cli minutes +upload \
      --file-token '<file_token>' \
      --as user \
      --format json
    

    結果から minute_token と minute_url を記録します。

  4. 事前に確定した最終処理の内容に基づいて分岐します:

    • アップロードのみ:minute_url を返して終了します。
    • 文字起こしのみ:現在の lark-minutes の詳細コマンドを使用し、準備完了まで待機する動作と --transcript を指定します。URL の作成は中間段階です。成功とみなすには、読み取り可能な文字起こし成果物が必要です。
    • プロジェクトへの納品:次の引き継ぎタプルを meeting-ingest に返し、同じ実行内で処理を続けます:prepared_media、file_token、minute_token、minute_url、next_required_phase=minute_ready。オーケストレーターはクラウド上の文字起こしを待ち、トークンを指定して取り込み/振り分け/委任を行う sync-feishu-minutes を呼び出し、完全な transcript-fixer を実行し、プロジェクトが管理するすべてのインデックスを更新して、Git の引き継ぎを完了し、納品を記録します。
    • 後続処理が未指定:minute_token、minute_url、outcome_pending、next_required_phase=outcome_decision を返します。ユーザーが後続処理を指定するまでは、文字起こしを待ったり、文字起こしをファイルとして登録したりしないでください。

準備完了までの待機がタイムアウトした場合、または後続段階がブロックされた場合は、最後に完了した段階と、永続的な minute_token/minute_url を正確に報告します。同じトークンから再開してください。新しい URL を取得するためだけに再アップロードしてはいけません。 期待される出力:

  • アップロードのみの成功:ユーザーが開ける単一の minute_url。
  • 後続処理が未指定の場合の成功:要求されたアップロードが完了し、実行は outcome_pending の状態で明示的に再開可能なままです。これをプロジェクトの納品と呼ばないでください。
  • 文字起こしのみの成功:minute_url と、読み取り可能な文字起こし成果物。
  • プロジェクト納品の成功:meeting-ingest がプッシュした納品レシートのみです。前処理、URL の作成、文字起こしのダウンロードは中間フェーズです。
  • 失敗:drive +upload または minutes +upload からの正確な API エラーと、次に推奨する操作を 1 つ。

誤ったスキルに入った場合の復帰:sync-feishu-minutes の実行中にこのリクエストを受けた場合は、求められる結果に応じて選択してください。アップロードのみの場合はこのスキルを使います。プロジェクト納品の場合は meeting-ingest を使い、これはこの前処理ブランチを呼び出してから、トークンのスコープに限定された Feishu 取り込みに戻ります。プロジェクト納品を Minute URL に黙って格下げしないでください。

ステップ 3:文字起こし(話者ラベルをデフォルトで付与)

パス L:長時間のローカル録音 — whisper.cpp + Silero VAD(30 分超のデフォルト)

30 分を超える録音、無人バッチ処理、または長い無音や会議後の環境音が含まれる可能性のあるソースには、このパスを使います。これは Apple Silicon 向けの長時間音声処理ルートです。ソース時間に基づくブロックをチェックポイントに保存し、各ブロックで whisper.cpp の Silero VAD を実行します。ブロックの境界では 2 秒の重複を重複排除し、その後で pyannote の話者ラベルを遅延融合します。

ランタイムアセットと GPU ビルドマニフェストは、オペレーターが明示的に指定する入力です。バッチ処理中にモデルを黙って検出またはダウンロードしないでください。このルートを使う前に、GPU のみのランタイム準備を読んでください。エージェントは既存のセットアップ承認の範囲内で、分離されたピン留め済みビルドを準備します。変更されていないバイナリの --use-gpu フラグは、GPU VAD の証明にはなりません。ランナーはモデルの読み込み前に未知のビルド識別情報を拒否し、ネイティブガードは推論前に CPU または未知のグラフ割り当てを拒否します。

# 1. Checkpointed ASR on the original source timeline
uv run ${CLAUDE_SKILL_DIR}/scripts/transcribe_long_whispercpp.py \
  INPUT_16K_MONO_PCM16.wav OUTPUT_DIR \
  --ffmpeg-path /absolute/path/to/ffmpeg \
  --whisper-bin /absolute/path/to/whisper-cli \
  --gpu-runtime-manifest /absolute/path/to/user-cache/gpu-runtime.json \
  --whisper-model /absolute/path/to/ggml-large-v2-or-v3.bin \
  --vad-model /absolute/path/to/ggml-silero-v6.2.0.bin

# 2. Independent speaker timeline (controlled FFmpeg decode; no TorchCodec path)
uv run --frozen ${CLAUDE_SKILL_DIR}/scripts/diarize_speakers.py \
  INPUT_16K_MONO_PCM16.wav OUTPUT_DIR/STEM.diarization.json \
  --device mps --ffmpeg-path /absolute/path/to/ffmpeg

# 3. Keep only speech-grounded ASR segments and assign speakers by time overlap
uv run ${CLAUDE_SKILL_DIR}/scripts/fuse_whispercpp_diarization.py \
  OUTPUT_DIR/STEM.whispercpp.json \
  OUTPUT_DIR/STEM.diarization.json \
  INPUT_16K_MONO_PCM16.wav OUTPUT_DIR

物理レコーダーが業務セッションの終了後も動作し続けていた場合は、根拠に基づくソースタイムスタンプを、該当する両方のステップに渡してください(--end-at SECONDS)。元の音声と上限のない ASR 証拠は保持しますが、会議後の車内ノイズや無音を無理に会議の文字起こしに含めないでください。

完了とは、最終的な TXT/CSV/融合レシートが存在し、冒頭、中間、末尾、およびすべての外側ブロック境界がサンプリングされていることを意味します。N/N blocks complete だけでは品質を示す根拠になりません。再実行時は、完了済みの各ブロックがキャッシュ済みであることを報告する必要があります。

公式のアーキテクチャ上の根拠:whisper.cpp は推論前に VAD で音声を抽出します。OpenAI Whisper は、繰り返しループを避けるために直前のテキストのコンテキストをリセットします。NeMo の長時間音声に関するガイダンスでは、オーバーラップするバッファーチャンクを使用します。プロジェクト固有の測定証拠とパラメーターの根拠は、references/speaker_diarization.md に記載されています。

パス A:ローカル MLX(macOS Apple Silicon)— 短時間・中時間向けの代替手段

分離された話者パイプラインを実行してください。依存関係のピン留め、チャンクごとの生成上限、再開可能なチェックポイント、モデルの読み込み、プロセスツリーのクリーンアップは、パイプライン内で処理されます。

入力の拡張子は、デコーダーの契約として信頼できません。Qwen ワーカーはまずピン留め済みの MLX デコーダーを使用します。そのデコーダーが認識済みの入力/コンテナーのデコードエラー(たとえば、miniaudio が Ogg/Opus を拒否する場合)を報告した場合は、ffmpeg を使ってピン留め済みのデフォルトモデル向けに一時的な 16 kHz モノラル PCM WAV を作成してから再試行します(カスタムのローカルモデルでは、宣言されたサンプルレートを使用します)。GPU、メモリ、および無関係なランタイムエラーは変更せずに伝播し、正規化を引き起こすことはありません。チェックポイント、出力名、来歴は元のソースバイトにひも付けられます。一時 WAV が完了成果物になることはありません。

uv run ${CLAUDE_SKILL_DIR}/scripts/speaker_transcribe.py \
  INPUT_AUDIO [INPUT_AUDIO2 ...] OUTPUT_DIR

無人バッチ実行:1回の呼び出しにつき1ファイル。いずれかのレッグで決定的な失敗が発生した場合 (たとえば、1つの録音がチャンクごとのトークン上限に達したときの ASR_DETERMINISTIC_BLOCKED: ChunkTokenLimitError)、設計上、複数入力の実行全体が終了し、 その後ろにキューされたファイルは開始されません — 31ファイルのバッチはそのようにして11番目のファイルで 停止しました。ファイル単位の呼び出しなら、被害はその1つの録音に限定されます。中間レッグは キャッシュされるため、他のファイルを再実行するコストはアラインメントのみです。

想定される出力(ファイルごと):

Device: mps
+ uv run .../transcribe_local_mlx.py ...        (leg 1: session text)
Chunk 1/6 starting at 0.0s (max_tokens=8192)
Chunk 1/6 committed: 6310 chars, 3812 tokens
+ uv run .../word_timestamps_whisper.py ...     (leg 2: timing lattice)
... diarization ...                             (leg 3: pyannote segments)
STEM: 42 turns, speakers=['SPEAKER_00', 'SPEAKER_01'], anchored_ratio=0.93
Wrote STEM.txt, STEM.csv, STEM.alignment.json, STEM.receipt.json

入力ごとの出力:<stem>.txt([MM:SS - MM:SS] SPEAKER_xx + テキスト)、 <stem>.csv(file,start,end,duration,speaker,text — レビュー UI と 声紋識別に供給)、<stem>.diarization.json、<stem>.alignment.json(来歴

  • anchored_ratio 信頼シグナル。0.5 未満の場合は大きな警告を表示 — 信頼する前に 音声とラベルを照合して確認してください)、および <stem>.receipt.json(ソースバイト、4つの最終成果物すべてのハッシュ、生成スクリプト、 固定されたモデル/依存関係、セマンティックパラメータを束縛するアトミックな完了記録)。 中間レッグは OUTPUT_DIR/_align/ にキャッシュされるため、再実行は安価です(--force は最終レッグをやり直します)。 各中間キャッシュのサイドカーは、ソース音声のバイト、生成スクリプトのバイト、 セマンティックパラメータ、成果物のバイトを束縛します。ファイルの存在だけでは キャッシュヒットとは見なされません。下流の完了チェックには最終レシートが必要であり、 空でない成果物やアラインメント JSON だけでは不十分です。Qwen のチャンクチェックポイントはそのステージングディレクトリの下にあり、 中断された実行はソース音声の SHA-256、生成/分割/依存関係のコントラクト、不変のモデル リビジョン、生成パラメータ、完了したチャンクハッシュを検証してから、録音を最初からやり直す代わりに 完了済みチャンクをスキップします。言語非依存の12文字 n-gram ガードが、最終配信前に高度に反復的なチャンクまたはセッション全体のテキストを拒否します。 品質ポリシー ID はチェックポイントの同一性の一部であるため、古い未チェックのパートが ガードをサイレントにすり抜けることはできません。

Qwen3 ルートを使用する前に、そのレッグを一度スモークテストしてください:

uv run ${CLAUDE_SKILL_DIR}/scripts/transcribe_local_mlx.py --smoke-test

同梱スクリプトの依存関係検証と Smoke test OK を期待してください。固定は scripts/transcribe_local_mlx.py で定義されています。パフォーマンス、チャンクごとのトークン セマンティクス、リソース境界、リカバリについては、references/local_mlx_guide.md をお読みください。

仕組み(そしてその理由): セッション全体の Qwen3-ASR テキスト + mlx-whisper の単語 タイムスタンプ + pyannote の話者セグメントを、事後にアラインメントします。これは、 ダイアライズされた各話者ターンを個別に文字起こしするという、品質を損なう旧来のカスケードを 回避します。その有界な生成チャンクを長尺の保証に昇格させないでください。実際の 複数時間の録音では、20分、10分、5分のウィンドウでトークン上限に達しました。アーキテクチャ、アラインメントアルゴリズム、障害モードについては、 references/decoupled_speaker_alignment.md を参照してください。

初回実行:pyannote には一度だけの HuggingFace トークンが必要です。 スクリプトがセットアップのヒントとともに終了した場合(終了コード 3)、停止して AskUserQuestion を使用してください:

Speaker diarization needs a one-time setup (gated model, free):
  1. Accept terms at https://hf.co/pyannote/speaker-diarization-3.1
  2. Run `huggingface-cli login` (or set HF_TOKEN)

Options:
A) Set it up now — I'll wait, then rerun with full speaker labels (Recommended)
B) Continue without speakers this time — plain text only
  • A → ユーザーがログインを確認したら、同じコマンドを再実行します。トークンは 毎回の実行で自動検出されます。完全な機能はそれ以降ずっと有効です。
  • B → 選択を永続化し(config.json の diarization_declined: true)、 同じコマンドを再実行します。スクリプトはフラグを検出し、2つのセットアップ手順を含む一行の警告を表示して、 その実行ではプレーンテキストに自動フォールバックします — --no-diarization を渡す必要はありません(フォールバックは自動になりました。ドキュメントだけでなく スクリプトで強制されます)。トークンがまだ見つからない間は、以降の実行でも同じ警告と継続が行われます。 後でトークンが現れると、ダイアライゼーションは自動的に再開します(トークンが 存在すればフラグは無視されます) — セットアップさえすればよいとユーザーがわかるように、このことを伝えてください。

プレーンテキストの高速パス(モノローグ、ポッドキャスト、「とにかく要約して」):

uv run ${CLAUDE_SKILL_DIR}/scripts/speaker_transcribe.py \
  INPUT_AUDIO OUTPUT_DIR --no-diarization

リモート/事前生成された ASR テキスト(例:Path B、または別の ASR サービスから取得したもの):Qwen3 の段階を省略し、そのテキストをアラインメントします。--text-file は 1 つの入力 wav に対して 1 つのトランスクリプトを対応付けます。複数の入力を渡すと拒否されます(1 つのトランスクリプトを複数のファイルにアラインメントすることはできません)。

uv run ${CLAUDE_SKILL_DIR}/scripts/speaker_transcribe.py \
  INPUT_AUDIO OUTPUT_DIR --text-file TRANSCRIPT.txt

Apple Silicon 以外のマシン: Whisper のタイミング取得段階は MLX 専用です。これがないと、話者を対応付けるタイミング格子が得られません。--no-diarization を指定して実行し、話者モードには現在 Apple Silicon が必要であることをユーザーに伝えてください(ローカル GPU を使わない代替手段として、Feishu Minutes など、話者分離機能を内蔵したクラウド ASR があります)。

多数の短いファイルを一括処理する前に(プロモーション動画、モンタージュ編集など、音楽のみの音声を含む可能性があるもの)は、以下の ## Batch Transcription (many short files) を読んでください。音楽のみのクリップが 1 つあるだけで、バッチ全体が 10 分以上停滞することがあります。

Path B:リモート API

リモートエンドポイントが返すのはプレーンテキストのみです。話者情報は、そのテキストをローカルのタイミング取得段階と話者分離段階(レッグ 1)にアラインメントして追加します。つまり Path B では、テキストをリモートから取得し、その後 --text-file を使って Path A のパイプラインを実行します。

既存の設定で、同梱のリモートランナーを使用してください。レスポンスのセマンティクスを確認し、ソースと結果のバイト列を紐付け、設定されたサービスが公開している場合は、観測された生成元の識別情報を記録します。共有モデルを切り替えることはありません。セルフホスト GPU のエビデンスとパフォーマンス比較については、実行場所と比較を参照してください。

python3 ${CLAUDE_SKILL_DIR}/scripts/transcribe_remote.py \
  INPUT_AUDIO OUTPUT.txt --config "${CLAUDE_PLUGIN_DATA}/config.json"

OUTPUT.txt と OUTPUT.txt.asr.json が生成されます。レシートが表すのはプレーンテキスト段階であり、認識品質や話者情報の補完を保証するものではありません。

その後、話者情報をローカルで付与します(Apple Silicon と pyannote トークンが必要です)。

uv run ${CLAUDE_SKILL_DIR}/scripts/speaker_transcribe.py \
  INPUT_AUDIO OUTPUT_DIR --text-file OUTPUT.txt
セルフホスト vLLM:わかりにくい形で問題を起こす制限

ここではバージョンが重要です。このうち 2 つはリリース間で挙動が変わりました。 以下の挙動は、Qwen/Qwen3-ASR-1.7B を提供する vLLM 0.15.2rc1.dev68(開発ビルド。0.15.2 のリリースはなく、PyPI では 0.15.1 から 0.16.0 に移行)でエンドツーエンドに測定し、その後 v0.26.0 のソースコードで再確認したものです。まず、お使いのバージョンを確認してください(pip show vllm)。#1 と #3 のバージョンに関する注記も読んでください。

1. WAV ではなく OGG を送信し、MP3 は決して使わないでください。 MP3 は 0.15.x では完全に拒否されますが、その場しのぎで WAV に変換すると、#2 のサイズ上限に引っかかります。

形式16 kHz モノラル、16 ビットで 60 秒受け入れ(0.15.x)
WAV pcm_s16le1,920 KBはい
FLAC1,092 KBはい
OGG Vorbis245 KBはい
MP3—いいえ

同じサンプルレートなら、OGG は WAV の約 8 分の 1 のサイズです。

ffmpeg -nostdin -v error -i INPUT -ar 16000 -ac 1 -c:a libvorbis OUTPUT.ogg

自分で形式を比較する場合は、ビット深度を指定してください。非可逆音源をデコードすると、ffmpeg はビット深度を広げることができるため、24 ビット FLAC は 16 ビット PCM よりも大きくなる場合があります。2 つの録音条件が揃っていなかっただけなのに、「FLAC は圧縮されない」と見えてしまいます。-sample_fmt s16 を追加してください。

MP3 が拒否される際は、HTTP 200 とエラー本文が返される点に注意が必要です。%{http_code} だけを確認するチェックでは、成功と判定されてしまいます。

HTTP=200
{"error": {"message": "Error opening <_io.BytesIO object>: Format not recognised.", ...}}

バージョンに関する注記: 0.15.x ではアップロードを BytesIO 経由で librosa/soundfile を使って読み込みます。この方法では、ホストの libsndfile がディスク上の MP3 を処理できる場合でも、MP3 は拒否されます。v0.26.0 では、soundfile の LibsndfileError の後に pyav によるフォールバックが追加されたため(multimodal/media/audio.py)、現在のリリースでは MP3/M4A をデコードできる可能性が高いですが、上記のサイズ上の理由から、OGG が引き続き最適な選択肢です。

2. リクエストの上限は 25 MB です。

{"error":{"message":"Maximum file size exceeded (parameter=audio_filesize_mb, value=28.6)",...}}

VLLM_MAX_AUDIO_CLIP_FILESIZE_MB のデフォルト値は 25 です(vllm/envs.py。0.15.1 から v0.26.0 まで変更なし)。OGG は約 245 KB/分なので、この上限に達するのは約 100 分後です。会議の録音なら十分ですが、丸一日の録音や複数セグメントを結合したデータは上限を超えます。処理時間が長く、影響がある場合は上限を引き上げてください。

VLLM_MAX_AUDIO_CLIP_FILESIZE_MB=800 vllm serve <model> --port <port> ...

3. v0.26.0 では、これとは別に音声 10 分の上限が追加されました。 サイズ上限を引き上げても、この制限は解除されません。これらは独立したチェックであり、この制限に達すると切り捨てではなく拒否されます。

Audio exceeds maximum allowed duration of 600s (metadata reports 5998.0s).
Set VLLM_MAX_AUDIO_DECODE_DURATION_S to increase this limit.

VLLM_MAX_AUDIO_DECODE_DURATION_S のデフォルト値は 600 で、envs.py のサイズ上限のすぐ後の行にあります。0.15.x には存在しないため、古いサーバーでは処理できる講義の長さのファイルが、新規インストールしたサーバーでは拒否されます。v0.26.0 以降では、両方を設定してください:

VLLM_MAX_AUDIO_CLIP_FILESIZE_MB=800 VLLM_MAX_AUDIO_DECODE_DURATION_S=36000 \
  vllm serve <model> --port <port> ...

4. huggingface.co に接続できないホストでは、モデルがすでにローカルにキャッシュされていても読み込みに失敗します。 vLLM は起動時に config.json に対して HEAD リクエストを送信し、5 回再試行してから終了します。ファイルが実際にはそこにあるのに、エラーには「キャッシュされたファイル内で見つけられなかった」と表示されます:

HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 vllm serve <model> ...

同じ症状でも、まず除外しておくべき別の原因があります。コンテナ化されたサーバーは独自の HF_HOME を持ち、ホストユーザーの ~/.cache/huggingface を参照できないため、ホスト上で ls できるモデルも、そのコンテナからは実際に見えません。

5. vLLM は長い音声をすでにチャンク分割します。これはクライアント側の分割より優れています。 SpeechToTextConfig には overlap_chunk_second=1 と min_energy_split_window_size=1600 が設定されています。つまり、固定オフセットではなく、約100 msのウィンドウ内で最も静かな箇所で分割するため、単語の間で区切られます。上記の上限を解除すれば、100分のファイルを1回のリクエストで送信できます。これが、以下のステップ5のフォールバックを、この処理に対応していないサーバーに限定している理由です。

サーバーを再起動する権限がありませんか? #2/#3 の上限はサーバー起動時に設定されるため、サーバーに手を加えられない場合はクライアント側で分割するしかありません。これがステップ5です。このようなエンドポイントでは、これはフォールバックではなく適切なツールです。

⚠️ ただし、overlap_merge_transcribe.py は 0.15.x の vLLM エンドポイントをそのままでは扱えません。チャンクを -acodec copy で chunk_NN.mp3 に切り出すため、入力がすでに MP3 の場合は MP3 が出力されます(#1 により拒否されます)。それ以外の入力では単純に失敗します。ffmpeg の終了ステータスを確認しないため、不正なチャンクが「ffmpeg failed」ではなく、後で JSON 解析エラーとして表面化します。また、チャンクは1つの TemporaryDirectory の中だけで作成され、その終了時に削除されるため、変換できるタイミングがありません。このようなエンドポイントでは、手動で OGG に分割して、それぞれのパートを送信してください:

ffmpeg -nostdin -v error -i INPUT -f segment -segment_time 900 \
  -ar 16000 -ac 1 -c:a libvorbis chunk_%02d.ogg

これではオーバーラップを使った結合が失われるため、文が継ぎ目で分断される場合があります。これこそ、#5 のサーバー側のエネルギーに基づく分割が回避しようとしている問題です。

リモートのヘルスチェックに失敗した場合は、次の順に診断してください:

  1. ネットワーク:ping -c 1 HOST または tailscale status | grep HOST
  2. サービス:tailscale ssh USER@HOST "curl -s localhost:PORT/v1/models"
  3. プロキシ:--noproxy '*' を切り替えて再試行します

4. 「実際に待ち受けているプロセスはありますか?」— ss だけでは判断できません。 自分のユーザーのプロセスしか表示されないため、別のユーザーとして、またはコンテナ内で実行されているサーバーは、問題なくトラフィックを処理していても見えません。あわせて Docker に問い合わせてください:

tailscale ssh USER@HOST "ss -ltn | grep -E ':(8000|8001|8002)'; \
  docker ps --format '{{.Names}}\t{{.Ports}}\t{{.Status}}'"

5. 「このタスクに GPU を利用できますか?」 — 別のモデルを起動する前に、メモリと宣言されたサービスの占有状況を確認してください。特に WSL では、compute-apps の一覧が空であることだけでは、GPU がアイドル状態だとは証明できません:

tailscale ssh USER@HOST "nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv"
# under WSL nvidia-smi is often off PATH: /usr/lib/wsl/lib/nvidia-smi

GPU を空けたり、コールドパスをテストしたりするために、別のセッションのサービスを再起動したりアンロードしたりしないでください。現在の占有状況を確認し、既存の互換性のある経路を使用してください。利用できない実行 ID は明示し、暗黙のうちに CPU にフォールバックしないでください。

ステップ 4:出力を検証する

文字起こし後、完全性を確認します。

  1. 出力が空でないことを確認します
  2. 文字数と密度(中国語は約400文字/分、英語は約200語/分)は検査の手掛かりとしてのみ使用し、最低出力量や不合格のしきい値にはしません。これらの数値に達するよう文章を決して水増ししないでください
  3. ローカル MLX の場合、チェックポイントマニフェストに status: complete と記載されていることを確認します。いずれかのチャンクがトークン上限に達すると、スクリプトは最終 .txt の書き込みを拒否します
  4. 旧式またはリモートの出力の場合、末尾を確認します。文の途中で終わっている場合は、途中で切れている可能性があります
  5. プレビューとして、最初と最後の約200文字をユーザーに表示します
  6. 話者の経路:アラインメントレポートを確認します。anchored_ratio は ≥ 0.5 が目安です(それを下回るとスクリプトが警告します)。話者数が録音内容として妥当か確認します(2人のインタビューで話者が5人と表示される、または独白が2人以上に分割される場合は、話者ダイアライゼーションで過剰に分割されています。ラベルを疑うべき場合については references/speaker_diarization.md を参照してください)

空の ASR 結果、ループの疑いがある結果、または所要時間が不自然な結果は、信頼できないドラフトとして扱い、音声内容は不明のままにします。元の出力は保持し、正式な文字起こしには書き込みません。採用する前に、影響を受けた区間を元の音声または既存の検証済み文字起こしと照合してください。単なる繰り返し(単語や歌詞の繰り返しを含む)だけでは、ASR の失敗や発話の不在を示す根拠にはなりません。no speech とラベル付けできるのは、申告された区間の元の音声を独立して検証した後に限られます。ASR 結果や密度のヒューリスティックだけを根拠にしてはいけません。

今回の実行が、既存の重要度の高い文字起こしに対する独立した証拠トラックである場合、完全性とは、ベースライン録音全体が完全なチェックポイント/最終受領書に到達していることを意味します。選択したクリップで確認できるのは、選択した発話だけです。文字起こし全体に関する主張や「より高品質な最終版」という主張の根拠にはできません。完全な出力を transcript-fixer に渡し、未解決の固有名詞の候補の判断は、人間によるゲートに委ねてください。

出力が途中で切れている場合や信頼できない場合は、タスクで許可済みの復旧手順を実行します。ソースを調べるか、検証済みの文字起こしを再利用し、観察された失敗に対処できる新しい入力、前処理、パラメーター、または証拠がある場合に限って再試行します。ソースの検証で実際に音声が密集していることが確認され、チャンクごとのトークン上限が原因で途中で切れた場合は、測定したチャンクごとの予算を最大16384まで増やして再試行します。そのような変更なしに、同じソース/モデルの実行を繰り返さないでください。部分的な出力は保持し、本当に新たな許可、費用、またはユーザー自身が決める必要のある選択がある場合に限って確認を求めます。

ステップ 5:フォールバック — オーバーラップマージ(リモート API のみ)

この方法を使う前に、サーバーが内部でチャンク分割するか確認してください。 vLLM は内部で分割します(パス B の制限 #5)。また、エネルギーベースの分割は、このスクリプトの固定オフセット方式より優れています。そのため、制御可能な vLLM エンドポイントでファイルが長すぎる場合、クライアント側で分割するのではなく、上限を引き上げて解決します。

エンドポイントがファイル全体を受け付けられない場合は、クライアント側でチャンク分割します。たとえば、長い音声を拒否する(固定コンテキストウィンドウ、厳格なリクエストあたりの時間制限)、毎回同じ入力長で OOM が発生する、または内部でチャンク分割しているものの上限を引き上げる権限がない場合です。

タイムアウトは別の問題であり、通常はより低コストな解決策があります。リクエストは受け付けられ、処理が継続していました。まず設定の max_timeout を引き上げてください(リアルタイムの約60倍で処理する場合、100分のファイルでも数分かかることがあります。デフォルト値ではそれより短い可能性があります)。十分に大きな上限を設定してもタイムアウトする場合に限り、チャンク分割を検討してください。その場合、サーバーは1回で処理するには実際に遅すぎます。

上記のいずれかに該当する場合は、チャンク分割による文字起こしに切り替えます。

python3 ${CLAUDE_SKILL_DIR}/scripts/overlap_merge_transcribe.py \
  --config "${CLAUDE_PLUGIN_DATA}/config.json" \
  INPUT_AUDIO OUTPUT.txt

18分のチャンクに2分のオーバーラップを設けて分割し、句読点を除去したファジーマッチングで結合します。アルゴリズムの詳細は references/overlap_merge_strategy.md を参照してください。

ローカル MLX モードでは、オーバーラップマージは不要です。付属のスクリプトは固定された Qwen の低エネルギー分割を使用し、すべてのチャンクをアトミックにコミットします。また、デフォルトではチャンクごとに max_tokens=8192 を使用します。

ステップ 6:文字起こしの修正を提案する

ASR の出力には、同音異義語、技術用語の誤認識、文の崩れなど、常に認識エラーが含まれます。文字起こしに成功したら、出力に対して transcript-fixer スキルを実行するよう積極的に提案してください。

Transcription complete: [N] chars saved to [output_path].

ASR output typically contains recognition errors (homophones, garbled terms, broken sentences).
Would you like me to run /daymade-audio:transcript-fixer to clean up the text?

Options:
A) Yes — run daymade-audio:transcript-fixer on the output now (Recommended)
B) No — the raw transcription is good enough for my needs
C) Later — I'll run it myself when ready

ユーザーが A を選択した場合は、出力ファイルのパスを指定して transcript-fixer スキルを呼び出します。2 つのスキルは、文字起こし → 修正 → レビューという自然なパイプラインを形成します。

ユーザーが同じターンですでに修正、複数トラックのマージ、または高品質な文字起こしをリクエストしている場合、そのリクエストによってすでに A が選択されています。同じ作業について再度承認を求めずに transcript-fixer に進んでください。

再設定

rm "${CLAUDE_PLUGIN_DATA}/config.json"

その後、ステップ 0 を再実行します。

バッチ文字起こし(多数の短いファイル)

外部 VAD を使用してバッチ対象を選択したり、空のウィンドウを解釈したり、未処理分をカバーできていると判断したりする前に、VAD とバッチのカバレッジを読んでください。ソースサンプル、候補ウィンドウ、試行、検証済みコンテンツを区別しています。選択したプロデューサーの既存のチェックポイントを再利用し、以下の分岐に従ってください。

多数のファイルを 1 回の transcribe_local_mlx.py 呼び出しに渡すと効率的です(モデルの読み込みは 1 回だけです)。ただし、すべてのファイルに実際の音声が含まれている場合に限ります。 バッチに音楽のみ/BGM のみのクリップ(短いプロモーション動画、ナレーションの代わりに字幕が表示されるモンタージュクリップなど)が含まれる可能性がある場合は、1 つのプロセスでまとめて処理しないでください。

  • 音楽やリズムのみの音声では、モデルが反復ループの幻覚に陥ることがあります(例:「One, two, three, one, two, three...」を延々と繰り返す)。チャンクあたり 8192 トークンの上限によってリソースの増加は抑えられます。また、12 文字の n-gram 品質ゲートにより、上限に達する前に止まったループも拒否されます。それでも問題のあるファイルが 1 つあると、チャンク全体のタイムアウトを消費し、バッチの処理が滞る可能性があります。
  • バッチジョブは、ファイルごとのタイムアウトを設けて、1 プロセスにつき 1 ファイルで実行してください(例:各呼び出しを timeout 240 または perl -e 'alarm 240; exec @ARGV' で囲み、失敗は条件に応じた復旧のために保持します)。処理が止まったファイルによる遅延を、バッチ全体ではなく 4 分に抑えられます。
  • 処理が止まったファイルは、まずソースと失敗原因を確認してください。出力予算を小さくする根拠がある場合に限り --max-tokens 3000 を使用します。切り詰められた結果はドラフトとして保持し、ステップ 4 の条件付き再試行ルールに従ってください。
  • 信頼できない文字起こしを示し、内容について根拠のない判定を下さないでください。ユニーク単語の比率が低いこと、テキストが空であること、不審な反復などは、確認の手がかりとして使用します。ステップ 4 のドラフト/採用の境界を適用してください。こうしたシグナルは、クリップにナレーションがないことを証明するものではなく、正当な反復発話や歌詞を破棄する根拠にもなりません。画面上の字幕の OCR は、実際のソースがその別個のタスクを裏付ける場合に限って使用してください。

単語レベルのタイムスタンプ(字幕、音声と映像の同期)

mlx-whisper の単語タイミングは、短・中尺の Qwen 話者パイプラインにおけるタイミング部分です(第 2 段階 — scripts/word_timestamps_whisper.py が自動的に実行します)。このセクションは、単語タイムスタンプを単独で使用する場合、つまり字幕生成、ナレーションとショット境界の同期、クリップごとのキャプション作成を対象としています。

Qwen3-ASR は LLM デコーダー型の ASR であり、ローカルとリモートのどちらの処理経路でも、アラインメント情報のないプレーンテキストを出力します。単語ごとの発話時刻を把握する必要があるタスクでは、word_timestamps=True を指定して mlx-whisper を使用してください。Whisper のクロスアテンションによる単語アラインメントは、この種のタスクにおけるデファクトのローカルソリューションです。

主なポイント(詳しい手順は references/whisper_word_timestamps.md を参照してください)。

  • モデル:mlx-community/whisper-large-v3-turbo(約 1.6GB)。純粋な文字起こしでは中国語の WER が Qwen3-ASR より高くなりますが、アラインメントタスクでは Qwen3-ASR は選択肢になりません。initial_prompt で対象分野の用語を事前に指定してください。
  • セグメント粒度の落とし穴:短い動画(15~40 秒)では、Whisper がクリップ全体を 1 つのセグメントとして返すことがよくあります。必ず単語リストを基にし、単語の中間時刻を使って各時間ウィンドウに割り当ててください。
  • 映像側の処理には ffmpeg のシーン検出(select='gt(scene,0.3)')を組み合わせます。非 ASCII 文字を含むパスでは PySceneDetect を避けてください。

話者のダイアライゼーションと識別(誰が何を話したか)

話者ラベルは、ステップ 3 のデフォルト出力です。どちらのローカルルートも ASR と pyannote を分離して、時間で統合します。どちらも話者交代の区切りを個別に文字起こしするわけではありません。このセクションでは、その構成要素について説明します。

  • 短時間/中時間のパイプライン — scripts/speaker_transcribe.py は、3 つの処理とアラインメントを 1 つのコマンドで実行し、話者ラベル付きの文字起こしと CSV を出力します。アーキテクチャ、アラインメントアルゴリズム、信頼性シグナル(anchored_ratio)、失敗モードについては、references/decoupled_speaker_alignment.md を参照してください。本番環境での注意点(過剰な分割、マイクのドメイン効果、ラベルを信頼すべきでない状況)については、references/speaker_diarization.md を参照してください。
  • ダイアライゼーションのみ — scripts/diarize_speakers.py は、speaker × time のセグメントのみを出力します(文字起こしは行いません)。
  • レガシーカスケード — scripts/speaker_transcribe_cascade.py は、分割してから文字起こしする旧式の方式です(ダイアライゼーション → 話者交代ごとに音声を切り出す → 各断片を ASR で処理)。切り出すたびに ASR の文脈が途切れるため、テキストの品質が低下します。セッション全体の ASR よりも、支配的な近接話者を断片ごとに分離するほうが適切な、極めてノイズが多い音声や大きな重なりがある音声に限って使用します。それ以外はすべて、デフォルトの分離方式を使用します。
  • 声紋による識別 — ダイアライゼーションのラベルは匿名(SPEAKER_00…)で、ファイルごとに付与されます。実名への対応付け、ファイル間での話者の統合、またはダイアライゼーションによる過剰な分割の統合には、scripts/voiceprint_id.py で CAM++ の声紋を使用してください。手順と重要な音響ドメインに関する注意点 — ある種類のマイクで作成した声紋は、別のマイクを使った同一人物の音声にはるかに照合しにくくなります — については、references/voiceprint_speaker_id.md を参照してください。

pyannote の初回セットアップ(ゲート付きモデル):hf.co/pyannote/speaker-diarization-3.1 で利用規約に同意してから、huggingface-cli login を一度実行します(または HF_TOKEN を設定します)。以降は実行のたびに自動検出されます。

文字起こしの監査とレビュー(HTML)

リスニング、話者の注釈付け、文字起こしの修正を 1 か所で行うには、同梱の Listening Desk / 声间 を使用してください。集中して確認できるレビューキューと、文字起こし全体のナビゲーション、同期されたソース音声、キーボードによる判断、永続的なローカル保存、元に戻す機能、JSON/CSV/Markdown へのエクスポートを組み合わせています。ブラウザーが、判断を経ずにモデルの候補を人間が確認した識別結果として扱うことはありません。

python3 ${CLAUDE_SKILL_DIR}/scripts/review-workbench/prepare_project.py \
  --csv /absolute/output/meeting.csv --audio /absolute/audio/meeting.m4a \
  --output /absolute/review/project.json --title "Meeting review"
python3 ${CLAUDE_SKILL_DIR}/scripts/review-workbench/server.py \
  --project /absolute/review/project.json --state-dir /absolute/review/state

サーバーが表示するローカル URL を開いてください。プロジェクトデータと判断結果は、Skill バンドルの外部に保存してください。複数のレコーダー、選択した音声サンプル、ソースのタイムスタンプ、状態の復元、辞書/キュー操作との境界については、Listening Desk ガイドを参照してください。

オフライン監査用エクスポート

オフライン閲覧や一括閲覧のために、既存の自己完結型ページを引き続き使用できます。ブラウザー内のエイリアスと問題フラグは、Listening Desk に保存されたアイテムごとの判定とは別のものです。エイリアスを独立して検証された本人確認情報として扱わないでください。

話者ダイアライゼーションの後、ファイルごとに CSV(file,start,end,duration,speaker,text)が生成されます。付属の監査 HTML ジェネレーターは、これらの CSV から、音声再生、ターンごとのフラグ/メモ、話者エイリアスの設定、エクスポート機能を備えた、閲覧を重視した単一のレビュー用ページを作成します。

話者文字起こしの出力ディレクトリから生成します:

uv run ${CLAUDE_SKILL_DIR}/scripts/generate_audit_html.py \
  OUTPUT_DIR \
  --output OUTPUT_DIR/audit/index.html \
  --audio-dir /path/to/original/audio

デフォルトでは、PROJECT_DIR の下にフラットな構成があることを想定します。つまり、PROJECT_DIR/*.csv に文字起こし、PROJECT_DIR/*.diarization.json にダイアライゼーションファイルがあり、元の音声ファイルは出力ファイルと同じ場所に置かれます。speaker_transcribe.py 自体は、CSV、TXT、ダイアライゼーションファイルを OUTPUT_DIR の直下に書き出します。プロジェクトの構成が異なる場合は、これらのパスを任意に上書きできます:

uv run ${CLAUDE_SKILL_DIR}/scripts/generate_audit_html.py \
  /path/to/project \
  --output /path/to/project/audit/index.html \
  --csv-dir /path/to/project/csv \
  --txt-dir /path/to/project/txt \
  --diarization-dir /path/to/project/diarization \
  --audio-dir /path/to/project/audio \
  --original-dir /path/to/project/original \
  --manifest /path/to/project/manifest.json \
  --title "Project Audit" \
  --subtitle "Speaker-labeled transcript review" \
  --storage-key "project-audit" \
  --known-speaker "Speaker A" \
  --known-speaker "Speaker B"

主な CLI オプション:

オプション意味
project_dirベースとなるプロジェクトディレクトリ(必須)
--outputindex.html の出力先
--csv-dir*.csv 形式の文字起こしファイルがあるディレクトリ
--txt-dir*.txt 形式のプレーンテキスト文字起こしファイルがあるディレクトリ(任意)
--diarization-dir*.diarization.json ファイルがあるディレクトリ
--audio-dir再生用音声ファイルがあるディレクトリ
--original-dir元のソースメディアがあるディレクトリ(任意)
--manifestファイル ID とメタデータを対応付ける JSON マニフェスト(任意)
--title / --subtitleページのタイトルとサブタイトル
--storage-key状態の永続化に使用する localStorage 名前空間
--known-speaker繰り返し指定可能。「Name」は色を自動割り当てし、「Name=#hex」は色を明示的に指定します
--material-final / --material-roughフィルタリングに使用する、繰り返し指定可能な素材分類ラベル

出力されるのは、外部依存関係のない自己完結型 HTML ファイル 1 つです。ブラウザーで開くと、ターンのレビュー、フラグ付け、注釈の追加ができます。エクスポートボタンを押すと、フラグが付いたすべての行について、理由とメモを含むレポートが作成されます。

トラブルシューティング

ローカル MLX でモデルの読み込みに失敗する

次のようなエラーが表示され、モデルの読み込みに失敗する場合:

AttributeError: 'str' object has no attribute '__module__'

エージェントが現在の同梱スクリプトを使用しているか確認してください。バージョンの固定は、そのスクリプトの PEP 723 宣言とランタイム依存関係チェックで管理されています。--smoke-test を実行して検証結果を確認し、成功するまでは長時間の文字起こしを開始しないでください。

セルフホストのリモートエンドポイントが音声を拒否する

以下の症状はいずれも、本当の原因とは別の要因を示しています。そのため、症状から見分けられるようにしておくことが大切です。詳しい説明と対処法については、Path B の「Self-hosted vLLM: the limits that fail in confusing ways」のセクションを参照してください。

症状実際の原因
Maximum file size exceeded (parameter=audio_filesize_mb, ...)25 MB の上限は、分数ではなくバイト数で数えられます。通常、WAV への変換で上限を超えます。OGG を送信してください(約 8 分の 1 のサイズです)
HTTP 200 なのに本文が {"error": ... "Format not recognised."}0.15.x サーバーに MP3 を送信しています。ステータスコードだけを確認するチェックでは、これを成功と判定してしまいます
Audio exceeds maximum allowed duration of 600sv0.26.0 で追加された、サイズ上限とは別個の上限です。サイズ上限を引き上げても、こちらは解除されません → VLLM_MAX_AUDIO_DECODE_DURATION_S
サーバーが起動せず、モデルがキャッシュにあるのに「couldn't find them in the cached files」と表示される起動時に huggingface.co への接続を試みています → HF_HUB_OFFLINE=1。コンテナ化されている場合は、コンテナの HF_HOME からホストのキャッシュが見えていないだけの可能性もあります
長いファイルで失敗し、クライアント側で分割しようとしているvLLM はすでに低エネルギーポイントで分割します。サーバーを再起動できない場合を除き、代わりに上限を引き上げてください(分割が適切な場合についてはステップ 5 で説明しています)
whisper のタイミング処理が httpx.ProxyError: 503 Service Unavailable で失敗する

word_timestamps_whisper.py はモデルを読み込む前に、huggingface.co にモデルの情報を問い合わせます。 プロキシトンネルが不安定な環境ではこのリクエストが失敗して処理が終了し、モデルファイルがすべて ディスク上にある場合でも、ファイル全体の処理が失敗します(あるバッチでは 44 ファイル中 6 ファイル)。 実行時に HF_HUB_OFFLINE=1 を設定してください。オフラインモードではキャッシュから直接読み込まれ、 接続を開始しません。上記の vLLM サーバーの項目と同じ環境変数、同じ理由です。

${CLAUDE_SKILL_DIR} が置換されない

このスキルのスクリプトパスでは、スキル自体のディレクトリを示す ${CLAUDE_SKILL_DIR} を使用します。 これはスキルの読み込み時に Claude Code によって置換されます。コマンドに ${CLAUDE_SKILL_DIR} が そのまま含まれている場合(ランタイムによっては置換されません)、次の順序でスキルのディレクトリを 特定してください。

  1. スキル読み込み時のエンベロープ:Base directory for this skill: <path> → <path> がスキルのディレクトリです。
  2. エンベロープがない場合 → 候補を検索し、現在のセッションで利用可能なスキルの一覧が示すものを選びます(インストール済みのコピーはソースチェックアウトに遅れていることがあります)。 find ~/.claude ~/.claude-profiles ~/.codex ~/workspace -maxdepth 7 -type d -name asr-transcribe-to-text 2>/dev/null | head -5

このドキュメント内の ${CLAUDE_SKILL_DIR} をすべて、特定した絶対パスに置き換えてください。

同梱リソース

スクリプト:

  • transcribe_remote.py — ソース/結果/生成元のレシートと、任意の実行時 GPU ID の観測情報を記録する、既存のリモート向けプレーンテキスト ASR
  • measure_asr.py — 明示的に指定した既存の ASR コマンドを測定し、元音声の品質を確認して、必要に応じて観測結果を比較
  • resolve_media_input.py — ローカルパス、直接指定されたメディア URL、ポッドキャスト/ウェブページを、検証済みのローカルメディアファイルに解決
  • prepare_asr_input.py — 複数セグメントの録音を結合し、ASR 用に正規化(16 kHz モノラル)。課金対象のアップロード向けに、ピッチを維持した速度向上を任意で適用。再生時間の計算と接合境界を自己検証
  • transcribe_local_mlx.py — ローカル MLX 文字起こし(macOS ARM64、PEP 723 の依存関係)、低エネルギー区間のサイズを制限したチャンク、アトミックなチェックポイント/再開、実行主体の生存確認との紐付け
  • transcribe_long_whispercpp.py — 長時間音声 ASR のデフォルト:明示的なソース時刻ブロック+重複部分の所有権管理+whisper.cpp/Silero VAD+アトミックなチェックポイント/再開
  • build_whispercpp_gpu.py — ネイティブ GPU グラフのチェックを行い、分離された固定バージョンの Metal ランタイムを準備。バイナリとマニフェストはユーザーキャッシュに保持
  • fuse_whispercpp_diarization.py — 正規化済み whisper.cpp 時刻セグメントと pyannote の音声/話者情報を後段で統合。根拠のない無音区間の幻覚を除去し、TXT/CSV/レシートを出力
  • speaker_transcribe.py — 短〜中程度の音声向け分離パイプライン(セッション全体に適用する Qwen3-ASR+whisper によるタイミング+pyannote)。--no-diarization でプレーンテキストの高速処理、--text-file でリモート ASR テキストまたは事前作成済みテキストを指定
  • align_speakers.py — 分離型アライメントのコア(stdlib):全文字起こしを whisper の単語ラティス+pyannote セグメントにマッピング。休止と話者の切り替わりでターンを分割しますが、ラテン文字の単語の途中では分割しません。デバッグ用に単独で使用可能
  • word_timestamps_whisper.py — mlx-whisper の単語レベルのタイムスタンプ → JSON タイミングラティス(Apple Silicon)
  • speaker_transcribe_cascade.py — レガシーの分割後文字起こし方式(極端にノイズが多い音声、または重複発話が多い音声専用)
  • diarize_speakers.py — 話者ダイアライゼーションのみ(MPS 上の pyannote 3.1)→ セグメントごとの JSON
  • voiceprint_id.py — CAM++ 声紋の登録/照合:匿名の SPEAKER_xx を実名に対応付け
  • overlap_merge_transcribe.py — 重複部分を統合するチャンク分割文字起こし(リモート API へのフォールバック)
  • generate_audit_html.py — speaker-transcribe の CSV 出力から、自己完結型の HTML 監査/レビュー用ページを生成

参考資料:

  • execution_location_and_comparison.md — 条件に応じたローカル/リモートの選択、既存の文字起こし/キャッシュの再利用、安全な実行時 ID と同一ソースの比較
  • whispercpp_gpu_runtime.md — パス L の GPU-VAD 準備、ネイティブ計算の証拠、チェックポイントからの復旧境界
  • decoupled_speaker_alignment.md — デフォルトのアーキテクチャ:分離する理由、アライメントアルゴリズム、信頼性のシグナル、障害モード
  • speaker_diarization.md — 本番環境での落とし穴:過剰なセグメント分割、マイク環境の影響、ラベルを信用すべきでないケース。レガシーのカスケード方式に関する注記
  • voiceprint_speaker_id.md — CAM++ 話者 ID:登録/照合、しきい値+マージンのゲート、音響環境に関する注意点、ブートストラップ
  • local_mlx_guide.md — パフォーマンスベンチマーク、チャンクごとのトークン/リソース契約、チェックポイントからの復旧、モデルの互換性
  • whisper_word_timestamps.md — mlx-whisper の単語タイミング:短〜中程度の Qwen パイプラインのタイミング処理。字幕/AV アライメントの単独レシピ
  • overlap_merge_strategy.md — 素朴なチャンク分割が失敗する理由、あいまい一致による統合アルゴリズム

コメント