Communitygithub.com

dtsoden/stock-media-skill

Pexels と Pixabay をまとめて検索し、ストックフォトやストックビデオを取得。最適なものを選び、Web プロジェクト向けにダウンロード・最適化 (画像は WebP、動画は圧縮 MP4 + ポスター) します。ヒーロー画像、背景動画、カードサムネイル、イラストなどにお使いいただけます。

stock-media-skill とは?

このスキルは、フロントエンド開発プロジェクト向けにストックメディアを調達・最適化するためのツールです。Pexels と Pixabay の両方の API を統合し、写真・動画の検索では両方のサービスを同時に検索して結果を比較し、最適なものを選択できます。イラストやベクターアートの検索は Pixabay のみで行います。取得したメディアは、画像は WebP 形式に、動画は圧縮 MP4 と WebP ポスターフレームに自動変換・最適化され、そのまま Web プロジェクトに組み込める状態で保存されます。主なユースケースとして、ヒーロー画像、背景動画、カードサムネイル、イラストなど、Web ページに必要なストックメディア全般に対応します。API キー管理、検索クエリの生成、メディアのダウンロード・最適化、プロジェクトへの統合までを自動化し、開発者の手間を大幅に削減します。

対応✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/dtsoden/stock-media-skill/tree/HEAD/skills/stock-media

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

ストックメディア調達 (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

…

[翻訳のため省略]

関連スキル