海外の反応を創作させない — 3段の関門で実在コメントだけを残す
「海外の反応」を紹介するサイトを自動更新するとき、いちばん起こしやすい事故は反応そのものを生成してしまうことである。件数のノルマがあると、実在コメントが取れなかった日に「傾向をもとに書き起こす」という抜け道が生まれる。抜け道は1回使うと常用される。
アニメ海外の反応では、この事故を実際に起こしたあとで設計を変えた。方針は「書き起こしを禁止する」ではなく、書き起こしが構造的に入れない経路を作ることである。
この記事では、その3段の関門を実装から説明する。確認した環境は Windows 11 / Node.js v24.11.1 で、数字はすべて 2026-09-19 に実際に回した1話ぶんの実測値である。
なぜ「禁止」では足りないか
最初の方針は「反応を作成してよい」だった。ところがサイトの免責ページと著作権ページには「視聴者コメントを要約せず個別に翻訳して紹介する」「出典を明記する」「各コメントの著作権は原投稿者に帰属」と書いてあった。書いてあることと実態が食い違っていた。
このとき選べる直し方は2つある。記述を実態に合わせる(=「創作です」と書き直す)か、実態を記述に合わせる(=本当に翻訳する)かである。後者を選ぶと、次は「翻訳できなかった日はどうするか」が問題になる。ここで件数ノルマを残したまま「頑張って翻訳する」と決めると、取れない日に元の事故が再発する。
そこで件数の下限を撤廃した。取れた実在コメントの数がそのまま掲載数になる。3件しか取れなければ3件でよい。0件なら0件でよい。これが前提で、そのうえで3段の関門を置く。
全体の形
処理は3つのコマンドに分かれていて、担当が違う。
| 段 | コマンド | 担当 | 入れないもの |
|---|---|---|---|
| 1 | anime:mal-reactions |
取得(決定論スクリプト) | 短すぎ・長すぎ・引用の断片・permalink でない URL・必須欄の欠け・重複 |
| 2 | (LLM) | 翻訳だけ | — |
| 3 | anime:mal-apply |
反映(決定論スクリプト) | 未翻訳・架空の投稿者名・原文の書き換え・重複 |
要点は、LLM が触れるのは 2 段目の text(訳文)だけだということである。原文も出典URLも投稿者名も、1段目のスクリプトが取ってきた値がそのまま3段目まで運ばれる。LLM が原文や出典を書き足す経路が無いので、「それらしい原文」を作ることが構造的にできない。
1段目:取得側が捨てるもの
反応1件は、次の6つを全部持っていなければ載せない。1つでも欠けたらその反応は落とす。
1 | |
落とす判定は純粋関数になっていて、理由を文字列で返す。
1 | |
MAX_TEXT は当初 1200 だった。ところが実測すると、上限で落ちていた10件が各話でいちばん中身のある感想だった(1336〜2630字)。長い投稿を「長いから」という理由で落とすと、残るのは「面白かった」だけになる。ここを 4000 に広げたのが、質に対していちばん効いた変更である。
replyFragment は引用ブロックの断片である。掲示板の投稿は他人のコメントを引用して返信する形が多く、引用部分だけを拾うと他人の文を別人の名前で載せることになる。これは「実在コメント」の体裁を保ったまま中身が嘘になる、いちばん厄介な壊れ方なので、独立した判定になっている。
badPermalink は、一覧ページの URL を出典として持ってきた場合に落とす。一覧は時間が経つと中身が変わるので、出典として検証できない。
実測(2026-09-19・1話ぶん)
1 | |
出力に dropped が付く。この回の実測値は次のとおりだった。
| 区分 | 件数 |
|---|---|
| 採用 | 12 |
| tooShort(40字未満) | 4 |
| replyFragment(引用の断片) | 5 |
| overPerEpisode(1話あたりの上限超過) | 34 |
| tooLong / badPermalink / missingField / duplicate | 0 |
スレッドから読んだ55件のうち、9件は品質判定で落ち、34件は上限で溢れ、12件が残った。落とした理由が区分で残るので、「取れなかった」と「捨てた」を後から区別できる。overPerEpisode が続けて出るなら上限の設定が実態に合っていないということなので、ログに残して見直す材料にする。
2段目:LLM は訳文だけを埋める
取得の出力は、各反応が {handle, lang, text:"", original, sourceUrl, fetchedAt, postedAt} の形になっている。text が空文字で返るのがポイントで、未翻訳のうちはサイトに出ない。
訳文を埋めるときの対応付けは、配列の並び順ではなく sourceUrl の投稿IDで行うべきである。実際にここで1度はまった。MyAnimeList の permalink は次の形をしている。
1 | |
投稿IDを取るつもりで /id=(\d+)/ と書くと、topicid= の中の id= に先にマッチしてスレッドIDが返る。全12件が同じ値になり、1件も対応付かなかった。正しくはこうなる。
1 | |
区切りまで含めて書けば topicid は &topicid= なので [?&]id= には一致しない。並び順に依存しない対応付けにしておくと、取得をやり直しても同じ訳文が同じ投稿に当たる。
翻訳そのものの決まりは2つだけである。要約しない(サイトが「要約せず個別に翻訳」と明言しているため)ことと、原文の意味を変えないことである。長いから縮める、はしない。
3段目:反映側が弾くもの
anime:mal-apply は --write を付けるまで書き込まない。書き込む前に、次を弾く。
1 | |
海外視聴者A は、事故当時に生成されていたハンドル名の形そのものである。壊れ方をそのまま検査にしたので、同じ形が再び入ろうとすると必ず止まる。原文が取得時と変わっていないかも突き合わせるため、訳文以外を手で書き足しても通らない。
先に空実行して確かめてから書く。
1 | |
空実行の出力はこうなる。
1 | |
sourced: true が「この話は出典つきで埋まった」という意味である。
掲載側の切り分け:訳は全文、逐語引用は必要な範囲
長い投稿を落とさない方針にすると、今度は逐語引用が長くなりすぎるという別の問題が出る。ここはデータ側ではなく掲載側で切り分けた。
text(訳文)は投稿の全文を訳す。情報量はここで確保するoriginal(原文)はそのまま格納する。データ側で切り詰めない- 表示のとき、原文が 700 字を超えたら抜粋表示にし、抜粋である旨と出典リンクを画面に出す
データを切り詰めてしまうと、あとで出典と突き合わせられなくなる。切り詰めるのは表示の都合なので、表示のところでやる。この分担にしておくと、あとから引用の方針を変えてもデータを取り直さなくて済む。
まとめ:関門は「壊れ方」から作る
この3段は、きれいな設計から降りてきたものではない。実際に起きた壊れ方をひとつずつ関門にした結果である。
- 件数ノルマ → 撤廃(下限が無ければ埋める動機が生まれない)
- 生成されたハンドル名 → その形の正規表現で弾く
- 引用の断片を別人の名前で載せた →
replyFragmentとして独立に判定 - 一覧URLを出典にした →
badPermalinkで弾く - 長い投稿が落ちていた → 上限を実測に合わせて広げ、切り詰めは表示側へ
自動更新の仕組みを作るときは、「やってはいけないこと」をドキュメントに書くより、やろうとしても通らない経路を1つ作るほうが効く。そのとき、どこで何件落ちたかを必ず出力に残しておくこと。落ちた理由が見えないと、関門が厳しすぎるのか実態が変わったのかを区別できない。
実際の掲載結果はアニメ海外の反応で見られる。各反応には原文の折りたたみ表示と出典リンクが付いていて、この記事で説明した6つの欄が全部揃っているものだけが並んでいる。