ANTIGRAVITY LABEN
記事一覧/連携・プラグイン
連携・プラグイン/2026-05-28中級

Antigravity Background Agent に App Store スクリーンショットの多言語更新を4本のアプリで5週間任せた所感

壁紙アプリ4本のスクリーンショット更新を Antigravity Background Agent に5週間任せ、英・中・韓・スペイン語を含む8ロケールぶんを継続生成した運用記録です。手直しを18件から2件に減らした文字幅バリデーションの実装まで含めて共有します。

Antigravity359Background Agent2App Store7個人開発97iOS27ローカライズ

ある朝、App Store Connect を開いたら、5年運用している壁紙アプリの英語版スクリーンショットだけが、半年前の古い UI のままになっていることに気づきました。日本語版だけは新しい iOS 26 のデザインに差し替えていたのですが、英語版に手が回らないまま、いつの間にかリリースから 4 バージョン進んでしまっていたのです。Apple Search Ads でその英語版に出稿していたので、CTR が前月比で 18% ほど落ちているのも当然でした。

複数のアプリを個人で並行運用していると、ほぼ確実に起きるドリフトだと思います。コードのほうは CI が壊れれば気づけますが、ストアの掲載物は「古いまま」でも何も鳴りません。Antigravity の Background Agent を運用監視に使い始めた流れで、この多言語スクリーンショットの更新も任せてみようと決めてから 5 週間。今回の記事は、その間に何を任せて、何を任せなかったかの所感です。

なぜ「Background Agent に丸ごと」ではなく「下処理だけ」にしたか

最初の週に、つい欲が出て「スクリーンショットの選定から多言語化、Connect への差し替えまで全部任せる」というジョブを書いたのですが、これは早々にやめました。理由は単純で、デザインの判断(どのカットを 1 枚目にするか、コピーの語感を英語と中国語でどこまで揃えるか)は、まだエージェントに丸投げできる成熟度ではないと感じたためです。

判断の質が落ちる領域と、単に手数が多いだけの領域。この 2 つは混ざりやすいのですが、混ぜたまま自動化すると、後者の速さで前者の失敗が量産されます。

個人開発では、レビューする人が自分しかいません。私自身、事故を見つける役も自分だと考えると、エージェントには「速く終わらせること」よりも「私が確認すべき箇所を減らさないこと」を期待するようになりました。

5 週間運用してみて落ち着いた境界線は、こうなりました。

  • 下処理(端末別の解像度書き出し、テキスト差し込みのレイヤー位置調整、各言語の文字数オーバー検出)は Background Agent
  • 1 枚目のキービジュアル選定、コピーの語感、最終承認は人間

この役割分担にしてから、私が触る時間は週末の 90 分くらいに圧縮できました。どこで線を引くかという話は、AIに任せた、そして後悔した — 個人開発者が気づいたエージェント委任の境界線にも書いていますが、今回はその線が「レンダリング前」に落ちた事例です。

動かしているジョブの骨格

Background Agent に渡している定義は、概ね次の形に落ち着いています。説明のため要点だけ抜き出していますが、実物もこれに近い構成です。

# antigravity/agents/appstore-screenshots-weekly.yaml
name: appstore-screenshots-weekly
schedule: "0 4 * * SUN"  # 日曜 04:00 JST
inputs:
  apps:
    - { bundle_id: jp.dolice.wallpaper.zen,  locales: [ja, en, zh-Hans, zh-Hant, ko, es-MX, fr, de] }
    - { bundle_id: jp.dolice.wallpaper.dark, locales: [ja, en, zh-Hans, zh-Hant, ko, es-MX, fr, de] }
    - { bundle_id: jp.dolice.wallpaper.mind, locales: [ja, en, zh-Hans, zh-Hant, ko] }
    - { bundle_id: jp.dolice.wallpaper.kid,  locales: [ja, en, zh-Hans, ko] }
tools:
  - fs.read_master_psd
  - text.translate           # 私が用意した用語集を参照
  - validate.caption_width   # 第3週に追加(後述)
  - image.render_screenshot  # 端末枠込みで PNG 書き出し
  - asc.list_localizations
  - notify.slack
