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.urlopenがConnectionRefusedErrorまたはURLErrorを送出する。process()はこれをキャッチせず、即座にmain()のfinallyに飛ぶ。finallyブロック (:125-134) で改めて/stopを叩くが、plamo-ctl が停止中ならこれも失敗し、OSError→passで握り潰される。- 結果:ユーザーには
[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. ソーステキスト変更と進捗の不整合
- 進捗ファイルが存在する場合、
donedict のインデックスiが現在のchunksリストのインデックスと一致する前提で動く。 fulltext.txtが再生成・修正されてチャンク分割結果が変わった場合(チャンク数減少→out.write_textのrange(len(chunks))でKeyError、チャンク数増加→未翻訳チャンクに古い翻訳が混入)。- 進捗ファイルにソーステキストのハッシュや行数チェックがない。
4c. 進捗ファイルのゴミ(空行・重複)
- 空行が紛れ込むと
json.loads("")が失敗する(4a と同様)。 - 同一
iの行が重複しても最後の値で上書きされるためデータ破損には至らないが、不正な状態を示唆する。
5. その他の問題
5a. リトライ不在
translate()にネットワーク再送・指数バックオフがない。plamo-ctl 側の一時的な負荷やネットワーク瞬断でチャンク 1 つの失敗がバッチ全体を停止させる。finallyで plamo 停止が走るため、再開時に plamo-ctl が落ちている可能性が高い(手動再起動が必要)。
5b. --all の引数解釈が厳密すぎる
:114でargs == ["--all"]と完全一致チェックしている。--all citekey1のように余分な引数があると、それらが citekey として解釈されprocess()に渡る(ディレクトリ不在でエラー)。--allを含むかどうかの判定にすべき。
5c. rclone エラー時の進捗ファイル削除漏れ
:101prog.unlink()は rclone 成功後にしか実行されない。- rclone が失敗すると進捗ファイルが残り、
fulltext.ja.txtは生成済みなのに次回「翻訳待ち」扱いにならない。一方、次回起動時out.exists()が真のため翻訳処理はスキップされるが、進捗ファイルのゴミが残る。
5d. rclone エラー出力の隠蔽
subprocess.run(..., capture_output=True)(:105) で stderr/stdout を取得しているが、check=TrueでCalledProcessErrorが送出されても 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 を削除 |