wjs-transcribing-audio について
wjs-transcribing-audio は、音声や動画の発話を、元の言語のタイムスタンプ付き SRT にするスキルです。字幕や文字起こしの作成向けで、言語に応じて ASR を選び、単語単位の時刻から字幕を組み立てます。中国語では Volcano を優先します。
主な機能
- 01元言語に応じた ASR エンジン選択
- 02単語単位のタイムスタンプ生成
- 03SRT 字幕の組み立てと分割
- 04長時間音声の分割文字起こし
- 05字幕の重複検出と削除
- 06中国語 ASR の誤字修正
活用シーン
- 01動画の元言語字幕作成
- 02音声からタイムスタンプ付き文字起こしを生成
- 03翻訳済み字幕に不足する元言語 SRT の作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jianshuo/ claude-skills --skill wjs-transcribing-audioインストール後、Claude Code で/wjs-transcribing-audioを入力して読み込まれたか確認します。
同梱ファイルscripts/build_srt_from_asr.pyscripts/volc_asr_stream.py
詳細
wjs-transcribing-audio
音声入力 → 同じ言語のタイムスタンプ付き SRT 出力。**このスキルは、元の言語の SRT を作成するところまでです。**別の言語への翻訳は次のスキル(/wjs-translating-subtitles)で行います。
使用する場面
- ユーザーが動画または音声ファイルを提供し、元の言語での文字起こし/SRT を希望している場合。
- ユーザーが翻訳済みの SRT を持っているものの、元の言語の SRT がない場合。
- ユーザーが「做 SRT」/「字幕を作って」/「逐語録を出して」と依頼しており、まだ翻訳の手順を求めていない場合。
使用しない場面
- 元の言語の SRT がすでにある場合 →
/wjs-translating-subtitlesに進みます。 - ユーザーが話されている言語とは別の言語で文字起こしを希望している場合 → まずこのスキルを実行し、次に
/wjs-translating-subtitlesを実行します。 - ユーザーが吹き替え音声または映像への字幕焼き付けだけを希望している場合 → SRT があれば、このスキルは不要です。なければ、まずこのスキルを実行します。
ルーティング:使用するエンジン
| 元の言語 | デフォルトのエンジン | 理由 |
|---|---|---|
| 中国語(zh-CN、zh-HK、zh-TW) | Volcano(豆包) ASR | 中国語では Whisper より精度が明らかに高く、ユーザーの継続的な希望にも沿うため |
| その他(es、en、pt、fr、it、ja、ko、…) | OpenAI Whisper API(単語レベルの粒度) | Whisper の多言語対応は優れており、単語単位のタイムスタンプを使ってキューを独自に組み立てられるため |
| オフライン/API にアクセスできない場合 | ローカルの openai-whisper(medium) | 品質の下限を確保できます。同じループ/塊の失敗パターンが発生する場合があります |
中国語では、ユーザーが明示的に希望する場合、または Volcano が利用できない場合を除き、Whisper をデフォルトにしないでください。これは意図的なルーティング判断です。中国語 ASR の優先順位に関するユーザーのメモを参照してください。
OpenAI Whisper API の経路(中国語以外、および中国語でのフォールバック)
**重要な原則:response_format=srt をリクエストしないでください。**Whisper のキュー分割は、長い独白(30 秒の塊のキュー)や静かな区間(ループ状の幻覚)で失敗します。単語レベルのタイムスタンプをリクエストし、自分でキューを組み立ててください。後処理は決定的で、追加費用もかかりません。
response_format=srt を使わない理由
長いコンテンツで whisper-1 の SRT 出力を損なう、次の 2 つの失敗パターンがあります。
- **30 秒の塊のキュー。**長い独白では、
response_format=srtを指定したwhisper-1が、condition_on_previous_textの 30 秒のウィンドウ全体をカバーするキューを 1 つ出力します。文字起こし自体は問題ありませんが、画面上で読むにはタイミングが使いものになりません。 - **静かな末尾でのループ状の幻覚。**エネルギーの低い音声に対して貪欲法で
temperature=0を使うと、「你如果不把拥抱浪费写在这上面,你很难的」が 50 回繰り返されます。
どちらも、キューの境界を Whisper に決めさせることが原因です。解決策は、単語レベルのタイムスタンプと、句読点を考慮した独自のアセンブラーを使うことです。
API の呼び出し
# 1. Compress for upload — 64kbps mono MP3 is plenty for speech.
# OpenAI limit is 25MB per request; chunk into 10-min pieces
# (≈4.5MB at 64kbps) for resilience under flaky proxies.
ffmpeg -hide_banner -loglevel error -y \
-ss <start> -t 600 -i input.mp4 \
-vn -ac 1 -ar 16000 -c:a libmp3lame -b:a 64k chunk.mp3
# 2. Request word-level timestamps. Do NOT request response_format=srt.
import httpx, os
data = {
"model": "whisper-1",
"language": "es", # pin source language; never auto-detect
"response_format": "verbose_json",
"timestamp_granularities[]": "word", # ← the critical flag
"temperature": "0.2", # enable fallback chain (anti-loop)
}
with open("chunk.mp3", "rb") as f:
r = httpx.post(
"https://api.openai.com/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
data=data,
files={"file": ("chunk.mp3", f, "audio/mpeg")},
timeout=600.0,
)
r.raise_for_status()
j = r.json()
words = j["words"] # [{"word": "hola", "start": 0.12, "end": 0.34}, ...]
segments = j["segments"] # see surprise below
注意点:words[] に句読点がなく、segments[] には一貫性がない
Whisper の words[] 配列では、通常 word["word"] に句読点が含まれません。各エントリーは "做"、"个"、"测"、"试" のようなトークンのみです。句読点がある場合も、segments[] の text フィールドにだけ含まれます。
さらに、segments[] のテキストは、同じファイル内のチャンク間でも句読点の付け方が一定しません。79 分のポッドキャストの場合、チャンク 0 は句読点のない、1~2 秒の短いセグメントを 285 個(「做个测试」「你在」「呵呵」)出力する一方、チャンク 7 は句読点付きで 14~30 秒のセグメントを 34 個出力することがあります。同じ API レスポンス内で、この両方の挙動が起こります。
そのため、適切な方法は両方を組み合わせることです。自然な間(呼吸に沿った区切り)には segments[] を使いますが、独自のキューアセンブラーへの生入力として扱い、セグメントが長すぎる場合は単語のタイムスタンプを使って分割します。
キューの組み立て方
TARGET_DUR = 3.0 # try to make cues this long
MAX_CUE_DUR = 5.0 # never exceed
MAX_CHARS = 18 # ~one line at Fontsize 14 on 1080-wide vertical
MAX_GAP = 1.0 # silence threshold → force cue boundary
MIN_PIECE = 0.3 # below this, merge with neighbor
SPLIT_PUNCT = set(",。!?;,.;!?")
# Step A: merge short segments[] toward TARGET_DUR (use segments,
# not words — Whisper's segment boundaries are already
# pause-aligned).
def assemble(segments, offset):
cues, buf = [], []
def flush():
if buf:
cues.append((buf[0]["start"]+offset, buf[-1]["end"]+offset,
"".join(s["text"].strip() for s in buf)))
buf.clear()
for s in segments:
dur = s["end"] - s["start"]
# Long single segment WITH internal punct → split standalone
if dur > MAX_CUE_DUR and any(c in s["text"] for c in SPLIT_PUNCT):
flush(); cues.extend(split_long_segment(s, offset)); continue
if not buf: buf.append(s); continue
if (s["start"] - buf[-1]["end"]) >= MAX_GAP \
or (buf[-1]["end"] - buf[0]["start"]) >= TARGET_DUR \
or (s["end"] - buf[0]["start"]) > MAX_CUE_DUR:
flush()
buf.append(s)
flush(); return cues
# Step B: final pass — split every internal comma/period to its own cue
# (proportional timestamps by char position). Coalesce pieces
# shorter than MIN_PIECE forward.
# Step C: any cue still > MAX_CHARS gets split at the largest inter-word
# gap using words[] timestamps. Recursive until under cap.
プラットフォームの読み取りリズムに合わせて、TARGET_DUR と MAX_CHARS を調整してください。縦型 1080×1920 の映像に Fontsize=14 で字幕を焼き付ける場合、18 文字の上限が重要です。それより長いと複数行に折り返され、読みにくくなります。
運用上の詳細
- 認証: 認証情報は
~/code/.envにあります。実行前にset -a; source ~/code/.env; set +aで読み込んでください。 - このマシンの SOCKS プロキシ:
httpxにはsocksioエクストラが必要です。uvx --with httpx --with socksio python ...を使用してください(これがないとImportError: Using SOCKS proxy, but the 'socksio' package is not installedが発生します)。 - 分割: 64kbps のモノラル MP3 を 10 分ごとに分割したもの(各約 4.5MB)が、信頼性の面で最適です。20 分ごとの分割(約 9MB)では、不安定なプロキシ経由の場合に RST が発生することがあります。並列処理数は
max_workers=2のほうが4より安定します。 - 再試行: すべての API 呼び出しを 5 回の指数バックオフ(
time.sleep(min(2**n, 30)))でラップしてください。RemoteProtocolError: Server disconnectedはよく発生しますが、一時的なエラーです。 - オフセットの結合: 各チャンクの単語には、そのチャンクを基準としたタイムスタンプが付いています。結合時は、字幕キューを組み立てる前に、チャンクの絶対開始オフセットを各単語の
start/endに加算してください。 - ループ防止(念には念を):
temperature=0.2にしていても、サブチャンクでループすることがあります。組み立て後、各字幕キューのテキストをループ検出器にかけてください。長さが 8~40 文字のフレーズが 3 回以上連続して繰り返されていたら、そのキューを破棄します。
アンチパターン(避けてください)
- ❌ 約 2 分を超えるコンテンツで
response_format=srtをリクエストしないでください。 - ❌ 不適切な字幕キューを、2 回目の API 呼び出しで「修正」しないでください。 最初の呼び出しで塊状の字幕キューやループによる幻覚が発生した場合は、単語単位の粒度で一度やり直してください。問題のある範囲だけを再文字起こししないでください。
- ❌ 静かな音声(ヨガ、スピリチュアルなコンテンツ、ポッドキャストのアウトロなど)で
temperature=0を使わないでください。 貪欲デコードではループが発生します。0.2にするとフォールバックチェーンが有効になります。 - ❌
language=...を省略しないでください。 自動検出では、最初の 30 秒で中国語から日本語へ、またはスペイン語からポルトガル語へ誤判定されることがあり、その場合は文字起こし全体が誤ったものになります。
Volcano(豆包)ASR の手順 — 中国語に推奨
Volcano ASR は、標準中国語の精度(認識率、句読点、固有表現)で Whisper を日常的に上回ります。zh-* のソースでは、これをデフォルトとして使用してください。
⛔ ASR には絶対に 飞书妙记/lark-minutes を使用しないでください。 返されるタイムスタンプは発話ターン単位のみで、単語単位ではありません。これを使って作成した字幕は数秒ずれて、不自然な箇所で途切れます。ユーザーの継続的な指示:「今後、ASR に飞书妙记を使わないでください。SRT として使用できないので、覚えておいてください」。飞书妙记へのフォールバックはありません。Volcano が利用できない場合は、上記の OpenAI Whisper の単語単位の手順にフォールバックしてください(
language=zhを固定してください)。
ストリーミング WebSocket API を使用 — バイト列を送信するため、公開 URL は不要です
ファイル/录音文件识别/MediaKit API では、いずれもインターネットからアクセス可能な HTTP(S) 音声 URL が必要です。ユーザーは URL ホスティングを拒否しました(「サーバー側からこの mp3 を download するような方式はもう使わないで」――テザリングではトンネルが動作しません)。大模型流式语音识别(bigmodel streaming) API なら、WebSocket 経由で生の PCM バイト列を送信するため、URL は不要です。こちらが動作する方法です。 単語ごとのミリ秒単位のタイムスタンプが返されます。
同梱スクリプト(動作確認済みのこちらを使用してください):
# 1. Transcribe: pushes 16k mono PCM bytes over WebSocket → ASR JSON
# (decodes any input via ffmpeg; .pcm passes straight through)
export VOLC_ASR_APPID=… VOLC_ASR_ACCESS_TOKEN=… # credentials live with the user
python3 scripts/volc_asr_stream.py <clip.mp4|wav|mp3|pcm> <out.asr.json>
# 2. Build a clean, word-timed SRT from the ASR JSON
python3 scripts/build_srt_from_asr.py <out.asr.json> <out.srt> [max_chars=18]
# Segmentation knobs (optional):
# --max-chars N raise to keep cues whole / break on punctuation, not mid-sentence
# --soft-min N min chars before a ,、; flushes a cue (default 8)
# --strip-punct remove ALL punctuation from displayed text (clean subtitle look)
- エンドポイント:
wss://openspeech.bytedance.com/api/v3/sauc/bigmodel - ヘッダー:
X-Api-App-Key:{appid}、X-Api-Access-Key:{token}、X-Api-Resource-Id:volc.bigasr.sauc.duration、X-Api-Connect-Id:{uuid} - バイナリ v3 フレーミング: 4 バイトのヘッダー
[(ver<<4)|hdrsize, (msgtype<<4)|flags, (ser<<4)|comp, 0]。Full-client(0x1)+POS_SEQ は gzip 圧縮された JSON 設定を送信し、audio(0x2) パケットは gzip 圧縮された PCM チャンクを送信します(16 kHz、モノラル、s16le で 200 ms = 6400 バイト)。最後のパケットでは NEG_WITH_SEQ(0x3) を使用し、seq は負の値にします。サーバーの full-response(0x9) は gzip 圧縮された JSON を返します。 - 設定 JSON:
{user:{uid}, audio:{format:"pcm",rate:16000,bits:16,channel:1,codec:"raw"}, request:{model_name:"bigmodel", enable_punc:true, enable_itn:true, show_utterances:true}}。result_type:"single"は設定しないでください。各フレームで最新の文だけが返されます。デフォルトモードではすべての発話が蓄積されます。 - 結果の形式:
result.utterances[]。各要素にはtext(句読点付き)とwords[](トークン、およびミリ秒単位のstart_time/end_time)が含まれます。「AI」などのラテン文字トークンはstart=end=0で返されます。build_srt_from_asr.pyは前後の単語からタイムスタンプを補完します。 - build_srt_from_asr.py ではさらに、句読点に基づいて字幕キューを分割します(強い区切りの
。!?では確実に分割し、弱い区切りの,、;では--soft-min文字を超えた場合に分割します。--max-charsで上限を設定します)。表示する字幕から句読点をすべて除去する(--strip-punct)、えー/うーん/あっなどのフィラーを削除する、連続する短い重複トークン(才才→才)をまとめる機能もあります。各字幕キューのタイミングは最初と最後の単語に基づくため、音声と正確に同期し、ずれません。文の途中で区切られないようにするには、--max-charsを大きくして、文字数上限ではなく句読点で区切られるようにしてください。 - クリップごとの文字起こし: SRT のタイムスタンプがクリップ内の相対時間になるよう、各クリップの音声(16 kHz、モノラル PCM)を個別に文字起こしします。
- 認証情報: ユーザーと共有済みです(豆包音声エンジン)。スクリプトで使用できる環境変数名:
VOLC_ASR_APPID/VOLC_APPIDとVOLC_ASR_ACCESS_TOKEN/VOLC_TOKEN。FFMPEG_BINは任意です。- このユーザーの火山 ASR と TTS は、1 組の認証情報を共有しています。 同じ appid と access token を両方のサービスで使えます。これらは
~/code/.envのVOLC_TTS_APPID/VOLC_TTS_ACCESS_TOKEN(および~/.zshrcのVOLC_APPID)として保存されており、同じ値を参照するVOLC_ASR_APPID/VOLC_ASR_ACCESS_TOKENの別名も追加されています。そのため、set -a; source ~/code/.env; set +aだけで十分です。別の ASR トークンを探す必要はありません。別名がなくなった場合は、TTS の値を使って再作成してください(同じシークレットです)。
- このユーザーの火山 ASR と TTS は、1 組の認証情報を共有しています。 同じ appid と access token を両方のサービスで使えます。これらは
- 試しても動作しない方法(再試行しないでください): MediaKit asr-subtitles(doc 6448/2381968)には、引き続き公開 URL と別の MediaKit API キーが必要です。录音文件识别 ファイル API には公開 URL が必要です。
最終手段としてのローカル Whisper
オフラインの場合、API のクォータを使い切った場合、または非常に安価な粗い下書きが必要な場合に限って使用します。中国語の精度は明らかに低く、同じ blob/ループ関連の失敗も発生します。また、ローカル Whisper は CLI 経由で単語単位のタイムスタンプを出力しないため、適切な修正方法を利用できません。
ffmpeg -i input.mp4 -vn -ac 1 -ar 16000 -c:a pcm_s16le _audio.wav -y
uvx --from openai-whisper whisper _audio.wav \
--language zh --task transcribe \
--model medium --output_format srt --output_dir .
rm _audio.wav
中国語の精度を確保するには、実用上 medium が最低ラインです。small はスタジオ録音のように明瞭な英語の場合に限り使用できます。Whisper はミリ秒の区切りに . を出力しますが、ファイルは引き続き有効な SRT です。SRT を再生成する場合は、必ずミリ秒の区切りに , を使用してください。
AI による校正パス — 明らかな誤字を修正します(最終ステップ。中国語の場合は必ず実行)
Volcano ASR でさえ、画面上で見ると明らかにおかしい同音異字を出力します。確認された例:「总数」→「意数」、「需求」→「虚求」、「程序员」→「成员」。生の ASR テキストは最低限の基準であり、完成形ではありません。SRT を組み立てた後、文脈を使って明らかな誤りを修正するため、SRT 全体に対して Claude による校正パスを1回実行します。
このパスは Claude がセッション内で SRT を読むことで行います。外部 API 呼び出しはありません。.srt を上書きするか、<stem>.polished.srt に書き出します。
厳守事項 — これは修正であり、編集ではありません。
- ✅ 文脈から意図された語が明らかな場合に限り、明確な同音異字・誤字を修正します(意数→总数、虚求→需求)。
- ⛔ タイムスタンプ、キュー番号、キュー境界は絶対に変更しないでください。 既存の各キュー内のテキスト行だけを編集します。入出力のキュー数は同じにします。
- ⛔ 言い換え、文法の校正、短縮、表現の「改善」はしないでください。 話し言葉の調子、組み立て時に残されたフィラー、繰り返し、文の形を維持します。修正した文字を除き、SRT は引き続き音声と一語一句一致していなければなりません。
- ⛔ 固有名詞/人名/ブランド/製品名を勝手に「修正」しないでください。 同音異字から名前を推測すると、「黄一孟」→「黄一梦」のような誤りを招きます。そのまま残し、確認用にユーザーへ一覧を提示してください(下記の注意点を参照)。聞き取れなかった名前を決して作り出さないでください。
- ✅ gold-block-worthy な数字は、発話どおりに正確に維持します(50万、1,000万)。数字や単位を正規化しないでください。
ワークフロー: SRT を読む → タイミングを同一に保ったまま修正版 SRT を作成する → 変更点の短い差分リストを報告する(意数→总数 @ 00:01:11)。ユーザーがスポットチェックできるようにします。修正が不確かな場合は、推測せず元の表記を残し、固有名詞/不確かな項目のリストに追加します。
この作業は、セグメンテーション/クリップ作成の前に実行してください。後続のすべてのクリップに、修正済みテキストが引き継がれます。
出力
- ファイル名:
<source-stem>.srt(言語サフィックスなし。これは原文の SRT、マスターです)。 - 形式:標準 SRT、
HH:MM:SS,mmm(ミリ秒の区切りはカンマ)、1始まり。 - キューのルール:句読点で区切る。通常の長さは3~8秒。表示行あたり中国語は18文字以下、ラテン文字は42文字以下。
- 聞き取れない音声:必要な場合に限り
[inaudible]と記載します。推測しないでください。
引き渡し前の品質ゲート
- 字幕番号が連番になっている
- タイムスタンプが重なっていない
- ミリ秒の区切りにカンマを使用している
- キューが単語の途中で終わっていない
- 内部で分割されていないまま MAX_CHARS を超えるキューがない
- 同じフレーズが3回以上連続して繰り返されていない(ループの残り)
- AI による校正パスを実施済み — 明らかな誤字を修正し、タイムスタンプ/キュー数には手を加えず、固有名詞は勝手に変更せずユーザーに提示した
後続の処理
/wjs-mining-articles— 王建硕の独白/対談の SRT から、複数の微信公众号記事を作成します。/wjs-translating-subtitles— 原文 SRT を句読点で区切り直しながら、対象言語に翻訳します。/wjs-dubbing-video— ユーザーが原文の音声吹き替えを希望する場合のみ使用します(まれです)。通常は先に翻訳します。/wjs-burning-subtitles— ユーザーが原文 SRT を元動画に焼き込むことを希望する場合のみ使用します(例:聴覚障害者向けに、スペイン語の動画へスペイン語字幕を焼き込む場合)。
よくある落とし穴
- 60分のファイル全体を1回の API 呼び出しで送る。 OpenAI の上限は25 MBで、15分を超えると呼び出しが不安定になります。先に分割してください。
segments[]のテキストを正確なものとして扱う。 同じファイルでもチャンク間で句読点の付け方に一貫性がありません。組み立て処理を経ずに信用しないでください。- Whisper に言語を自動検出させる。 毎回、言語を指定してください。
- チャンクのオフセットを加え忘れる。 各 API の応答に含まれるタイムスタンプは、チャンクの t=0 を基準としています。チャンクの絶対開始時刻を加えずに結合すると、最初のチャンク以降のすべてのキューが数分ずれてしまいます。
- 固有名詞/人名はほぼ必ず誤りがあります。 ASR が人名、ブランド、製品名を同音異字として聞き取るのはよくあることです(実際に確認された例:「黄一孟」→「黄一梦」)。SRT 内の固有名詞はすべて、まず疑わしいものとして扱ってください。後続の文章作成/公開の前に(特に
/wjs-mining-articles)、必ずユーザーに確認し、誤字のまま書き出さないでください。