Claude Media
ClaudeでSRT字幕を翻訳する — 1行の字数と秒数を検査して収める

ClaudeでSRT字幕を翻訳する — 1行の字数と秒数を検査して収める

SRTのタイムコードを崩さずにClaudeで字幕を訳し、1行の字数・読む速さ・表示秒数をスクリプトで検査して縮め直させる手順。物差しはNetflixの日本語字幕ガイドです。

ClaudeでSRT字幕を訳すとき、意味の質より先に壊れるのは「枠」です。訳文が原文より長くなり、1行に収まらず、読み切る前に次の字幕に切り替わります。

対策は、Claudeに訳させる範囲を本文だけに絞り、枠の判定を人の目ではなくスクリプトに任せることです。違反をClaudeに返して縮めさせれば、字数オーバーの手直しがほぼ自動で回ります。

この記事では、物差しにNetflixのパートナー向けスタイルガイドの数値を借り、日本語字幕の検査スクリプトとClaude Codeのhooks設定までを通しで扱います。

SRTを訳すと何が壊れるのか

SRTは、字幕の番号、開始 --> 終了 のタイムコード、本文の3点を空行で区切って並べた単純なテキストです。壊れやすいのは次の3つです。

  • タイムコードや番号の書き換え: ファイル全体を渡すと、表示時間を書き換えたり、2つの字幕を1つにまとめたりする余地が生まれます
  • 字数の超過: 英語から日本語、日本語から英語のどちらでも、訳文の長さは原文と揃いません
  • 読む速さの超過: 字数が1行に収まっていても、表示時間が短ければ読み切れません

3つ目が見落とされがちです。字数の上限は目視でも気づけますが、秒数との割り算は機械でないと続きません。

前提

訳す前に決めておくこと

  • 何を訳すか

    番号とタイムコードは訳させず、本文だけを渡して本文だけ受け取ります。

  • 何で測るか

    1行の字数、1秒あたりの字数、表示秒数の上限と下限を数値で決めます。

  • 誰が判定するか

    スクリプトが判定し、違反の一覧だけをClaudeに返します。

物差しにするNetflixの数値

字幕の字数や速さには、配信先ごとの規格があります。ここではNetflixが公開している言語別のタイムドテキスト・スタイルガイドを物差しにします。Netflixへ納品する案件に限らず、「どの数値をどの粒度で決めればよいか」の見本として使えるためです。YouTubeなど他の配信先へ出すときや、納品先の仕様が別にあるときは、その仕様に合わせて後述のスクリプトの数値だけを差し替えます。

項目英語字幕日本語字幕日本語SDH
1行の字数英語字幕42字日本語字幕全角13字(横書き)日本語SDH全角16字(横書き)
読む速さの上限英語字幕大人向け20字/秒、子ども向け17字/秒日本語字幕4字/秒日本語SDH7字/秒

SDHは聴覚障害者向けの字幕で、台詞以外の音の情報も入ります。通常の翻訳字幕は左の2列を見ます。

日本語は、全角・スペース・句読点を1字、半角を0.5字と数えます。1行13字は、全角で13文字分の幅ということです。

共通の規則も押さえておきます。

  • 1つの字幕イベントは最大2行です
  • 表示時間は最長7秒です。最短は共通ガイドで5/6秒(24fpsなら20フレーム)、日本語ガイドでは500ミリ秒と書かれています
  • 日本語字幕では、句点「。」と読点「、」を使いません。読点は半角スペース、句点は全角スペースに置き換えます。書名など公式訳に句読点が含まれる場合は例外です
  • 字幕と字幕の間は最低2フレーム空けます

最短秒数の記述は、ガイド間で表現が揃っていません。共通ガイドは5/6秒、日本語ガイドは0.5秒、タイミングガイドは「20フレーム(または4/5秒)」です。24fpsの20フレームは5/6秒で、4/5秒とは一致しません。日本語の納品なら日本語ガイドの0.5秒、共通ガイドにも合わせるなら5/6秒(約0.83秒)が基準になります。後述のスクリプトは日本語ガイドに合わせて0.5秒のままにしてあります。

手順1: 訳させる範囲を本文だけに切る

SRTをそのまま貼ると、Claudeはタイムコードも「訳す対象」として読みます。まず番号と本文だけを取り出し、字幕ごとの表示秒数と字数の予算を添えて渡す形にします。

予算は、表示秒数に読む速さの上限を掛けた値と、2行分の字数のうち小さいほうです。日本語字幕なら次の式になります。

予算(全角換算) = min(13 × 2, 表示秒数 × 4)

2.0秒の字幕は8字、0.8秒の字幕は3.2字です。4字/秒という上限はかなり厳しく、台詞の完全な訳はほぼ不可能で、「何を落とすか」の判断になります。

