review-translate-batch.md

scripts/translate-batch.py のコードレビュー所見(2026-07-11)。修正は含まず、発見した問題点のみ列挙する。

1. 空の fulltext.txt

  • chunk_text("")[](空リスト)を返す。re.split(r"(?<=[.!?])\s+", "")[""] になり、下流の strip() で弾かれるため。
  • 結果として fulltext.ja.txt""(空ファイル)が書き出される。
  • 次回起動時、if out.exists() (:73) が真になり「翻訳済み」扱いでスキップされる。空の翻訳が完成扱いになり、後続処理に空ファイルが渡る可能性がある。

2. 巨大ファイル(fulltext が数 MB〜数十 MB)

  • src.read_text() (:77) で全文をメモリに読み込む。数百 MB の PDF 抽出テキストで OOM のリスク。
  • chunk_text() (:38-48) 内の re.sub / re.split がさらにコピーを生成する。メモリ使用量がファイルサイズの 2〜3 倍に達しうる。
  • done: dict[int, str] (:78) が全チャンクの原文+訳文を保持する。チャンク数 × (原文 + 訳文) のメモリを消費し、全完了までのピークが大きい。
  • ファイル全体をバッファせず、行単位・固定バイト単位のストリーム読み込みと書き出しを検討すべきだが、文境界チャンキングとの両立は難しい。

3. plamo-ctl 停止中の起動

  • translate() (:52-62) の urllib.request.urlopenConnectionRefusedError または URLError を送出する。
  • process() はこれをキャッチせず、即座に main()finally に飛ぶ。
  • finally ブロック (:125-134) で改めて /stop を叩くが、plamo-ctl が停止中ならこれも失敗し、OSErrorpass で握り潰される。
  • 結果:ユーザーには [citekey] の進捗表示なしで plamo停止(メモリ返却) だけが表示され、何が起きたか不明瞭。
  • plamo-ctl が起動中でも /stop 呼び出しに失敗した場合、Mac mini のメモリが解放されず次回起動時に GPU メモリ不足が発生する可能性がある。

4. 進捗ファイル(progress.jsonl)の破損

4a. 書き込み中断による行の途切れ

  • process() の書き込みループ (:86-96) は、チャンク翻訳完了ごとに JSONL 行を write + flush している。
  • しかし flush() は Python レベルのバッファフラッシュであり、OS / ファイルシステムレベルの書き込み完了を保証しない(クラッシュ時に中途半端なバイト列が残る)。
  • 次回起動時、破損行に対して json.loads(ln) (:81) が json.JSONDecodeError を送出し、process() 全体が失敗する。進捗がすべて失われる。

4b. ソーステキスト変更と進捗の不整合

  • 進捗ファイルが存在する場合、done dict のインデックス i が現在の chunks リストのインデックスと一致する前提で動く。
  • fulltext.txt が再生成・修正されてチャンク分割結果が変わった場合(チャンク数減少→ out.write_textrange(len(chunks))KeyError、チャンク数増加→未翻訳チャンクに古い翻訳が混入)。
  • 進捗ファイルにソーステキストのハッシュや行数チェックがない。

4c. 進捗ファイルのゴミ(空行・重複)

  • 空行が紛れ込むと json.loads("") が失敗する(4a と同様)。
  • 同一 i の行が重複しても最後の値で上書きされるためデータ破損には至らないが、不正な状態を示唆する。

5. その他の問題

5a. リトライ不在

  • translate() にネットワーク再送・指数バックオフがない。plamo-ctl 側の一時的な負荷やネットワーク瞬断でチャンク 1 つの失敗がバッチ全体を停止させる。
  • finally で plamo 停止が走るため、再開時に plamo-ctl が落ちている可能性が高い(手動再起動が必要)。

5b. --all の引数解釈が厳密すぎる

  • :114args == ["--all"] と完全一致チェックしている。--all citekey1 のように余分な引数があると、それらが citekey として解釈され process() に渡る(ディレクトリ不在でエラー)。
  • --all を含むかどうかの判定にすべき。

5c. rclone エラー時の進捗ファイル削除漏れ

  • :101 prog.unlink() は rclone 成功後にしか実行されない。
  • rclone が失敗すると進捗ファイルが残り、fulltext.ja.txt は生成済みなのに次回「翻訳待ち」扱いにならない。一方、次回起動時 out.exists() が真のため翻訳処理はスキップされるが、進捗ファイルのゴミが残る。

5d. rclone エラー出力の隠蔽

  • subprocess.run(..., capture_output=True) (:105) で stderr/stdout を取得しているが、check=TrueCalledProcessError が送出されても stderr の内容がユーザーに表示されない。

5e. チャンキングの文境界判定が粗い

  • re.split(r"(?<=[.!?])\s+", flat)Dr., U.S.A., e.g., i.e., et al., 数値の 3.14 などで誤分割する。
  • 日本語テキストが混入した場合、 などの句点を認識しない。

5f. レート制限なし

  • チャンク間の time.sleep がなく、連続リクエストで plamo-ctl 側に過負荷をかける可能性がある。

5g. 出力ファイルのアトミック性

  • out.write_text() (:98) は直接ファイルを上書きする。書き込み中にクラッシュすると fulltext.ja.txt が中途半端な内容で残り、次回「翻訳済み」と誤判定される。tempfile + rename のパターンにすべき。

5h. 文字コードの想定

  • encoding="utf-8" 固定。fulltext.txt に BOM 付き UTF-8 や latin-1 が混入した場合、UnicodeDecodeError で停止する。

深刻度サマリ

# 項目 深刻度 備考
1 空 fulltext → 空翻訳が完成扱い 後続パイプラインが空ファイルを受け取る可能性
2 巨大ファイル OOM 低〜中 数百MBのPDF抽出は稀だが可能性あり
3 plamo-ctl停止中に不明瞭な失敗 ユーザーが原因特定できない
4a 進捗ファイル破損で再開不能 長時間バッチが水泡に帰す
4b ソース変更と進捗の不整合 IndexError/KeyError で停止
5a リトライ不在 一時障害で全滅
5b --all 引数解釈の厳密さ 誤操作の余地
5c rclone失敗時のゴミ残り 手動クリーンアップで対処可
5g 出力の非アトミック書き込み クラッシュ時に不正ファイル

修正記録(2026-07-11)

以下の高深刻度3件+補強2件を scripts/translate-batch.py に反映した。

# 項目 深刻度 修正内容
1 空 fulltext → 空翻訳が完成扱い 中→修正済み chunk_text() の結果が空リストの場合、fulltext.txt が空、スキップ と表示して return
3 plamo-ctl停止中に不明瞭な失敗 高→修正済み translate()urllib.error.URLError の1回リトライ(5秒待機)を追加。process()RuntimeError を捕捉し、plamo-ctl の起動状態を確認してください と明瞭に表示して return
4a 進捗ファイル破損で再開不能 高→修正済み 進捗JSONL読み込み時、json.JSONDecodeError を捕捉して破損行をスキップし、該当チャンクを再翻訳対象に含める
4b ソース変更と進捗の不整合 中→修正済み 進捗ファイル先頭行に {"_hash": "<sha256>"} を記録。再開時に fulltext.txt のsha256と照合し、不一致なら進捗を破棄して最初から再開
5a リトライ不在 高→修正済み #3 に統合(translate() に1回の URLError リトライ)
5g 出力の非アトミック書き込み 中→修正済み fulltext.ja.txt を一時ファイル .fulltext.ja.tmp に書き出してから rename() で原子置換。例外時は tmp を削除