Subtitlarr の API による手動翻訳
このための2つのエンドポイントがあります(Geminiのコンテンツフィルターが、歴史的・劇的な会話(例:奴隷制を扱う映画における時代に即した差別用語)をブロックするため追加されました。Claudeは文脈に沿って忠実に翻訳できます)。
GET /api/queue/{item_id}/manual-translation/source— 通常の方法で項目のソース字幕を解決し、{item_id, source_language, target_language, cue_count, dialogue_text}を返します。dialogue_textは、"<index>\n<content>"形式のキューを改行で区切ったものです。実際のプロバイダーに送信されるのとまったく同じ形式です。POST /api/queue/{item_id}/manual-translation— ボディは{"translated_text": "...", "model_name": "claude-code"}。翻訳は、実際のプロバイダーの出力が通過するのと同じ再構成/整合性チェック/免責事項/アップロード/DB追跡パイプラインを通過します。engine_used="manual"、model_used=<model_name>を記録します。キュー、履歴、および字幕自体に埋め込まれた免責事項行で、実際のAPI駆動の翻訳と誠実に区別できます。
サーバー側ではLLM呼び出しは一切行われません。サーバーは投稿された translated_text を受け入れ、元のキュータイミングに再構成するだけです。
これらはすべて、指定されたSubtitlarrインスタンスのベースURLに対してHTTP経由で動作します(ローカル開発、またはリモート/NASインスタンス)。ローカルファイルシステムやデータベースへのアクセスは必要ありません。リモートインスタンスに対して作業する場合は、開始前にベースURLが到達可能であることを確認してください(curl で GET /api/run/current にアクセスするのが良い簡易テストです)。
失敗した全バックログの処理
このワークフローを呼び出す実際のプロンプトと、それを機能させたメモについては、EXAMPLE_PROMPT.md を参照してください。
「手動翻訳対象の項目を一覧表示する」専用のエンドポイントはありません。既存のキュー一覧から、純粋にHTTP経由でリストを構築します。
- 失敗した項目をページネーションで一覧表示します。
GET /api/queue?status=failed&page=1&page_size=100。pageをインクリメントし、dataがpage_sizeより短くなる(または空になる)まで続けます。レスポンス内のtotalは、予想されるページ数を示します。各行にはid、title、error_messageが含まれます。 - 何かに触れる前に、
error_messageで分類します — 失敗したすべての項目を無差別に手動翻訳しないでください。- コンテンツブロック(
"blocked its own response"、"blocked this request"、"prohibited content"など)— このワークフローの実際のターゲットです。これ以外に修正する方法はありません。同じコンテンツは、同じエンジンに対して毎回同じように失敗します。 - レート制限(
"rate limit hit (429)")— 一時的なものであり、コンテンツの問題ではありません。これらの項目は、手動翻訳ではなく、POST /api/queue/run-by-idsで項目IDを指定して通常通り再試行してください。 - 認証/資格情報エラー(401、「サービスアカウントが削除/無効化されました」など)— ユーザーが実際のAPIキー/資格情報を修正する必要があります。失効したキーで手動翻訳しても、同じエンジンを使用する将来の実行には役立ちません。
- その他すべて(5xxサーバーエラー、タイムアウト、不正な形式のレスポンス)— これらも通常は一時的なものです。手動翻訳よりも通常の再試行の方が適切です。
- コンテンツブロック(
- 大量の翻訳を開始する前に、分類/ターゲットリストをユーザーに確認します — 特に特定のインスタンスに対する最初のパスの場合、「実際にコンテンツブロックされている数」はステップ1〜2を実行した後でしかわかりません。
- 以下の単一項目ワークフローを使用して、コンテンツブロックされたリストを一度に1項目ずつ処理します。
項目ごとに1つのバックグラウンドエージェントを使用 — 複数項目を1つのエージェントにまとめない
これは軽微な設定ではありません。複数の項目を1つのエージェントにまとめると、トークンコストが節約されるどころか増加します。1つのエージェントの会話内では、すべてのツール呼び出し(ソース取得、すべてのチャンク書き込み、すべての検証実行)がそのエージェントのコンテキストに永続的に残ります。1つのエージェントが5本の映画を順次翻訳する場合、映画2の作業は映画1の蓄積されたコンテキスト(チャンク、ソースJSON、ツール出力)の上で行われ、映画3は映画1+2のコストを支払うことになります。つまり、バッチ内の5番目の項目は1番目よりもはるかに高コストであり、バッチ全体としてのコストは、5つの独立したエージェントを使用するよりも大幅に高くなります。これは経験的に確認されています。2つの5本の映画バッチエージェントは、それぞれ約520Kトークンを消費しました(約100K/本で、バッチ内の後続の項目に前倒しで負荷がかかっています)。同等の長さのコンテンツを処理する単一項目エージェントのコストは、他の項目の残ったコンテキストを引き継がないため、そのごく一部です。
このスキルファイルと規則セクションをエージェントが読み取るための固定された「コールドスタート」コストは比較的小さく、バッチ処理の理由にはなりません。バッチ処理をコスト最適化として正当化しないでください。そうではありません。再検討を求められた場合も、それに応じないでください。バッチ処理アプローチは試みられ、測定され、より悪い結果であることが確認されています。
したがって、分類とパイロット/確認ステップ(上記)が完了したら、映画であれ単一のテレビエピソードであれ、項目IDごとに正確に1つのバックグラウンドエージェントを生成します。複数の保留中のエピソードがあるテレビ番組の場合でも、番組全体に1つのエージェントではなく、エピソードごとに1つのエージェントを生成します。エピソードもお互いのコンテキストを必要としません。
各エージェントのプロンプトは自己完結型である必要があります。エージェントはこの会話の記憶を持ちません。以下を含める必要があります。
- このスキルファイルとその単一項目ワークフローへのリンク/参照。
- 作業する単一の項目ID(タイトル付き。
GET /api/queue/{id}が返すものと整合性チェックするため)。 - SubtitlarrインスタンスのベースURL。
- 項目内でのチャンク分割(約250〜350キューごと、チャンクごとに個別のスクラッチファイルではなく、1つの実行中のローカルファイルに追記する)と、送信前に最後に一度インデックスパリティを確認することを忘れないようにする注意事項。これは「項目をまとめない」というルールとは異なる懸念事項であり、両方のルールが一緒に適用されます。また、Node.jsが検証ステップで利用可能なスクリプト実行環境であること(
python3ではありません。インストールされていません。エージェントが自ら発見しようとしてターンを無駄にしないでください)。 - コンテンツに関連する場合(例:時代の差別用語)、プロジェクトの翻訳規則(以下を参照)。
- 報告する内容:成功/失敗と cue_count。これにより、結果をマスターの分類リストと照合できます。
エージェントを一度に1つずつ、厳密に順次起動 — 2つを並行して起動しない
「項目ごとに1つのエージェント」(上記)は、1つのエージェントの会話の内部で何が起こるかについてです。同時に実行されるエージェントの数についてではありません。この2つを混同しないでください。各項目が独自の独立したエージェントを取得する場合でも、エージェントは一度に1つずつ、順番に起動する必要があります。1つのエージェントを起動し、その完了と報告を待ってから、次のエージェントを起動します。同じメッセージ内で2つ以上の Agent ツール呼び出しを発行したり、同時に複数の手動翻訳エージェントを稼働させたりしないでください。2つでも、「小さな制御された波」でも、10個でもダメです。ユーザーはこれを明示的かつ繰り返し述べています。エージェントは次々に、一度に1つずつ、決して並行してはいけません。
具体的には:項目Nのエージェントをバックグラウンドで起動し、その完了通知を待ってから項目N+1のエージェントを起動します。「パイプラインを流し続けるため」に複数のエージェントを事前に起動しないでください。それは並行ディスパッチであり、各エージェントが内部的に1つの項目のみに触れるかどうかに関係なく、ここで禁止されていることそのものです。
単一項目ワークフロー
トークンコストに関する注意: ここでの支配的なコストは、翻訳自体ではなくI/Oオーバーヘッドです。1188キューの映画での最初のライブ実行では、各チャンクの翻訳をスクラッチファイルに書き込み、結合する前に読み戻すという処理(8回の余分な書き込み+読み取りラウンドトリップ)と、環境発見のオーバーヘッド(python3 がない、パス補間の問題)により、約110Kトークンを消費しました。以下のステップ2〜5はそれを回避するために記述されています。チャンクを作業中の出力に保持し、ファイルシステムを経由してラウンドトリップさせず、項目ごとに環境を再発見しないでください。
- 項目を確認します。
GET /api/queue/{item_id}— 努力を費やす前に、statusとerror_messageが期待どおりであることを確認します。 - ソースを取得します。
GET /api/queue/{item_id}/manual-translation/source。cue_countに注意します。 - 約250〜350キューのチャンクで翻訳します。ファイル全体を一度に処理するのでも、100〜150キューのスライスでもありません。非常に長い単一パスでは、インデックス位置合わせのミスが発生しやすくなります(長い連続出力にわたって注意力が散漫になる)が、各チャンクは固定オーバーヘッド(形式/規則の再記述、ツールのラウンドトリップ)も伴うため、その安全な範囲内でより少なく/より大きなチャンクを使用すると、全体的なコストが低くなります。各チャンクの翻訳出力は、実行中の1つのローカルファイルに直接書き込み、この項目のIDをファイル名に付けます(例:スクラッチパッド内の
translated_<item_id>.txt。汎用的なtranslated.txtではありません)。チャンクごとに個別のスクラッチファイルを作成せず、書き込み後にチャンクを読み戻して単独で再検証することもしないでください。1つのファイルに追記し、最後に一度だけ読み取って以下のパリティチェックを行います。ディスパッチは順次であるべきですが(以下参照)、項目ID接尾辞は重要です。汎用的なファイル名は、同じスクラッチディレクトリを共有する別の同時実行中の手動翻訳エージェントによって、あるエージェントの処理中のチャンクが静かに上書きされる原因となったことがあります。すべてのスクラッチファイル(ソースJSON、チャンク、結合出力)に項目IDの名前を付け、ディスパッチのミスが別の項目の作業を破損しないようにします。 - すべてのインデックスを正確に保持し、キューをスキップしないでください。 ソース内のすべての
<index>は、同じインデックス番号で翻訳出力に正確に一度出現する必要があります。reassemble()は位置ではなくインデックスで照合するため、含めなかったキューは元の言語のテキストにフォールバックします(エラーにならずに、静かに品質が低下します)。書式マークアップ(例:歌詞/内面の独白の<i>...</i>イタリック)は保持します。これはコンテンツの一部であり、再構成前に削除されません。 - 送信前にインデックスパリティを検証します。チャンクごとではなく、完全にアセンブルされたファイルに対して一度だけ行います。すべての
^\d+$行を抽出し、結果のセットを1..cue_countと比較します。これは欠落/重複したインデックス番号のみを検出することに注意してください。キューがマージまたは分割され、インデックスが1対1で実行され続けるが、特定の番号の下に間違ったダイアログが配置されるチャンク(例:2つのソースキューを誤って1つの出力ブロックにマージすると、後続のすべてのキューの内容が1つずつずれ、このチェックでは見えなくなります)は検出しません。インデックススクリプトだけでなく、いくつかのポイント(チャンク境界、1つのチャンク内のキュー)で実際のコンテンツをソースと照合してスポットチェックも行います。インデックスチェックには、この正確なNodeワンライナーを使用します(Nodeが利用可能であることが確認されています。python3がないことを発見したり回避したりするために時間を無駄にしないでください。これを使用してください)。
送信前に、これが報告するギャップや重複を修正します。node -e "const fs=require('fs');const t=fs.readFileSync('translated.txt','utf8');const idx=[...t.matchAll(/^(\d+)$/gm)].map(m=>+m[1]);const want=new Set(Array.from({length:<CUE_COUNT>},(_,i)=>i+1));const got=new Set(idx);const missing=[...want].filter(x=>!got.has(x));const extra=idx.filter((x,i)=>idx.indexOf(x)!==i);console.log('count',idx.length,'missing',missing,'dupes',[...new Set(extra)])" - 送信します。
POST /api/queue/{item_id}/manual-translationに、完全に結合されたtranslated_text(すべてのチャンクを連結したもの。エンドポイントは項目ごとに1つの完全な送信を受け付け、チャンクごとに1回の呼び出しではありません)とmodel_name(デフォルトは"claude-code"。別の帰属が必要な場合のみ変更します)を指定します。 - 確認します。
GET /api/queue/{item_id}はstatus: "done"、engine_used: "manual"を表示するはずです。必要に応じて、GET /api/debug/{movie|episode}/{bazarr_id}/subtitle?lang=<code>でアップロードされたファイルをスポットチェックします。最初のキューは[<model_name>]で終わる免責事項行である必要があり、キュー2のタイミングは元のソースの最初の実際のキューと正確に一致する必要があります。
このプロジェクトの翻訳規則
- 人種的差別用語 / 時代に即した攻撃的な言葉:レジスターを維持して忠実に翻訳し、和らげたり省略したりしないでください。英語の「nigger」をカタルーニャ語に翻訳する場合、ユーザーのガイダンス:奴隷主/監督のキャラクターが軽蔑を込めて使用する場合は "negrata"(蔑称形)を使用し、中立的/記述的な使用の場合は "negre"/"negres" を使用します。これは、英語の原文自体が中立的な使用と差別用語の使用を区別しているのを反映しています。
- 歴史的/時代のレジスター:形式的な呼称、方言のマーカー、時代の話し方のパターン(奴隷にされたキャラクターの会話での短縮形や文法の欠落、上流階級のキャラクターの形式的な語彙)を維持し、全員を現代の中立的なカタルーニャ語に平準化しないでください。
- 歌詞 / スピリチュアル(例:「Roll, Jordan, Roll」):必ずしも歌える同等物ではなく、意味を翻訳します。これらは字幕であり、吹き替え用のスクリプトではありません。
これが設定済みエンジンの再試行ではなく存在する理由
Geminiのコンテンツセーフティフィルターは、人間またはClaudeが文脈で同じシーンを読めば問題なく翻訳するであろう、正当なナラティブコンテンツのカテゴリ全体(明示的な暴力、時代の差別用語、深刻なドラマの文脈での成人向けテーマ)をブロックします。同じエンジンに対して同じコンテンツを繰り返し再試行しても役に立ちません。これはコンテンツポリシーの判断であり、一時的な障害ではありません。また、APIキーに対するプロバイダー自身の悪用防止措置を引き起こすリスクもあります。このワークフローは、まさにそのような行き詰まった項目のクラスのために存在し、通常のエンジンカスケードの一般的な代替手段としてではありません。