ストックメディア調達 (Pexels + Pixabay)
UI/UX デザイン向けにストック画像・動画を検索し、Web 対応形式で保存します。写真検索は Pexels と Pixabay の両方に対して実行され、2 つの候補プールから最適なものを比較・選択できます。イラスト・ベクター検索は Pixabay のみを対象とします。動画検索は両方をクエリし、圧縮 MP4 と WebP ポスターフレームを保存します。フロントエンドデザインスキルと組み合わせてご利用いただくことをお勧めします。
検索前に知っておくべき制限事項
これらは、Pexels と Pixabay がサインアップ時に提供する無料 API キーに適用されます。2026-10-15 にテスト済み。
- Pixabay の画像ダウンロードは最大幅 1280px (
largeImageURL) です。フル解像度には Pixabay の承認済みフル API アクセスが必要で、無料キーでは利用できません。1280px より広く表示する必要があるもの(フルブリードのヒーロー、1920px のバナーなど)には、Pexels の結果のみを使用してください。 - Pixabay のベクターは、通常透明な背景を持つ 1280px のラスター PNG としてダウンロードされます。無料キーでは SVG ソースは利用できません。ユーザーが真のスケーラブルな SVG を必要とする場合は、結果の
page_urlを提供して、Pixabay から手動でダウンロードできるようにし、その理由を説明してください。 - Pexels は写真と実写動画のみを提供します。イラスト、ベクター、アニメーションの検索は Pixabay のみで行います。
- Pexels の動画は最大 4K まで対応します。Pixabay の動画は通常最大 1920x1080 で、4K に対応するのは稀です。Pixabay の動画検索には向きフィルターがないため、本スキルでは検索後に幅と高さでフィルタリングします。
- 動画には、圧縮とポスターフレーム生成のために ffmpeg が PATH に必要です。
- Pixabay はクエリあたり最大 500 件の結果を返し、60 秒あたり 100 リクエストまで許可します。Pexels は 1 時間あたり 200 リクエスト、月間 20,000 リクエストまで許可します。
- Pixabay の
largeImageURLリンクは約 24 時間で期限切れになるため、ホットリンクしてはいけません。常にファイルをプロジェクトにダウンロードしてください。 - Pixabay キーは URL クエリ文字列に含めます。Pixabay の検索 URL を印刷したりログに記録したりしないでください。
これらの制限が選択に影響を与えた場合は、その旨をユーザーに伝えてください。例:「この Pixabay 画像は 1280px なので、全幅ヒーローとして使用するとぼやけて見えます。」
UTF-8 を出力しないと、写真家の名前が原因でスクリプトが停止します
ここにあるすべての Python スクリプトは写真家やアップロード者の名前を出力しますが、Windows 上の Python は標準出力を cp1252 で書き込みます。名前は世界中のものです。最初の ş、ü、ż、ñ で UnicodeEncodeError: 'charmap' codec can't encode character が発生し、出力の途中で検索が半ば成功した後にクラッシュします。
スクリプトの最初の、インポートの前にエンコーディングを設定します:
import sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
代わりに Bash からスクリプトを実行しますか? コマンドの前に PYTHONIOENCODING=utf-8 を付けてください。どちらでも機能します。どちらもしなければ、名前が英語でない瞬間に失敗します。
API キー
セットアップ (SETUP.md in https://github.com/dtsoden/stock-media-skill) で 2 つのキーを保存し、保存場所を ~/.config/stock-media/config.json に記録します。このファイルが存在せず、scripts/check_keys.py が失敗した場合、スキルがセットアップされていません。ユーザーをそのセットアップに誘導し、処理を停止してください。
キーを印刷したり、プロジェクトに書き込んだり、キーファイルを読み取ったり、ユーザーにチャットにキーを貼り付けさせたりしないでください。キーの読み込みは、このスキルの scripts/ フォルダにあるヘルパーを使用します。このヘルパーは最初に環境変数をチェックし、次に OS の資格情報ストアをチェックします。各検索スクリプトをスクラッチまたは一時ディレクトリ(プロジェクト外)に書き込み、次のように SKILL_DIR をこの SKILL.md があるフォルダの絶対パスに設定して起動してください:
import os, sys
SKILL_DIR = '/absolute/path/to/stock-media' # この SKILL.md があるフォルダ
sys.path.insert(0, os.path.join(SKILL_DIR, 'scripts'))
from stock_media_keys import get_key
PEXELS_KEY = get_key('pexels')
PIXABAY_KEY = get_key('pixabay')
スクリプトの実行方法は、設定ファイルの store 値によって異なります:
keyringまたはenv: 通常通り実行、python search.py。aihsm:aihsm_namesからのボルト名を使用してキーを注入:aihsm run --set PEXELS_API_KEY=<pexels name> --set PIXABAY_API_KEY=<pixabay name> -- python search.py。 python を注入コマンドとして直接実行します。bash -c '...'でラップすると変数が失われます。
python scripts/check_keys.py を実行して (ストアが aihsm の場合は aihsm 経由で)、キーが印刷されずに両方動作することを確認します。サンドボックスがネットワークアクセスまたは OS 資格情報ストアをブロックする場合は、ユーザーにこれらのスクリプトの許可を求めてください。
Pixabay キーはクエリ文字列で送信されるため、Pixabay の検索 URL を印刷したりログに記録したりしないでください。どちらのサービスからの画像や動画の URL も印刷しても問題ありません。
重要: ブラウザの User-Agent を送信する
Pexels の API と CDN は Cloudflare の背後にあり、デフォルトの Python-urllib やボットのユーザーエージェントを HTTP 403、error code: 1010 でブロックします。両サービスへのリクエスト(検索と画像ダウンロードの両方)には、実際のブラウザの User-Agent を送信してください:
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
引数
リクエストでは、必要な画像とその使用方法を説明します。以下のいずれかになります:
- 必要な画像の自然言語による説明 (例:「フィンテックアプリのヒーロー背景」、「アバウトページ用のチーム写真」、「製品紹介画像」)
- 特定の検索クエリ
- UI/UX を強化する画像を決定するデザインコンテキスト
ワークフロー
1. デザインコンテキストを理解する
- 対象のコンポーネント/ページファイルを読み、ビジュアルデザイン言語、カラーパレット、レイアウト、目的を理解する
- 画像が果たす役割(ヒーロー、背景、イラスト、カードサムネイル、アバター、テクスチャなど)を決定する
- 必要な寸法(フルブリード、カード、アイコンなど)を計算する。これにより Pixabay が使用可能かどうかが決まります(以下のサイズ制限を参照)。
2. 両方のサービスを検索する
1 つの関数で両方をクエリし、共通の形状で結果を返すため、各サービスの候補を並べて比較できます:
import sys, os, json, urllib.request, urllib.parse
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
SKILL_DIR = '/absolute/path/to/stock-media' # この SKILL.md があるフォルダ
sys.path.insert(0, os.path.join(SKILL_DIR, 'scripts'))
from stock_media_keys import get_key
PEXELS_KEY = get_key('pexels')
PIXABAY_KEY = get_key('pixabay')
def _get(url, headers=None):
req = urllib.request.Request(url, headers={'User-Agent': UA, **(headers or {})})
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
def search_pexels(query, orientation='landscape', per_page=15, color=None):
"""orientation: landscape | portrait | square
color: red | orange | yellow | green | turquoise | blue | violet | pink |
brown | black | gray | white、または 'ffffff' のような16進コード"""
params = {'query': query, 'per_page': per_page, 'orientation': orientation}
if color:
params['color'] = color
data = _get('https://api.pexels.com/v1/search?' + urllib.parse.urlencode(params),
{'Authorization': PEXELS_KEY})
return [{
'source': 'pexels',
'type': 'photo',
'id': p['id'],
'width': p['width'], 'height': p['height'],
'max_download_width': p['width'],
'description': p.get('alt') or '',
'author': p['photographer'],
'page_url': p['url'],
'preview_url': p['src']['medium'],
'download_url': p['src']['original'],
'avg_color': p.get('avg_color'),
'popularity': None,
} for p in data['photos']]
PIXABAY_ORIENTATION = {'landscape': 'horizontal', 'portrait': 'vertical', 'square': 'all'}
def search_pixabay(query, orientation='landscape', per_page=15, color=None, min_width=0,
image_type='photo'):
"""image_type: photo | illustration | vector | all
color: grayscale | transparent | red | orange | yellow | green |
turquoise | blue | lilac | pink | white | gray | black | brown
クエリは最大100文字に制限されます。"""
params = {'key': PIXABAY_KEY, 'q': query[:100], 'image_type': image_type,
'orientation': PIXABAY_ORIENTATION[orientation],
'per_page': max(3, per_page), 'safesearch': 'true'}
if color:
params['colors'] = color
if min_width:
params['min_width'] = min_width
data = _get('https://pixabay.com/api/?' + urllib.parse.urlencode(params))
hits = [h for h in data['hits']
if not h.get('isAiGenerated') and not h.get('isLowQuality')]
return [{
'source': 'pixabay',
'type': h['type'],
'id': h['id'],
'width': h['imageWidth'], 'height': h['imageHeight'],
'max_download_width': min(1280, h['imageWidth']),
'description': h['tags'],
'author': h['user'],
'page_url': h['pageURL'],
'preview_url': h['webformatURL'],
'download_url': h['largeImageURL'],
'avg_color': None,
'popularity': {'likes': h['likes'], 'downloads': h['downloads']},
} for h in hits]
def search_all(query, orientation='landscape', per_page=15, color=None, image_type='photo'):
"""image_type 'photo' は両方のサービスを検索します。'illustration' または 'vector'
は Pixabay のみを検索します。Pexels には写真以外のものがないためです。"""
results = []
calls = [lambda: search_pixabay(query, orientation, per_page, color, image_type=image_type)]
if image_type in ('photo', 'all'):
calls.insert(0, lambda: search_pexels(query, orientation, per_page, color))
for call in calls:
try:
results += call()
except Exception as e:
# 片方のサービスが失敗しても検索全体を台無しにしない
print(f'search failed: {type(e).__name__} {getattr(e, "code", "")}')
return results
イラストとベクター
Pixabay にはイラストやベクターアートも含まれているため、search_all(..., image_type='illustration') でスポットイラスト、空の状態のアート、写真では適さない個性的なアイコンなどをカバーできます。イラスト検索ではベクターアイテムも返されます(type は vector/svg として返されます)。
このキーでは SVG ソースは取得できません。ベクターは、多くの場合透明な背景を持つ 1280px の PNG としてダウンロードされます。download_and_optimize は WebP 内の透明度を維持します。ユーザーが真のスケーラブルな SVG を必要とする場合は、その旨を伝え、Pixabay から手動でダウンロードできるように page_url にリンクしてください。
color の値はサービス間で異なります。両方が受け入れるもの(red、orange、yellow、green、turquoise、blue、pink、white、gray、black、brown)を渡すか、各関数を個別に呼び出してください。
さまざまなクエリで複数の検索を実行します。両方のサービスの候補を一緒に評価します:
- サイズ:
max_download_widthがユースケースをカバーしている必要があります。Pixabay は最大 1280px なので、1920px のヒーローの場合、Pexels の結果のみが適格です。 - 色: Pexels は
avg_colorを提供します。Pixabay の場合はプレビューを確認します。 - 被写体: Pexels の
descriptionは代替テキスト、Pixabay のはタグリストです。 - 人気: Pixabay のいいね数とダウンロード数は、大まかな品質指標です。ダウンロード数が多い画像は、他のサイトでも表示される可能性が高くなります。
- 実際に見てみる: 選択する前に、候補に絞り込んだ画像の
preview_urlをスクラッチパッドにダウンロードして表示します。テキストメタデータだけでは適切に選択できません。
search_pixabay は既に isAiGenerated または isLowQuality とフラグが立てられた結果を除外しています。
3. ダウンロードと最適化
選択した画像をダウンロードし、最適化された WebP として保存します。同じ関数がいずれかのソースを処理します:
from PIL import Image
from io import BytesIO
def download_and_optimize(photo, output_name, output_dir, max_width=1920, quality=75):
"""
photo: search_all / search_pexels / search_pixabay からの1つの結果辞書
output_name: 拡張子なしのファイル名
output_dir: 保存先(例: public/images/)
max_width: この最大幅にリサイズ(アスペクト比保持)
quality: WebP 品質 1-100。ガイドライン:
- ヒーロー/ショーケース画像: 75-85
- カードサムネイル: 65-75
- 背景/テクスチャ画像: 40-55
- 装飾/ほとんど見えないもの: 30-45
"""
os.makedirs(output_dir, exist_ok=True)
url = photo['download_url']
if photo['source'] == 'pexels':
url += f'?auto=compress&cs=tinysrgb&w={max_width}'
req = urllib.request.Request(url, headers={'User-Agent': UA})
with urllib.request.urlopen(req) as resp:
img_data = resp.read()
img = Image.open(BytesIO(img_data))
if img.width < max_width:
print(f' 注意: ソースの幅は {img.width}px のみで、{max_width}px が要求されました')
if img.width > max_width:
ratio = max_width / img.width
img = img.resize((max_width, int(img.height * ratio)), Image.LANCZOS)
webp_path = os.path.join(output_dir, f'{output_name}.webp')
img.save(webp_path, 'WEBP', quality=quality, method=6)
orig_size = len(img_data)
new_size = os.path.getsize(webp_path)
print(f'{output_name}.webp: {img.width}x{img.height}, {new_size:,} バイト ({(1-new_size/orig_size)*100:.0f}% 削減)')
print(f' ソース: {photo["source"]} {photo["page_url"]}')
print(f' 作者: {photo["author"]}')
return webp_path
Pixabay の largeImageURL リンクは約 24 時間で期限切れになるため、ホットリンクしてはいけません。常にファイルをプロジェクトにダウンロードして配信してください。
4. 統合
- 画像をプロジェクトの images ディレクトリに保存します(プロジェクト構造から検出、通常は
public/images/、static/、assets/、src/assets/など) - コード内でフレームワークに適したパスを使用して画像を参照します
- 処理前に
Pillowがインストールされていることを確認します (pip install Pillow) - 選択した各画像がどのサービスとページから来たかをユーザーに伝えます
動画
両方のサービスにストック動画があり、動画検索は常に両方をクエリします。ヒーロー背景のループや、短い製品紹介やムードクリップに使用します。いずれのサービスからのファイルも MP4 です。Pixabay は動画を film(実写映像)と animation(モーショングラフィックス、抽象的なループ)に分類しています。
2026-10-15 にテストした際のサイズ: Pexels off
…
[翻訳のため省略]