Communitygithub.com

dtsoden/stock-media-skill

Durchsucht Pexels und Pixabay gemeinsam nach Stockfotos und -videos, wählt den besten Treffer aus, lädt ihn herunter und optimiert ihn für Webprojekte (WebP für Bilder, komprimiertes MP4 mit Poster für Videos). Ideal für Hero-Bilder, Hintergrundvideos, Karten-Thumbnails und alle Stockmedien, die eine Webseite benötigt.

Was ist stock-media-skill?

Dieses Skill ermöglicht es einem KI-Agenten, in zwei großen Stockmedien-Plattformen – Pexels und Pixabay – gleichzeitig nach Fotos und Videos zu suchen, die besten Ergebnisse auszuwählen, herunterzuladen und für die Webnutzung zu optimieren. Es unterstützt Fotos (beide Dienste), Illustrationen, Vektorgrafiken und Animationen (nur Pixabay) sowie Videos (beide Dienste). Der Skill berücksichtigt wichtige technische Einschränkungen: Pixabay-Bilder sind auf 1280px Breite begrenzt, Vektorgrafiken werden als PNG mit Transparenz heruntergeladen (kein SVG-Quellcode mit kostenlosen API-Keys). Der Download- und Optimierungsprozess konvertiert Bilder in WebP mit einstellbarer Qualität (30–85) und skaliert sie auf eine maximale Breite. Für Videos wird ffmpeg benötigt, um eine komprimierte MP4-Datei und ein WebP-Posterbild zu erstellen. Der Skill legt großen Wert auf korrekte API-Schlüsselverwaltung (Umgebungsvariablen, Keyring oder aihsm), UTF-8-Kodierung für internationale Fotografennamen und das Setzen eines Browser-User-Agents, um Cloudflare-Blocks (HTTP 403) zu vermeiden. Die Ausgabe erfolgt als Python-Skripte, die in einem temporären Verzeichnis ausgeführt werden, und der Agent kann die Ergebnisse direkt im Projekt integrieren (z. B. in `public/images/`).

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

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Stockmedien-Beschaffung (Pexels + Pixabay)

Finde Stockfotos und -videos für UI/UX-Designs und speichere sie in webtauglichen Formaten. Die Bildersuche durchsucht sowohl Pexels als auch Pixabay, so dass du zwei Kandidatenpools vergleichen und den besten aus beiden auswählen kannst. Die Suche nach Illustrationen und Vektorgrafiken erfolgt nur bei Pixabay. Die Videosuche durchsucht beide Dienste und speichert eine komprimierte MP4-Datei mit einem WebP-Posterbild. Passt gut zu einem Frontend-Design-Skill.

Grenzen, die vor der Suche bekannt sein sollten

Diese gelten für die kostenlosen API-Keys, die Pexels und Pixabay bei der Anmeldung ausgeben. Getestet am 15.10.2026.

  • Pixabay-Bilder werden maximal 1280px breit heruntergeladen (largeImageURL). Volle Auflösung erfordert den genehmigten Vollzugriff auf die Pixabay-API, den kostenlose Keys nicht haben. Für alles, das breiter als 1280px gerendert werden muss (ein randloses Hero-Bild, ein 1920px-Banner), verwende ausschließlich Pexels-Ergebnisse.
  • Pixabay-Vektorgrafiken werden als 1280px-Raster-PNG heruntergeladen, normalerweise mit transparentem Hintergrund. Die SVG-Quelldatei ist mit einem kostenlosen Key nicht verfügbar. Wenn der Benutzer eine echte skalierbare SVG benötigt, gib ihm die page_url des Ergebnisses, damit er sie manuell von Pixabay herunterladen kann, und erkläre warum.
  • Pexels hat nur Fotos und Live-Aufnahmen-Videos. Die Suche nach Illustrationen, Vektorgrafiken und Animationen erfolgt ausschließlich bei Pixabay.
  • Pexels-Videos gehen bis zu 4K. Pixabay-Videos enden normalerweise bei 1920x1080 und haben nur manchmal 4K. Die Pixabay-Videosuche hat keinen Orientierungsfilter, daher filtert das Skill nach der Suche anhand von Breite und Höhe.
  • Für die Komprimierung und das Posterbild wird ffmpeg im PATH benötigt.
  • Pixabay gibt maximal 500 Ergebnisse pro Abfrage zurück und erlaubt 100 Anfragen pro 60 Sekunden. Pexels erlaubt 200 Anfragen pro Stunde und 20.000 pro Monat.
  • Pixabay-largeImageURL-Links laufen nach etwa 24 Stunden ab und dürfen nicht verlinkt werden. Lade die Datei immer in das Projekt herunter.
  • Der Pixabay-Key wird in der URL-Abfragezeichenfolge übergeben. Gib niemals eine Pixabay-Such-URL aus oder protokolliere sie.