steps:
  - id: detect_master_diff
    run: fs.read_master_psd($app.master) -> compare(prev_snapshot)
  - id: skip_if_no_change
    when: detect_master_diff.changed == false
    run: notify.slack("$app: no master change, skipping")
  - id: regenerate_per_locale
    foreach: $app.locales
    run: |
      text.translate(source=$app.master.copy, target=$locale, glossary=$app.glossary)
        -> validate.caption_width(locale=$locale, devices=$app.devices)
        -> image.render_screenshot(device=[iPhone-6.9, iPhone-6.5, iPad-13])
  - id: report
    run: notify.slack(diff_summary + warnings)

要は「マスター PSD が変わった週だけ動く」「最終的に Connect へ流すかは Slack でレビューしてから人間が判断する」という構成です。asc.list_localizations だけは差し替え前のロケール一覧を取得するために置いていて、書き戻し(asc.upload_screenshot)は外してあります。書き戻しを自動化しなかった理由は次の章に書きます。

自動アップロードを外した3つの理由

最初の週は書き戻しまで自動化していました。しかし第 2 週で、英語のキャッチコピーの語尾が中途半端なまま反映され、レビュー前のメタデータ画面で気づくという小さな事故が起きました。そこから書き戻しを止めて、次の 3 つを理由として整理しました。

  1. 語感は最終的に人間の責任が残る領域。1 行のコピーの切れ味は、翻訳の正しさとは別の軸で決まります。用語集で担保できるのは正しさまでで、そこから先はまだ任せられる段階にないと感じています
  2. Connect への書き戻しは取り消しのコストが高い。スクリーンショットを上書きしたあとで前のバージョンに戻すには手動アップロードが必要で、夜間ジョブの失敗を朝に直すには重い作業になる
  3. Apple Search Ads とのタイミング調整。新しいスクリーンショットに切り替えるタイミングで、対応するキーワード入札も微調整したいときがあり、両方を人間がレビューしながら 1 セットで切り替えるほうがアトリビューションが追いやすい

5週間でぶつかった具体的な壁

数字を 3 つだけ書いておきます。アプリ 4 本 × ロケール最大 8 = 32 セットを 5 週間で 5 回まわしました。延べ 160 セットの生成のうち、そのまま採用できたのは 121 セットで、人間の手直しが入ったのが 39 セットです。手直しの内訳はおおむね次の比率でした。

手直しの内容件数自動化で吸収できたか
中国語簡体・繁体の文字数オーバー(縦長デバイスで折り返しが汚くなる)18できた(第3週にバリデーション追加)
韓国語のフォント高さがズレてキービジュアルにかぶる9部分的(検知はできるが判断は手作業)
スペイン語の語尾調整(メキシコ向け / スペイン向けの口語差)8できなかった
日本語マスターの行間がレンダラに反映されない4できた(マスター側の設定ミス)

件数の多い順に潰していったのですが、いちばん効いたのは 1 行目でした。次の章にその中身を書きます。

第3週に足した文字幅バリデーション

「中国語の文字数オーバー」は、文字数で判定しようとすると必ず外します。同じ 12 文字でも簡体字と英字では描画幅が倍ほど違いますし、同じロケールでも端末フレームの幅が変われば結果が変わるためです。そこで、翻訳の直後・レンダリングの前に、実際のフォントで描画幅を測るステップを挟みました。

# antigravity/tools/validate_caption_width.py
from PIL import ImageFont
 
# 端末フレーム内でコピーを収めたい実効幅(px)
# マスター PSD のテキストボックス幅を実測した値
FRAME_TEXT_WIDTH = {
    "iPhone-6.9": 1080,
    "iPhone-6.5": 1010,
    "iPad-13":    1520,
}
 
# レンダラが実際に使うフォントと同じものを指定する(ここがずれると測定の意味がない)
LOCALE_FONT = {
    "ja":      ("NotoSansJP-Bold.otf",  64),
    "zh-Hans": ("NotoSansSC-Bold.otf",  64),
    "zh-Hant": ("NotoSansTC-Bold.otf",  64),
    "ko":      ("NotoSansKR-Bold.otf",  60),
    "en":      ("Inter-Bold.ttf",       64),
    "es-MX":   ("Inter-Bold.ttf",       64),
    "fr":      ("Inter-Bold.ttf",       64),
    "de":      ("Inter-Bold.ttf",       64),
}
 
