2026年5月、iOS 向けの4アプリ(Beautiful HD Wallpapers、浮世絵壁紙、Relaxing Healing、Law of Attraction Everyday)の大型アップデートを同時進行させていました。新しい iPhone モデルへの解像度対応、AdMob メディエーション拡張、StoreKit 2 への移行と、やることが山積みだったのですが、その中でひとつ想定外に時間を取られたのが「多言語 App Store 説明文の再生成」です。
2014年から個人アプリ開発を続けて12年、累計 5,000 万 DL 超の壁紙・癒し系アプリを運営していると、各ストアのメタデータを12言語で管理する必要があります。モデル交代やコンセプト変更のたびに全言語版を更新するのですが、Gemini API を使った順次処理のスクリプトが遅くて困っていました。asyncio で並列化したところ、処理時間が約12分の1になりました。今回はその実装と、実際にハマった落とし穴をまとめます。
順次処理の限界:12言語 × 4アプリで13分かかっていた
当初のスクリプトは単純な for ループで各言語の説明文を生成していました。12言語の App Store 説明文を順番にリクエストするだけのシンプルな実装です。
import google.generativeai as genai
import time
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel("gemini-2.5-flash")
LANGUAGES = [
"Japanese", "English", "Chinese (Simplified)", "Chinese (Traditional)",
"Korean", "French", "German", "Spanish", "Portuguese", "Italian",
"Arabic", "Russian"
]
def generate_description(app_name: str, lang: str) -> str:
prompt = f"""
Write an App Store description for "{app_name}" in {lang}.
Keep it under 4000 characters. Focus on beauty, relaxation, and daily use.
"""
response = model.generate_content(prompt)
return response.text
# 順次処理(遅い)
results = {}
start = time.time()
for lang in LANGUAGES:
results[lang] = generate_description("Beautiful HD Wallpapers", lang)
print(f"✓ {lang} 完了")
elapsed = time.time() - start
print(f"\n合計: {elapsed:.1f}秒") # 実測: 約780秒(13分)12言語で約780秒(13分)かかっていました。4アプリ分だと1時間近くになります。アップデートのたびにこれを待つのは現実的ではなく、「面倒だから後で」という先送りの温床になっていました。
asyncio と threading の違い:なぜ asyncio を選んだか
並列化の方法として threading も選択肢に入ります。ただ、Gemini API の呼び出しは CPU ではなくネットワーク I/O の待ち時間が支配的です。こういったケースでは threading よりも asyncio の方が軽量で扱いやすいと感じています。
threading はスレッドごとにメモリを消費し、GIL(グローバルインタープリタロック)の影響もあります。一方 asyncio はシングルスレッドで動作し、I/O 待ちの間に別のタスクを進める設計です。Gemini API のようなネットワーク往復が多い処理には、asyncio の方がシンプルに書けて管理もしやすいと感じています。
具体的に言うと、threading で12スレッドを立てた場合、それぞれのスレッドが独立したメモリ空間を持ち、スレッド間の同期(Lock や Queue)を手動で管理する必要があります。asyncio では Semaphore ひとつで同時実行数を制御でき、エラー処理も try/except の通常のパターンで書けます。個人開発でメンテナンスコストを低く抑えたい場合には、asyncio の方が合っていると感じています。
もう一点、Gemini API の Python SDK (google-generativeai) は generate_content_async() という非同期メソッドを公式にサポートしているため、既存の同期コードからの移行が比較的スムーズです。generate_content() を generate_content_async() に置き換え、呼び出し元を async def 関数にするだけで基本的な構造は変わりません。
multiprocessing については、このケースでは不要です。CPU バウンドの処理(画像処理や数値計算など)には multiprocessing が有効ですが、API 呼び出しは I/O バウンドなので asyncio で十分です。
asyncio.gather() で並列化する
asyncio と Semaphore を使って、複数のリクエストを同時に送る実装です。Semaphore で同時リクエスト数を制限することで、レート制限への対策も同時に行います。
import asyncio
import google.generativeai as genai
import time
genai.configure(api_key="YOUR_GEMINI_API_KEY")
LANGUAGES = [
"Japanese", "English", "Chinese (Simplified)", "Chinese (Traditional)",
"Korean", "French", "German", "Spanish", "Portuguese", "Italian",
"Arabic", "Russian"
]
async def generate_description_async(
model: genai.GenerativeModel,
app_name: str,
lang: str,
semaphore: asyncio.Semaphore
) -> tuple[str, str]:
"""
semaphore で同時リクエスト数を制限しながら非同期生成する。
返り値は (言語, 説明文) のタプル。
"""
async with semaphore:
prompt = f"""
Write an App Store description for "{app_name}" in {lang}.
Keep it under 4000 characters. Focus on beauty, relaxation, and daily use.
"""
# generate_content_async は非同期版
response = await model.generate_content_async(prompt)
return lang, response.text
async def generate_all_languages(app_name: str) -> dict[str, str]:
model = genai.GenerativeModel("gemini-2.5-flash")
# 同時リクエスト数を5に制限(RPM超過を防ぐ)
semaphore = asyncio.Semaphore(5)
tasks = [
generate_description_async(model, app_name, lang, semaphore)
for lang in LANGUAGES
]
# 全タスクを並列実行
results = await asyncio.gather(*tasks, return_exceptions=True)
output = {}
for result in results:
if isinstance(result, Exception):
print(f"❌ エラー: {result}")
else:
lang, text = result
output[lang] = text
print(f"✓ {lang} 完了")
return output
# 実行
start = time.time()
results = asyncio.run(generate_all_languages("Beautiful HD Wallpapers"))
elapsed = time.time() - start
print(f"\n合計: {elapsed:.1f}秒") # 実測: 約65秒(1分5秒)実測で約65秒になりました。780秒 → 65秒、約12倍の高速化です。
落とし穴1:429 エラー(レート制限)への対処
並列化するとレート制限(429 Too Many Requests)に当たる頻度が上がります。Gemini API の無料枠は RPM(リクエスト/分)の上限が比較的低く、特に注意が必要です。実際に運用していて、Semaphore の値を10に設定したところ、複数言語分の結果が ResourceExhausted エラーになる場面がありました。
以下は指数バックオフ付きのリトライ実装です。リトライ間隔にランダム値(ジッター)を加えることで、複数タスクが同時にリトライして再び詰まる「サンダリングハード問題」を避けられます。
import asyncio
import random
import google.generativeai as genai
from google.api_core import exceptions as google_exceptions
async def generate_with_retry(
model: genai.GenerativeModel,
prompt: str,
semaphore: asyncio.Semaphore,
max_retries: int = 3
) -> str:
"""
レート制限時に指数バックオフでリトライする。
"""
async with semaphore:
for attempt in range(max_retries):
try:
response = await model.generate_content_async(prompt)
return response.text
except google_exceptions.ResourceExhausted as e:
if attempt == max_retries - 1:
raise
# ジッターを加えた指数バックオフ
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"⚠️ レート制限: {wait:.1f}秒待機後リトライ")
await asyncio.sleep(wait)
except Exception as e:
raiseSemaphore の最適値は利用プランによって変わります。無料枠(15 RPM)では Semaphore(3) 程度が安定していました。有料プランでは5で問題ありませんでした。
落とし穴2:asyncio.gather() のエラー処理
asyncio.gather() は return_exceptions=True を指定しないと、最初の例外が発生した時点で全タスクが中断されます。12言語のうち1言語が失敗しただけで、残り11言語の結果が全て失われてしまいます。
# ❌ 危険:一つのエラーで全部止まる
results = await asyncio.gather(*tasks)
# ✅ 安全:エラーも返り値として受け取る
results = await asyncio.gather(*tasks, return_exceptions=True)
# 結果を安全に処理
for result in results:
if isinstance(result, Exception):
print(f"スキップ: {result}")
else:
lang, text = result
output[lang] = text失敗した言語は後から個別に再実行する設計にしておくと、運用が楽になります。
4アプリ分を一括処理する
最終的に4アプリ × 12言語 = 48リクエストを一括処理するスクリプトに仕上げました。
APPS = [
"Beautiful HD Wallpapers",
"Ukiyo-e Wallpapers",
"Relaxing Healing",
"Law of Attraction Everyday"
]
async def generate_all_apps():
model = genai.GenerativeModel("gemini-2.5-flash")
semaphore = asyncio.Semaphore(5) # 全アプリ合計で5並列
all_tasks = []
for app in APPS:
for lang in LANGUAGES:
all_tasks.append(
generate_description_async(model, app, lang, semaphore)
)
results = await asyncio.gather(*all_tasks, return_exceptions=True)
# ... 結果を {app: {lang: text}} 形式の辞書に整理して返す48リクエストを一括処理しても実測で約90〜120秒で完了しました。順次処理なら52分かかる計算だったので、体感としては全く別のツールになりました。
実運用での注意:生成結果の品質確認を省略しない
高速化の副作用として「結果を確認せずに使ってしまう」罠があります。順次処理で1言語ずつ確認しながら進めていた頃は、おかしな表現に気づきやすかったのですが、一括生成では画面をスクロールするだけになりがちです。
私の場合、特に以下の言語で細かい確認が必要でした。
アラビア語(Arabic)は右から左に書く言語のため、App Store で表示したときの折り返しが他言語と異なります。生成された説明文が文法的に正しくても、実際のストアページでの見た目の確認は必要です。中国語繁体字(Traditional Chinese)は、簡体字で生成された後に変換されたような表現になることがあり、台湾のストアで使うには少しぎこちない場合があります。
これらの確認作業自体は並列化で高速化できるものではありませんが、「13分待ってやっと出た結果を確認する」のと「65秒で出た結果を確認する」のでは、心理的な余裕が全然違います。速くなったことで、確認に時間をかけられるようになりました。
また、生成結果をファイルに保存しておくことも重要です。Gemini API の応答は毎回微妙に異なるため、一度良い結果が出たらそれをバージョン管理しておくことをお勧めします。私は app_descriptions/{app_name}/{lang}/v{date}.txt という形式で保存し、更新時に前回との diff を確認するフローにしています。
個人開発者が Gemini API の asyncio を使う上で感じていること
宮大工だった祖父が「手を動かすことが一つの信心」と言っていました。開発でも同じだと感じています。13分待つスクリプトは「後でやろう」になり、1分のスクリプトは「今すぐやろう」になります。この違いが、アップデートの品質に影響します。
並列化してから、各言語の説明文を丁寧に確認・調整できるようになりました。特に Arabic と中国語繁体字は微妙なニュアンスの調整が必要で、以前は「時間がないから今回はこれで」と妥協していました。処理が速くなると、その妥協がなくなります。
Gemini API は generate_content_async() を公式サポートしているため、既存の同期コードからの移行は思ったより少ない変更で済みます。まず1〜3言語の小さなバッチで試してから、徐々に並列数を増やしていくのがお勧めです。
asyncio の基礎については Gemini API Python SDK 公式ドキュメント のサンプルが参考になります。Python の非同期処理をさらに深く