Informiere den Benutzer, wenn eine dieser Grenzen die Auswahl beeinflusst hat, z. B. "dieses Pixabay-Bild ist 1280px breit, daher wird es als vollbreites Hero-Bild weich aussehen."

UTF-8 ausgeben, sonst stirbt das Skript an einem Fotografennamen

Jedes Python-Skript hier gibt die Namen der Fotografen und Hochlader aus, und Python unter Windows schreibt stdout als cp1252. Die Namen stammen aus aller Welt: Das erste ş, ü, ż oder ñ löst UnicodeEncodeError: 'charmap' codec can't encode character mitten in der Ausgabe aus, sodass die Suche halb gelingt und dann abstürzt.

Setze die Kodierung als erstes in jedem Skript, vor den Imports, die etwas ausgeben:

import sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')

Wird das Skript stattdessen von Bash aus ausgeführt? Setze PYTHONIOENCODING=utf-8 vor den Befehl. Beides funktioniert; keines von beidem führt dazu, dass es beim ersten nicht-englischen Namen fehlschlägt.

API-Keys

Die Einrichtung (SETUP.md in https://github.com/dtsoden/stock-media-skill) speichert die beiden Keys und vermerkt den Speicherort in ~/.config/stock-media/config.json. Wenn diese Datei fehlt und scripts/check_keys.py fehlschlägt, wurde das Skill nicht eingerichtet: Verweise den Benutzer auf diese Einrichtung und brich ab.

Gib niemals einen Key aus, schreibe niemals einen in das Projekt, lese niemals eine Key-Datei und bitte den Benutzer niemals, einen Key in den Chat einzufügen. Lade die Keys mit dem Helfer im scripts/-Ordner dieses Skills, der zuerst Umgebungsvariablen und dann den OS-Anmeldeinformationsspeicher prüft. Schreibe jedes Suchskript in ein Scratch- oder temporäres Verzeichnis (nicht das Projekt) und starte es wie folgt, wobei SKILL_DIR auf den absoluten Pfad des Ordners gesetzt ist, der diese SKILL.md enthält:

import os, sys
SKILL_DIR = '/absolute/path/to/stock-media'  # der Ordner, der diese SKILL.md enthält
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')

Wie das Skript ausgeführt wird, hängt vom store-Wert in der Konfigurationsdatei ab:

  • keyring oder env: Führe es normal aus, python search.py.
  • aihsm: Injiziere die Keys mit den Tresornamen aus aihsm_names: aihsm run --set PEXELS_API_KEY=<pexels name> --set PIXABAY_API_KEY=<pixabay name> -- python search.py. Führe python direkt als den injizierten Befehl aus. Ein Verpacken in bash -c '...' verliert die Variablen.

python scripts/check_keys.py (durch aihsm, wenn dies der Store ist) bestätigt, dass beide Keys funktionieren, ohne sie auszugeben. Wenn deine Sandbox den Netzwerkzugriff oder den OS-Anmeldeinformationsspeicher blockiert, bitte den Benutzer, dies für diese Skripte zu erlauben.

Der Pixabay-Key wird in der Abfragezeichenfolge übertragen, daher gib niemals eine Pixabay-Such-URL aus oder protokolliere sie. Bild- und Video-URLs von beiden Diensten sind sicher auszugeben.

KRITISCH: Einen Browser-User-Agent senden

Die Pexels-API und das CDN liegen hinter Cloudflare, das den Standard-Python-urllib und Bot-User-Agents mit HTTP 403, error code: 1010 blockiert. Sende einen echten Browser-User-Agent bei jeder Anfrage an beide Dienste, sowohl für Suchanfragen als auch für Bilddownloads:

UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'

Argumente

Die Anfrage beschreibt, welche Bilder benötigt werden und wie sie verwendet werden sollen. Dies kann sein:

  • Eine natürlichsprachliche Beschreibung der benötigten Bilder (z. B. "Hero-Hintergrund für eine Fintech-App", "Teamfotos für die Über-Seite", "Produktpräsentationsbilder")
  • Eine spezifische Suchanfrage
  • Ein Designkontext, in dem du bestimmst, welche Bilder das UI/UX verbessern würden

Arbeitsablauf

1. Den Designkontext verstehen

  • Lese die Zielkomponenten-/Seitendateien, um die visuelle Designsprache, Farbpalette, das Layout und den Zweck zu verstehen
  • Bestimme, welche Rolle das/die Bild(er) spielen wird/werden: Hero, Hintergrund, Illustration, Karten-Thumbnail, Avatar, Textur usw.
  • Ermittle die benötigten Abmessungen (randlos, Karte, Symbol usw.). Dies entscheidet, ob Pixabay überhaupt verwendet werden kann, siehe die Größenbeschränkung unten.

2. Beide Dienste durchsuchen

Eine Funktion durchsucht beide und gibt Ergebnisse in einer gemeinsamen Form zurück, sodass Kandidaten von beiden nebeneinander verglichen werden können:

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'  # der Ordner, der diese SKILL.md enthält
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, or a hex code like 'ffffff'"""
    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
    Query is limited to 100 characters."""
    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' searches both services. 'illustration' or 'vector'
    searches Pixabay only, because Pexels has photos and nothing else."""
    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:
            # One service failing should not sink the search.
            print(f'search failed: {type(e).__name__} {getattr(e, "code", "")}')
    return results

Illustrationen und Vektorgrafiken

Pixabay hat auch Illustrationen und Vektorgrafiken, daher deckt search_all(..., image_type='illustration') Spot-Illustrationen, Leerstandsgrafiken und Icons mit Charakter ab, für die ein Foto nicht geeignet wäre. Eine Illustrationssuche gibt auch Vektorelemente zurück (type kommt als vector/svg zurück).

Dieser Key erhält nicht die SVG-Quelldatei. Vektorgrafiken werden als 1280px-PNG heruntergeladen, oft mit transparentem Hintergrund. download_and_optimize behält die Transparenz im WebP bei. Wenn der Benutzer eine echte skalierbare SVG benötigt, sage dies und verlinke die page_url, damit er sie manuell von Pixabay herunterladen kann.

Die color-Werte unterscheiden sich zwischen den Diensten. Übergebe einen, den beide akzeptieren (red, orange, yellow, green, turquoise, blue, pink, white, gray, black, brown) oder rufe jede Funktion separat auf.

Führe mehrere Suchen mit variierten Suchbegriffen durch. Bewerte die Kandidaten aus beiden Diensten gemeinsam:

  • Größe: max_download_width muss den Anwendungsfall abdecken. Pixabay endet bei 1280px, daher kommen für ein 1920px-Hero-Bild nur Pexels-Ergebnisse in Frage.
  • Farbe: Pexels liefert avg_color. Bei Pixabay schaue auf die Vorschau.
  • Motiv: Pexels description ist der Alt-Text, Pixabay's ist eine Liste von Tags.
  • Popularität: Pixabay-Likes und -Downloads sind ein grobes Qualitätssignal. Ein stark heruntergeladenes Bild taucht auch eher auf anderen Websites auf.
  • Anschauen: Lade die preview_url der in die engere Wahl gezogenen Kandidaten in den Zwischenspeicher herunter und betrachte sie, bevor du eine Auswahl triffst. Textmetadaten allein treffen eine schlechte Auswahl.

search_pixabay entfernt bereits Ergebnisse, die als isAiGenerated oder isLowQuality markiert sind.

3. Herunterladen & Optimieren

Lade das ausgewählte Bild herunter und speichere es als optimiertes WebP. Dieselbe Funktion behandelt beide Quellen:

from PIL import Image
from io import BytesIO

def download_and_optimize(photo, output_name, output_dir, max_width=1920, quality=75):
    """
    photo: one result dict from search_all / search_pexels / search_pixabay
    output_name: filename without extension
    output_dir: where to save (e.g. public/images/)
    max_width: resize to this max width (keeps aspect ratio)
    quality: WebP quality 1-100. Guidelines:
        - Hero/showcase images: 75-85
        - Card thumbnails: 65-75
        - Background/texture images: 40-55
        - Decorative/barely-visible: 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'  note: source is only {img.width}px wide, asked for {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:,} bytes ({(1-new_size/orig_size)*100:.0f}% smaller)')
    print(f'  Source: {photo["source"]} {photo["page_url"]}')
    print(f'  By: {photo["author"]}')
    return webp_path

Pixabays largeImageURL-Links laufen nach etwa 24 Stunden ab und dürfen nicht verlinkt werden, daher lade die Datei immer herunter und serviere sie aus dem Projekt.

4. Integrieren

  • Speichere Bilder im Bilder-Verzeichnis des Projekts (erkenne es anhand der Projektstruktur, normalerweise public/images/, static/, assets/, src/assets/ usw.)
  • Referenziere Bilder im Code mit dem korrekten Pfad für das Framework
  • Stelle sicher, dass Pillow installiert ist (pip install Pillow), bevor die Verarbeitung beginnt
  • Teile dem Benutzer mit, von welchem Dienst und von welcher Seite jedes ausgewählte Bild stammt

Video

Beide Dienste haben Stockvideos, und die Videosuche durchsucht immer beide. Verwende sie für Hero-Hintergrundschleifen und kurze Produkt- oder Stimmungsclips. Jede Datei von beiden Diensten ist MP4. Pixabay unterteilt seine Videos in film (Live-Aufnahmen) und animation (Bewegungsgrafiken, abstrakte Schleifen).

Größen, die beim Testen am 15.10.2026 gesehen wurden: Pexels aus

…

[Gekürzt für die Übersetzung]

Videosuche und -optimierung

Die Videosuche durchsucht beide Dienste und wählt das beste aus. Die Videoauswahl bewertet Kandidaten anhand von Auflösung, Dauer und Qualität.

Nach dem Herunterladen wird das Video mit ffmpeg komprimiert. Falls verfügbar, wird ein WebP-Posterbild erstellt:

import subprocess

def download_and_optimize_video(video, output_name, output_dir, max_width=1920, bitrate='2M'):
    """
    video: ein Ergebnis-Wörterbuch von search_videos (siehe unten)
    output_name: Dateiname ohne Erweiterung
    output_dir: Zielverzeichnis (z. B. public/videos/)
    max_width: maximale Breite (Seitenverhältnis beibehalten)
    bitrate: ffmpeg-Videobitrate, z. B. '1M', '2M', '5M'
    """
    os.makedirs(output_dir, exist_ok=True)

    # Video herunterladen
    url = video['download_url']
    req = urllib.request.Request(url, headers={'User-Agent': UA})
    with urllib.request.urlopen(req) as resp:
        video_data = resp.read()

    temp_path = os.path.join(output_dir, f'{output_name}_temp.mp4')
    with open(temp_path, 'wb') as f:
        f.write(video_data)

    # Video mit ffmpeg komprimieren und skalieren
    output_path = os.path.join(output_dir, f'{output_name}.mp4')
    cmd = [
        'ffmpeg', '-i', temp_path,
        '-vf', f'scale={max_width}:-2',
        '-b:v', bitrate,
        '-c:v', 'libx264',
        '-preset', 'medium',
        '-movflags', '+faststart',
        '-y', output_path
    ]
    subprocess.run(cmd, check=True, capture_output=True)

    # WebP-Posterbild aus dem ersten Frame erstellen
    poster_path = os.path.join(output_dir, f'{output_name}_poster.webp')
    poster_cmd = [
        'ffmpeg', '-i', temp_path,
        '-vframes', '1',
        '-vf', f'scale={max_width}:-2',
        '-c:v', 'libwebp',
        '-quality', '75',
        '-y', poster_path
    ]
    subprocess.run(poster_cmd, check=True, capture_output=True)

    # Temporäre Datei löschen
    os.remove(temp_path)

    orig_size = len(video_data)
    new_size = os.path.getsize(output_path)
    print(f'{output_name}.mp4: {new_size:,} bytes ({(1-new_size/orig_size)*100:.0f}% kleiner)')
    print(f'  Quelle: {video["source"]} {video["page_url"]}')
    print(f'  Poster: {poster_path}')
    return output_path, poster_path

Kritische Regeln

  1. Kein Hotlinking: Lade Dateien immer herunter und diene sie lokal. Verlinke niemals direkt auf largeImageURL oder andere Remote-URLs.
  2. Browser-User-Agent: Verwende immer einen echten Browser-User-Agent, um HTTP 403 von Cloudflare zu vermeiden.
  3. API-Keys schützen: Gib niemals API-Keys im Chat, in Dateien oder in der Ausgabe aus. Verwende den Key-Helper.
  4. UTF-8-Kodierung: Setze die stdout-Kodierung immer auf UTF-8, bevor du irgendetwas ausgibst.
  5. Größenbeschränkungen: Überprüfe immer die max_download_width gegen die Anforderungen des Designs. Informiere den Benutzer über Einschränkungen.
  6. Transparenz: Gib nach jedem Download die Quelle, den Fotografen und alle relevanten Details aus.

Verwandte Skills