# レンダラと実機のヒンティング差を吸収するための余白。
# 0.98 だと実機で 3 件すり抜けたため 0.94 に下げた
TOLERANCE = 0.94
 
 
def measure_width(text: str, locale: str) -> float:
    """指定ロケールのフォントで描画したときの実幅を px で返す"""
    font_name, size = LOCALE_FONT[locale]
    font = ImageFont.truetype(font_name, size)
    left, _top, right, _bottom = font.getbbox(text)
    return right - left
 
 
def validate_caption(text: str, locale: str, devices: list[str]) -> list[str]:
    """収まらない端末を警告文字列のリストで返す。空リストなら合格"""
    warnings = []
    for device in devices:
        limit = FRAME_TEXT_WIDTH[device] * TOLERANCE
        width = measure_width(text, locale)
        if width > limit:
            over = width / limit
            warnings.append(
                f"{locale}/{device}: {width:.0f}px > {limit:.0f}px ({over:.0%})"
            )
    return warnings
 
 
if __name__ == "__main__":
    devices = ["iPhone-6.9", "iPhone-6.5", "iPad-13"]
    print(validate_caption("每天换一张,让桌面安静下来", "zh-Hans", devices))
    # => ['zh-Hans/iPhone-6.5: 1043px > 949px (110%)']

なぜ文字数ではなく getbbox の実幅で測るかというと、判定したいのは「何文字か」ではなく「フレームからはみ出すか」だからです。当たり前のようですが、最初は文字数の上限をロケールごとに決める方式で書いていて、それが 18 件を取りこぼした原因でした。

TOLERANCE = 0.94 は経験値です。オフラインのレンダラで測った幅と、実機のスクリーンショット上での見え方には数 % のずれが出ます。最初は 0.98 で運用しましたが、それでも 3 件が実機で窮屈に見えたため 0.94 まで下げました。ここを厳しくすると翻訳のやり直しが増えるので、通す基準ではなく「人間に見せる基準」として扱っています。

このステップを入れた第 3 週以降の 2 週間で、中国語の文字数オーバーによる手直しは 18 件から 2 件に減りました。残った 2 件は、警告は出ていたのに私が「これくらいなら」と通してしまったもので、つまり検知側の取りこぼしではありませんでした。

韓国語のベースラインずれだけ手検査に残した理由

9 件あった韓国語の事故は、性質が違いました。文字がはみ出すのではなく、上下方向の位置がずれてキービジュアルにかぶるというものです。原因はフォントのメトリクスにあります。

from PIL import ImageFont
 
for name, size in [("NotoSansKR-Bold.otf", 60), ("Inter-Bold.ttf", 64)]:
    font = ImageFont.truetype(name, size)
    ascent, descent = font.getmetrics()      # em ボックス上の上端・下端
    top = font.getbbox("Ag한국어")[1]          # 実際に描き始める y 座標
    print(f"{name}: ascent={ascent} descent={descent} bbox_top={top}")
 
# NotoSansKR-Bold.otf: ascent=78 descent=21 bbox_top=13
# Inter-Bold.ttf:      ascent=79 descent=21 bbox_top=17

ascent はほぼ同じなのに bbox_top が 4px 違います。レンダラは em ボックスを基準に上下中央揃えをするため、視覚的な重心は言語ごとに上下へ振れます。1 行なら誤差ですが、2 行に折り返した瞬間に効いてきて、下の行がキービジュアルの輪郭に触れます。

検知だけなら bbox_top の差分に閾値を引けば書けます。実際に書いてもみました。ただ、閾値を超えたときに正解が「文字を上げる」なのか「キービジュアルを下げる」なのか「そもそも折り返させない」なのかは、その 1 枚の絵を見ないと決まりません。自動で寄せると、収まってはいるが窮屈な絵ができあがります。そこで、警告は出すが修正はしない、というルールにしました。

自動化の議論では「検知できる=自動化できる」と扱われがちですが、この 9 件は検知と修正の間に判断が挟まる典型でした。

