2014年からiOSとAndroidの壁紙・ヒーリング系アプリを個人で開発してきて、今では複数のアプリを運営しています。この分野で長く続けてこられた理由のひとつは、コンテンツの量と質をどう維持するかという問題に向き合い続けてきたことだと思っています。
Claude APIを自分のアプリに組み込んでみたのは、その延長線上でした。「AIで壁紙を生成する」という方向ではなく、「すでにある大量のコンテンツをより賢く扱う」ための実装です。
ここでは私が実際に行った3つの実装パターンを、コードと一緒に記録します。壁紙アプリに限らず、画像コンテンツを扱う個人開発者の方に参考になる部分があると思っています。
背景:なぜ Claude API を選んだか
壁紙アプリのコンテンツ管理で長年困っていたのは、大量の画像に対するメタデータ管理です。カテゴリ分類、説明文生成、タグ付与、多言語対応——これらを人手でやると膨大な時間がかかります。
画像生成AIを使えばコンテンツ自体を自動化できますが、私のアプリは独自の世界観とキュレーションで差別化してきたので、そこは変えたくありません。欲しかったのは「既存のコンテンツを賢く整理し、説明する」機能でした。
Claude APIを選んだ理由はいくつかあります。日本語の扱いがよいこと、長いコンテキストで一貫した判断ができること、そして何より「トーンの指定」が効くことです。ヒーリング系アプリの文脈では、文体のトーンが重要です。「おだやかに」「自然体で」という指示が実際に反映される点が、他のモデルと比べて私には合っていました。
実装1:コンテンツキュレーションスコアリング
最初に実装したのは、新しい壁紙画像をライブラリに追加するときのキュレーションスコアリングです。
画像そのものをClaudeに渡すのではなく、人間が付けた簡易なメモ(色調、モチーフ、撮影条件など)をテキストとして渡して、アプリのコンセプトとの適合度を評価してもらうアプローチを取りました。
import anthropic
import json
client = anthropic.Anthropic()
CURATION_SYSTEM_PROMPT = """
あなたは癒し系・ミニマリスト系の壁紙アプリのコンテンツキュレーターです。
以下の基準でコンテンツを評価してください。
アプリのコンセプト:
- テーマ: 静寂、自然、ミニマリズム、感情的な余白
- ターゲット: 日々の疲れを感じている20〜40代のユーザー
- 避けるべき要素: 派手な色使い、商業的なグラフィック、情報量の多い構図
スコアリング基準(0-10の整数):
- concept_fit: アプリコンセプトとの適合度
- calm_factor: 落ち着きを感じさせる度合い
- uniqueness: 差別化できる独自性
必ずJSON形式で返してください。
"""
def score_wallpaper_content (content_metadata: dict ) -> dict :
"""
壁紙コンテンツのキュレーションスコアを取得する
Args:
content_metadata: 画像のメタデータ辞書
{
"filename": "sunrise_mist_001.jpg",
"colors": "ソフトオレンジ、薄いグレー、白",
"motif": "朝霧の中の湖面",
"mood_memo": "穏やか、少し物悲しい",
"season": "秋",
"composition": "水平線が中央、余白多め"
}
Returns:
スコアと選定理由を含む辞書
"""
prompt = f """
以下の壁紙コンテンツを評価してください。
ファイル名: { content_metadata.get( 'filename' , '不明' ) }
色調: { content_metadata.get( 'colors' , '不明' ) }
モチーフ: { content_metadata.get( 'motif' , '不明' ) }
雰囲気メモ: { content_metadata.get( 'mood_memo' , '不明' ) }
季節: { content_metadata.get( 'season' , '不明' ) }
構図: { content_metadata.get( 'composition' , '不明' ) }
以下のJSON形式で返してください:
{{
"scores": {{
"concept_fit": <0-10>,
"calm_factor": <0-10>,
"uniqueness": <0-10>
}} ,
"total": <合計0-30>,
"recommendation": "add" | "review" | "skip",
"reason": "<選定理由を日本語で1〜2文>"
}}
"""
try :
message = client.messages.create(
model = "claude-haiku-4-5-20251001" , # コスト効率のため Haiku を使用
max_tokens = 300 ,
system = CURATION_SYSTEM_PROMPT ,
messages = [{ "role" : "user" , "content" : prompt}]
)
response_text = message.content[ 0 ].text.strip()
# JSON抽出(前後に余分なテキストがある場合の対処)
start = response_text.find( '{' )
end = response_text.rfind( '}' ) + 1
if start >= 0 and end > start:
result = json.loads(response_text[start:end])
result[ 'filename' ] = content_metadata.get( 'filename' )
return result
else :
raise ValueError ( f "JSONが見つかりません: { response_text } " )
except json.JSONDecodeError as e:
# JSONパースエラー時はスキップ推奨として返す
return {
"filename" : content_metadata.get( 'filename' ),
"scores" : { "concept_fit" : 0 , "calm_factor" : 0 , "uniqueness" : 0 },
"total" : 0 ,
"recommendation" : "review" ,
"reason" : f "評価エラー: { str (e) } "
}
# 使用例
if __name__ == "__main__" :
sample = {
"filename" : "autumn_lake_morning.jpg" ,
"colors" : "テラコッタ、薄いグレー、霧のような白" ,
"motif" : "秋の朝霧に包まれた静かな湖" ,
"mood_memo" : "懐かしさと静けさが混在する" ,
"season" : "秋" ,
"composition" : "湖面が画面の60%を占め、空との境界がぼやけている"
}
result = score_wallpaper_content(sample)
print (json.dumps(result, ensure_ascii = False , indent = 2 ))
# 期待する出力例:
# {
# "scores": { "concept_fit": 9, "calm_factor": 8, "uniqueness": 7 },
# "total": 24,
# "recommendation": "add",
# "reason": "アプリのコンセプトに高い適合度。霧のテクスチャが独自性を持つ。",
# "filename": "autumn_lake_morning.jpg"
# }
このスコアリングをバッチで実行すると、100枚の画像評価が10〜15分で終わります。以前は1枚ずつ自分で判断していたので、この自動化だけで週に数時間が返ってきました。
実装で注意したこと : Haikuモデルを使っているのは、このタスクではSonnetほどの能力が必要なく、コストを10分の1以下に抑えられるからです。キュレーション判断は最終的に人間が確認するので、スコアの参考値として使う分には十分です。
実装2:App Store説明文の多言語生成
これが最も費用対効果が高かった実装です。
壁紙アプリを8言語対応させているのですが、説明文の翻訳・ローカライズは長年の悩みでした。機械翻訳では文体が硬くなり、アプリの「ヒーリング」な雰囲気が伝わらありません。
import anthropic
import time
client = anthropic.Anthropic()
TARGET_LANGUAGES = {
"en" : "English (US)" ,
"ja" : "日本語" ,
"fr" : "Français" ,
"de" : "Deutsch" ,
"es" : "Español" ,
"pt" : "Português (BR)" ,
"ko" : "한국어" ,
"zh-Hans" : "简体中文"
}
def generate_localized_description (
app_name: str ,
base_description_ja: str ,
target_lang: str ,
lang_name: str ,
max_chars: int = 4000
) -> str :
"""
アプリの説明文を特定言語向けにローカライズして生成する
Args:
app_name: アプリ名
base_description_ja: 日本語の基本説明文
target_lang: 言語コード (例: "en", "fr")
lang_name: 言語の表示名
max_chars: App Store の文字数上限
Returns:
ローカライズされた説明文テキスト
"""
if target_lang == "ja" :
# 日本語はそのまま返す
return base_description_ja
system_prompt = f """
あなたはヒーリング・ウェルネス系モバイルアプリのコピーライターです。
App Store / Google Play の説明文を { lang_name } で作成します。
重要な指針:
- トーンは穏やか、温かく、押しつけがましくない
- ユーザーの「疲れ」「休息」「日常の美しさ」への共感を示す
- 機能の羅列ではなく、体験を伝える
- 最大 { max_chars } 文字( { lang_name } )に収める
- { lang_name } のApp Storeユーザーが自然に読める表現を使う
- 翻訳ではなく、 { lang_name } のネイティブが書いたようなテキストにする
"""
user_prompt = f """
アプリ名: { app_name }
日本語の説明文(参考):
{ base_description_ja }
この説明文を参考に、 { lang_name } のApp Storeユーザーに向けた説明文を作成してください。
直訳せず、 { lang_name } 圏のユーザーに自然に響く表現で書いてください。
"""
try :
message = client.messages.create(
model = "claude-sonnet-4-6" , # 品質重視のためSonnetを使用
max_tokens = 1500 ,
system = system_prompt,
messages = [{ "role" : "user" , "content" : user_prompt}]
)
return message.content[ 0 ].text.strip()
except anthropic.RateLimitError:
# レート制限に当たった場合は30秒待ってリトライ
time.sleep( 30 )
return generate_localized_description(
app_name, base_description_ja, target_lang, lang_name, max_chars
)
except anthropic.APIError as e:
raise RuntimeError ( f " { lang_name } の生成に失敗: { str (e) } " )
def generate_all_localizations (
app_name: str ,
base_description_ja: str
) -> dict :
"""全対応言語の説明文を生成する"""
results = {}
for lang_code, lang_name in TARGET_LANGUAGES .items():
print ( f " 生成中: { lang_name } ..." )
try :
text = generate_localized_description(
app_name = app_name,
base_description_ja = base_description_ja,
target_lang = lang_code,
lang_name = lang_name
)
results[lang_code] = {
"status" : "success" ,
"text" : text,
"char_count" : len (text)
}
except RuntimeError as e:
results[lang_code] = {
"status" : "error" ,
"error" : str (e),
"text" : ""
}
# APIレート制限への配慮(言語間で2秒待機)
if lang_code \ != list(TARGET_LANGUAGES.keys())[-1]:
time.sleep( 2 )
return results
# 使用例
if __name__ == "__main__" :
app_name = "Healing Wallpapers — Calm Screens"
base_ja = """
毎日の疲れを、壁紙で少しだけ和らげませんか。
「Healing Wallpapers」は、自然の静けさと現代的なミニマリズムが交わる壁紙を集めたコレクションアプリです。湖面に映る朝霧、秋の光が差し込む森、モノクロームの波紋——どれも、眺めるだけで少し呼吸が楽になるような画を選んでいます。
毎週新しい壁紙を追加。季節に合わせた特集も定期的にお届けします。
""" .strip()
print ( f "説明文を { len ( TARGET_LANGUAGES ) } 言語で生成中... \n " )
results = generate_all_localizations(app_name, base_ja)
for lang_code, result in results.items():
print ( f " \n [ { lang_code } ] { TARGET_LANGUAGES [lang_code] } " )
if result[ "status" ] == "success" :
print ( f "文字数: { result[ 'char_count' ] } " )
print (result[ "text" ][: 200 ] + "..." if len (result[ "text" ]) > 200 else result[ "text" ])
else :
print ( f "エラー: { result[ 'error' ] } " )
8言語の説明文生成に要した時間は約3分、APIコストは1回あたり約$0.15でした。人が翻訳会社に依頼する場合との比較で考えると、個人開発規模では十分に現実的な選択肢です。
品質管理の現実 : 生成された説明文は必ず1度は目を通しています。特にドイツ語・フランス語など私自身が話せない言語は、ネイティブの友人に確認してもらったことがあります。今のところ大きな問題は見つかっていませんが、文化的な感度が必要な表現は慎重に確認することをお勧めします。
実装3:タグ自動付与とカテゴリ分類
3つ目は、コンテンツのタグ管理です。数百枚の画像に一貫したタグを付け、カテゴリ分類をする作業は、後からやり直すと特に大変です。
import anthropic
import json
client = anthropic.Anthropic()
# アプリで使用するタグセット(変更は少ない)
ALLOWED_TAGS = {
"color" : [ "monochrome" , "pastel" , "earth-tones" , "cool" , "warm" ],
"motif" : [ "nature" , "water" , "forest" , "sky" , "architecture" , "abstract" ],
"mood" : [ "calm" , "nostalgic" , "minimal" , "meditative" , "joyful" ],
"time" : [ "morning" , "evening" , "night" , "golden-hour" ],
"season" : [ "spring" , "summer" , "autumn" , "winter" , "timeless" ]
}
ALLOWED_CATEGORIES = [
"nature-calm" ,
"urban-minimal" ,
"abstract-art" ,
"seasonal-special" ,
"monochrome"
]
def tag_and_categorize (content_description: str ) -> dict :
"""
コンテンツの説明文からタグとカテゴリを自動生成する
Args:
content_description: コンテンツの自然言語による説明
Returns:
タグとカテゴリを含む辞書
"""
system_prompt = """
壁紙アプリのコンテンツタグ付けを行います。
指定されたタグと カテゴリのみを使用してください。
必ずJSON形式で返してください。
"""
user_prompt = f """
以下のコンテンツにタグとカテゴリを付けてください。
コンテンツ説明:
{ content_description }
使用可能なタグ(各グループから0〜2個選択):
{ json.dumps( ALLOWED_TAGS , ensure_ascii = False ) }
使用可能なカテゴリ(1つだけ選択):
{ ALLOWED_CATEGORIES }
JSON形式で返してください:
{{
"tags": {{
"color": [...],
"motif": [...],
"mood": [...],
"time": [...],
"season": [...]
}} ,
"primary_category": "...",
"confidence": <0.0-1.0の数値>
}}
"""
try :
message = client.messages.create(
model = "claude-haiku-4-5-20251001" ,
max_tokens = 400 ,
system = system_prompt,
messages = [{ "role" : "user" , "content" : user_prompt}]
)
response_text = message.content[ 0 ].text.strip()
start = response_text.find( '{' )
end = response_text.rfind( '}' ) + 1
if start >= 0 and end > start:
result = json.loads(response_text[start:end])
# バリデーション: 許可されていないタグを除去
validated_tags = {}
for group, tags in result.get( "tags" , {}).items():
if group in ALLOWED_TAGS :
validated_tags[group] = [
t for t in tags if t in ALLOWED_TAGS [group]
]
result[ "tags" ] = validated_tags
# バリデーション: 許可されていないカテゴリはデフォルトに
if result.get( "primary_category" ) not in ALLOWED_CATEGORIES :
result[ "primary_category" ] = "nature-calm"
result[ "confidence" ] = 0.5
return result
else :
raise ValueError ( f "JSONが見つかりません" )
except (json.JSONDecodeError, ValueError ) as e:
# エラー時はデフォルト値を返す(処理は止めない)
return {
"tags" : {k: [] for k in ALLOWED_TAGS .keys()},
"primary_category" : "nature-calm" ,
"confidence" : 0.0 ,
"error" : str (e)
}
# 使用例
if __name__ == "__main__" :
desc = "霧の中、湖面に映る一本の裸木。早朝の青みがかった光。音が消えたような静けさ。"
result = tag_and_categorize(desc)
print (json.dumps(result, ensure_ascii = False , indent = 2 ))
# 期待する出力例:
# {
# "tags": {
# "color": ["cool", "monochrome"],
# "motif": ["nature", "water"],
# "mood": ["calm", "minimal"],
# "time": ["morning"],
# "season": ["timeless"]
# },
# "primary_category": "nature-calm",
# "confidence": 0.91
# }
このバリデーション処理が重要です。AIは時として、指定していないタグを生成することがあります。許可リストと照合してフィルタリングすることで、アプリのデータ整合性を保てます。
コスト管理の現実
個人開発でAPIを使う際に一番気になるのは、月々のコストです。私の場合、上記3つの実装を合わせた月間APIコストは**$15〜$25**程度に収まっています。
コストを抑えるために実践している工夫:
モデルの使い分け : キュレーションスコアリングとタグ付けはHaikuで十分。品質が重要な説明文生成だけSonnetを使います。Haikuはコストがおよそ1/10です。
キャッシュ活用 : 同じ画像を再処理しないよう、スコアリング結果をローカルのJSONファイルに保存しています。Claude API のプロンプトキャッシュを使ったコスト最適化 も、長いシステムプロンプトを繰り返し使う実装では有効です。
バッチ処理の時間帯 : 大量処理はオフピーク時間に実行するようにしています。レート制限に当たりにくくなります。
よくある落とし穴と対処法
実装していて実際に詰まったポイントを正直に書いておきます。
JSONが壊れて返ってくる問題 : Claude は概ねJSONを正しく返しますが、長い回答や複雑な指示のときに前後に説明文が混入することがあります。find('{') と rfind('}') でJSON部分を抽出する処理は必須です。
レート制限でバッチ処理が途中で止まる問題 : anthropic.RateLimitError を捕捉して指数バックオフでリトライする仕組みを入れましょう。本番で1,000枚の画像を処理中にクラッシュすると辛いです。
言語によって生成品質にばらつきがある問題 : 日本語・英語・韓国語は品質が安定しています。一部のアジア言語や中東言語では、文字コードの扱いや文体の適切さに注意が必要です。
公式ドキュメントには書かれていない、運用してわかったこと
ここからは、実際に自分のアプリのコンテンツパイプラインで動かし続けて、数字として見えてきたことを書いておきます。公式ドキュメントには載っていない、運用してはじめて気づいた部分です。
1画像あたりのトークンとコストの実測
3つの実装を1枚の画像に通すと、消費トークンはおおよそ次の内訳になりました。あくまで私のプロンプトとアプリ構成での実測値ですが、見積もりの出発点にはなると思います。
キュレーションスコアリング(Haiku): 入力 約 480 トークン / 出力 約 90 トークン
タグ・カテゴリ分類(Haiku): 入力 約 520 トークン / 出力 約 120 トークン
App Store 説明文の多言語生成(Sonnet・5言語): 入力 約 700 トークン / 出力 約 1,400 トークン
説明文生成だけが Sonnet で、しかも多言語ぶん出力が膨らむため、1画像あたりのコストの 8割近くがこの工程 に偏ります。逆に言えば、スコアリングとタグ付けをいくら回しても費用はほとんど増えません。「重い工程はどこか」を最初に測っておくと、最適化の順番を間違えずに済みます。
新規に 1,000 枚を一括処理したときの月のAPI明細は、Haiku 側が $2 弱、Sonnet 側が $12 前後でした。冒頭で触れた月 $15〜$25 という幅は、その月に何枚を新規説明文生成にかけたかでほぼ決まります。
バッチを途中で落とさないための完全な実装
「よくある落とし穴」で触れた、レート制限でバッチが止まる問題への対処を、断片ではなく動く形で残しておきます。指数バックオフに加えて、処理済みフラグをローカルに保存して再開できる ようにしているのが実運用上のポイントです。1,000枚の途中で落ちても、最初からやり直さずに済みます。
import json
import os
import time
import random
import anthropic
client = anthropic.Anthropic()
STATE_PATH = "curation_state.json"
def load_state () -> dict :
if os.path.exists( STATE_PATH ):
with open ( STATE_PATH , encoding = "utf-8" ) as f:
return json.load(f)
return {}
def save_state (state: dict ) -> None :
# 一時ファイルに書いてから置き換える(処理中のクラッシュで壊さない)
tmp = STATE_PATH + ".tmp"
with open (tmp, "w" , encoding = "utf-8" ) as f:
json.dump(state, f, ensure_ascii = False )
os.replace(tmp, STATE_PATH )
def call_with_backoff (score_fn, item, max_retries: int = 6 ):
"""指数バックオフ付きで1件処理する。429/5xx のみリトライする"""
for attempt in range (max_retries):
try :
return score_fn(item)
except anthropic.RateLimitError:
wait = min ( 2 ** attempt + random.uniform( 0 , 1 ), 60 )
print ( f " rate limited, retry in { wait :.1f } s ( { attempt + 1 } / { max_retries } )" )
time.sleep(wait)
except anthropic.APIStatusError as e:
if e.status_code and e.status_code >= 500 :
wait = min ( 2 ** attempt, 30 )
time.sleep(wait)
else :
raise # 4xx は設定ミスなので即座に表面化させる
raise RuntimeError ( f "max retries exceeded for { item[ 'id' ] } " )
def process_library (items, score_fn):
"""再開可能なバッチ処理。落ちても処理済みはスキップする"""
state = load_state()
done = 0
for item in items:
if item[ "id" ] in state:
continue # すでに処理済み
result = call_with_backoff(score_fn, item)
state[item[ "id" ]] = result
save_state(state) # 1件ごとに永続化
done += 1
print ( f "完了: 新規 { done } 件 / 累計 { len (state) } 件" )
return state
要点は3つです。ひとつは、リトライ対象を 429 と 5xx に限る こと。400系(リクエストの作り間違い)まで黙ってリトライすると、壊れたリクエストを延々と投げ続けてコストだけ溶かします。ふたつめは、状態ファイルを 一時ファイル経由で置き換える こと。保存の途中でクラッシュしても JSON が壊れません。みっつめは、1件ごとに保存する こと。まとめて最後に保存する設計だと、999枚目で落ちたときに全部やり直しになります。
多言語生成の品質を一定に保つ運用
言語によって品質がばらつく問題は、プロンプトの作り方で大きく改善できました。私が落ち着いたのは、全言語を1回のリクエストでまとめて生成しない という方針です。
5言語を1回で出すと、後半の言語ほど指示の細部が薄れる傾向がありました。日本語と英語を基準言語として先に確定させ、それを参照訳として渡したうえで残りの言語を生成すると、トーンのぶれが目に見えて減ります。1画像あたりのリクエスト回数は増えますが、Haiku 換算の追加コストはごくわずかで、手直しの時間が減るぶん割に合います。
導入前のチェックリスト
同じように個人開発で画像コンテンツを扱う方が、最初の1本を組むときに確認しておくと安全な項目です。
重い工程を先に測る — どの呼び出しがトークンを食うかを1枚で計測してから量産する
モデルを工程ごとに分ける — 判定・分類は Haiku、品質が要る生成だけ Sonnet
再開可能にする — 処理済みフラグをローカルに持ち、落ちても続きから回せるようにする
JSON 抽出を必ず挟む — find('{') / rfind('}') で本文から JSON を切り出す
コストに上限アラートを置く — 想定の倍に達したら止まるよう、月次トークンの監視を入れておく
このあたりの月次コストの見積もりは、トークンコストの予測 も合わせて読むと精度が上がります。
実装を通じて感じたこと
Claude APIを壁紙アプリに組み込んで、最も大きかった変化は「自分がやりたいことに使える時間が増えた」ことです。
コンテンツキュレーションとメタデータ管理という、大切だけれど時間がかかる作業を半自動化できたことで、新しい壁紙を探したり、アプリのUI改善に集中したりする時間ができました。
完全自動化は目指していません。最終的な判断は自分でします。ただ、「下書きを作る」「大量データを整理する」「多言語化の準備をする」という部分でAPIに任せることで、個人開発の生産性が確実に上がっています。
Claude APIの基本的な使い方については、Claude API のバッチ処理設計 も参考になります。実装を本番環境に展開する前には、APIのレート制限とコスト最適化 も一読をお勧めします。