Claude Codeで作業するなら、判断の基準をCLAUDE.mdに書いておきます。次は、そのまま使える規約の断片です。

## 字幕翻訳の規約(日本語字幕)
- *.en.srt から *.ja.srt を作る。番号とタイムコードは原文のまま複製する
- 1行は全角13字まで(半角は0.5字)。1イベントは最大2行
- 読む速さは4字/秒まで。表示秒数 × 4 を超える訳文は書かない
- 句点「。」と読点「、」は使わない。区切りが要るときはスペースを置く
- 縮めるときは削る・言い換えるで対応し、意味を足さない
- 仕上げに python3 -I check_srt.py ep01.en.srt ep01.ja.srt を実行し、出力が空になるまで直す

最後の1行が要点です。検査コマンドを規約に書いておくと、Claudeは訳した後に自分で実行して結果を読み、直しに戻れます。

手順2: タイムコードと字数を検査するスクリプト

検査は、原文と訳文の2つのSRTを突き合わせて行います。見るのは、番号とタイムコードが一致しているか、行数、1行の幅、1秒あたりの幅、表示秒数、句読点の6点です。

import re, sys, unicodedata
 
LIMITS = {"cpl": 13, "cps": 4.0, "min_s": 0.5, "max_s": 7.0, "lines": 2}
TC = re.compile(r"(\d+):(\d\d):(\d\d)[,.](\d{3}) --> (\d+):(\d\d):(\d\d)[,.](\d{3})")
 
 
def secs(h, m, s, ms):
    return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000
 
 
def width(text):  # 全角=1、半角=0.5
    return sum(0.5 if unicodedata.east_asian_width(c) in ("Na", "H") else 1.0 for c in text)
 
 
def parse(path):
    cues = []
    for block in re.split(r"\n\s*\n", open(path, encoding="utf-8").read().strip()):
        rows = block.splitlines()
        g = TC.match(rows[1]).groups()
        cues.append({"n": rows[0], "tc": rows[1], "dur": secs(*g[4:]) - secs(*g[:4]), "lines": rows[2:]})
    return cues
 
 
def main(src, dst):
    a, b, errs = parse(src), parse(dst), []
    if len(a) != len(b):
        errs.append(f"cue数 {len(a)} -> {len(b)}")
    for x, y in zip(a, b):
        n, d = y["n"], y["dur"]
        if (x["n"], x["tc"]) != (y["n"], y["tc"]):
            errs.append(f"#{n} 番号かタイムコードが原文と違う")
        if len(y["lines"]) > LIMITS["lines"]:
            errs.append(f"#{n} {len(y['lines'])}行ある")
        for l in y["lines"]:
            if width(l) > LIMITS["cpl"]:
                errs.append(f"#{n} 1行{width(l):g}字 > {LIMITS['cpl']}: {l}")
            if "、" in l or "。" in l:
                errs.append(f"#{n} 句読点が残っている: {l}")
        cps = sum(width(l) for l in y["lines"]) / d
        if cps > LIMITS["cps"]:
            errs.append(f"#{n} {cps:.1f}字/秒 > {LIMITS['cps']}")
        if not LIMITS["min_s"] <= d <= LIMITS["max_s"]:
            errs.append(f"#{n} 表示{d:.2f}秒")
    print("\n".join(errs), file=sys.stderr)
    sys.exit(2 if errs else 0)
 
 
main(sys.argv[1], sys.argv[2])

違反をstderrに出し、終了コードを2にしている理由は次の手順で説明します。

字幕2件の小さな原文で、実際に動かした結果を示します。原文は英語で、1件目が2.0秒、2件目が0.8秒です。

1
00:00:01,000 --> 00:00:03,000
I never thought
you would come back.
 
2
00:00:03,200 --> 00:00:04,000
Sit down.

直訳寄りで句読点付きの訳文は、次のようにはねられます。

python3 -I check_srt.py en.srt bad.srt
#1 句読点が残っている: まさか戻ってくるなんて、
#1 句読点が残っている: 思ってもいなかったよ。
#1 11.5字/秒 > 4.0

1行の幅は12字と11字で、13字の枠には収まっています。それでも23字を2秒で読ませるので、毎秒11.5字になり、上限の約3倍です。字数だけの検査では通ってしまう例です。

「まさか戻るとは」「思わなかった」の2行に縮めると、13字を2秒で表示するので6.5字/秒となり、まだ上限を超えます。

#1 6.5字/秒 > 4.0

内容を絞って「戻るとはね」の1行にすると、5字を2秒で2.5字/秒になります。2件目の「座って」は3字を0.8秒で3.75字/秒です。この2件は通り、出力は空で終了コードは0でした。

もう1つ、タイムコードを1か所だけ変えた訳文を渡すと、#2 番号かタイムコードが原文と違う と報告されます。Claudeが秒数を触った場合に、変更箇所がすぐ分かります。

手順3: 違反をClaudeに返して縮めさせる

検査コマンドをCLAUDE.mdに書くだけでは、実行されない回が出ます。確実にするなら、Claude Codeのhooksで、ファイルを書き換えるたびに検査を走らせます。

hooksは、Claude Codeの動作の前後で任意のシェルコマンドを実行する仕組みです。PostToolUse はツールの実行に成功した後に動きます。ここで終了コード2を返し、理由をstderrに書くと、Claudeにそのstderrが渡されます。終了コード0のhookのstderrはClaudeには見えません。検査スクリプトが違反時に2を返してstderrへ書くのは、このためです。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "if [ -f ep01.ja.srt ]; then python3 -I check_srt.py ep01.en.srt ep01.ja.srt; fi"
          }
        ]
      }
    ]
  }
}

これを .claude/settings.json に置きます。書き方の要点は2つです。

  • Edit|Write のmatcherは、字幕以外のファイルを編集したときも動きます。訳文ファイルがまだ無い間は、存在確認で空振りさせます
  • if 文は最後のコマンドの終了コードをそのまま返します。ファイルがあって違反があれば2、無ければ0です。|| true を末尾に付けると、2が0に潰れてClaudeに何も渡らなくなります

hooksを使わない運用でも構いません。claude -p のような非対話実行でファイルごとに訳させ、シェルスクリプトで検査し、違反が残る字幕だけを再依頼する組み方もできます。字幕ファイルが数十本あるなら、後者のほうが並列にしやすくなります。

縮めさせるときの指示と順番

違反を返すだけだと、Claudeは訳文を丸ごと作り直しがちです。違反した字幕の番号だけを直すよう、範囲を明示します。

check_srt.py の出力にある字幕だけを直してください。
- 番号とタイムコードは変えない
- 削る優先順位: 言い淀み・相づち、繰り返し、修飾語、固有名詞以外の補足
- 直した字幕の番号を最後に列挙する

Netflixの日本語SDHの項には、読み速度に合わせて編集するときは「削る・間引く・凝縮する」を優先し、言い換えで別の内容にしないという趣旨の記述があります。縮める順序の参考になります。

落とし穴

縮めたつもりで字幕を分割・延長してしまう

字数が足りないとき、字幕を2つに割ったり表示を長く延ばしたりすれば、確かに数値は通ります。ただし、番号とタイムコードを動かすことになり、検査の前提が崩れます。表示の延長は、前後の字幕との間隔(最低2フレーム)やカットの切れ目との関係を見る判断なので、翻訳とは別の工程として人が行うのが安全です。

検査の外に残る確認

スクリプトが見ていない項目も、納品前には確認が要ります。

  • 使える文字: Netflixでは、指定のグリフ一覧に含まれる文字だけが使えます。一覧はここでは検査していません
  • 固有名詞の統一: 話数をまたいで人名や用語がぶれないよう、表記表を作って使う規則があります。用語集を使った訳し分けは、英文プレスリリースを用語集で翻訳する手順と同じ考え方で運用できます
  • 縦書き: 日本語ガイドには縦書きの規則(1行は全角11字まで)もあります。今回の検査は横書き前提です
  • 画面上の文字との重なり: 字幕は上か下に置き、画面上の文字と重なる場合は読みやすい側にします。位置はSRTでは表せません

英語字幕を作る逆方向の場合は、スクリプトの LIMITS を {"cpl": 42, "cps": 20.0, ...} に変え、句読点の検査を外します。子ども向けなら20を17に下げます。最短秒数は5/6秒(約0.83秒)に変えると共通ガイドと揃いますが、その場合は例の2件目(0.8秒)が最短表示秒数で落ちます。

翻訳の精度を訳す前後で確かめる流れは、Claude翻訳の精度を検証してから確定するワークフローに別にまとめています。専用の翻訳サービスを使う方法もあり、用語集を効かせる場合はClaudeとDeepLの連携が参考になります。

まとめ

字幕翻訳で品質を決めるのは、訳の巧さより枠の管理です。番号とタイムコードは機械に固定させ、字数と速さは数値で判定し、違反だけをClaudeに戻すと、人が見る範囲は「落としてよい情報の選択」に絞れます。

とくに日本語字幕の4字/秒は厳しく、2秒の字幕に入る内容は約8字です。最初の訳は必ず検査に落ちる前提で、縮める工程を最初から組み込んでおくほうが、後から慌てずに済みます。

この記事を共有:XはてブLinkedIn