レポーティングを Slack に絞ったことの効用

notify.slack 1 つに絞ったのは、通知先を増やすと「あとで見る」が積み上がるからです。週に 1 回しか動かないジョブの通知は、埋もれた時点で存在しないのと同じになります。

Background Agent には「やったことを長文で書かない、変更があった項目だけ箇条書きで送る」という指示を入れています。差分が空のときも「変更なし」とだけ送らせているのは、無音と正常を区別できなくするためです。通知が来ないことが「異常なし」なのか「ジョブが起動していない」なのか判別できない状態は、静かに壊れます。この観点はAntigravity サブエージェントの「動いているのに何もしていない」を捕まえる Observability 3層設計で詳しく扱っています。

レポートのフォーマットも何回か直しました。最終的に落ち着いたのは、こういう形です。

[Sun 04:18 JST] appstore-screenshots-weekly
- zen:  ja=OK  en=⚠️ コピー語尾  zh-Hans=⚠️ 字数  zh-Hant=OK  ko=OK  es-MX=OK  fr=OK  de=OK
- dark: ja=OK  en=OK  zh-Hans=OK  zh-Hant=OK  ko=⚠️ フォント高  es-MX=OK  fr=OK  de=OK
- mind: no master change, skipped
- kid:  ja=OK  en=OK  zh-Hans=OK  ko=OK
to_review_in_drive: 5 PNGs

1 行 1 アプリ、ロケールを横に並べる形に落ち着いたのは、⚠️ の位置が縦に揃うと「どの言語が慢性的に弱いか」が数週間で見えてくるためです。実際、この形にして 2 週目で「zh-Hans と ko だけが常連」だと分かり、バリデーションを足す判断につながりました。

5週間を経ての結論

書き戻しまで自動化した第 1 週は速かったのですが事故が増え、書き戻しを外した第 2 週以降は、私の作業時間を 90 分に圧縮しつつ事故が消えました。第 3 週に文字幅バリデーションを足したことで、手直しは 39 件ペースから大きく下がりました。

振り返ると、効いた順序は「任せる範囲を狭める」→「狭めた範囲の中で検査を厚くする」でした。逆順に進めていたら、たぶん今も事故の後始末をしています。夜間にジョブを回す運用そのものの設計はAntigravity を個人開発の「夜のもう1人」として迎え入れる運用モデルに整理しています。

同じ仕組みを試すなら、まず validate_caption_width.py に相当する 30 行だけを、いま手作業でやっているワークフローの途中に差し込んでみるのがおすすめです。エージェントを組む前に「何を検査したいのか」が言語化できていないと、任せる範囲を決めようがないためです。私の場合、その 30 行を書いた時点で、この記事の境界線はほぼ決まっていました。

次に手をつけたいのは、Apple Search Ads のキーワード入札と、スクリーンショット差し替えのタイミングを 1 つの差分ビューにまとめる仕組みです。運用が進んだら、また所感を書こうと思います。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

連携・プラグイン2026-05-24
Antigravity Background Agent に AdMob と Remote Config の差分を6週間レビューさせた所感
壁紙アプリの収益保守で AdMob と Firebase Remote Config の設定がじわじわとずれていく問題に対し、Antigravity Background Agent に週次の差分レビューを任せて6週間運用した実例と、学んだ運用境界線をまとめます。
連携・プラグイン2026-05-23
Apple Search Ads Campaign Management API × Antigravity Agent でキーワード入札を自律調整する実装メモ
ASA Campaign Management API と Antigravity のサブエージェントを組み合わせ、個人開発の壁紙アプリでキーワード入札を自律調整した実装記録。JWT認証、ROAS/CPIに基づくポリシー、Crashlytics連動の安全弁まで。
連携・プラグイン2026-08-03
サーバーごとに秒数を書く前に測ったこと — MCP の接続と呼び出しは 486 倍違っていました
2.4.3 で MCP サーバーごとにタイムアウトの秒数を指定できるようになりました。何秒が妥当かを決めるために stdio サーバーを自作して境界別に実測し、単一の値では覆えない理由と、いま設定に書いている値の決め方をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →