Communitygithub.com

dtsoden/stock-media-skill

同时搜索 Pexels 和 Pixabay 获取库存照片、视频,以及 Pixabay 上的插图、矢量图和动画,挑选最佳结果,下载并优化为网页项目可用格式(图片转 WebP,视频压缩为 MP4 并生成海报图),适用于英雄图、背景视频、卡片缩略图、插图等页面所需的任何库存媒体。

stock-media-skill 是什么?

该技能(Stock Media Sourcing)是一个专为 UI/UX 设计获取库存图片和视频的 AI 代理工具。它通过同时查询 Pexels 和 Pixabay 两大免费图库,让用户能够并排比较候选结果,并从中选择最佳图片或视频。对于插图、矢量图和动画,则单独搜索 Pixabay。下载后,技能会自动将图片转换为 WebP 格式,视频则压缩为 MP4 并生成一张 WebP 海报帧,确保它们已为网页项目优化。 技能的核心优势在于其自动化的工作流程:用户只需描述设计需求(如“金融应用英雄背景”),技能便会理解上下文,搜索多个服务,评估候选结果的尺寸、颜色、主题和流行度,最终下载并集成到项目中。它巧妙地处理了两个 API 的关键限制,例如 Pixabay 的最大下载宽度为 1280 像素,Pexels 需要浏览器用户代理才能绕过 Cloudflare 的封锁,以及所有 API 密钥的安全管理。 该技能适用于 Claude Code、Cursor、Codex 等支持自定义技能或脚本的 AI 代理平台。它依赖于本地运行的 Python 脚本,需要 `Pillow` 库进行图像处理,`ffmpeg` 用于视频压缩,并安全地管理 API 密钥(通过环境变量或操作系统凭证存储)。当图片或视频因服务限制而表现不佳时,技能会主动告知用户,确保透明度。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/dtsoden/stock-media-skill/tree/HEAD/skills/stock-media

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

库存媒体获取(Pexels + Pixabay)

为 UI/UX 设计查找库存图片和视频,并以网页就绪格式保存。照片搜索会同时在 Pexels 和 Pixabay 上进行,因此您可以比较两个候选池,并从任一服务中选出最佳结果。插图和矢量搜索则仅对 Pixabay 进行。视频搜索会查询两者,并保存一个压缩的 MP4 文件以及一张 WebP 海报帧。与前端设计技能配合使用效果更佳。

搜索前须知限制

以下限制适用于 Pexels 和 Pixabay 在注册时提供的免费 API 密钥。测试于 2026-10-15。

  • Pixabay 图片下载的最大宽度为 1280 像素(largeImageURL)。全分辨率需要 Pixabay 批准的完整 API 访问权限,免费密钥不具备。对于任何需要渲染宽度超过 1280 像素的场景(例如全宽英雄图、1920px 横幅),请仅使用 Pexels 的结果。
  • Pixabay 矢量图会下载为 1280 像素的栅格 PNG,通常带有透明背景。免费密钥无法获取 SVG 源文件。如果用户需要真正的可缩放 SVG,请将结果的 page_url 提供给用户,以便他们从 Pixabay 手动下载,并说明原因。
  • Pexels 仅包含照片和实景视频。插图、矢量和动画搜索仅对 Pixabay 进行。
  • Pexels 视频可达 4K。Pixabay 视频通常最高为 1920x1080,仅偶尔有 4K。Pixabay 视频搜索没有方向筛选器,因此该技能会在搜索后根据宽度和高度进行筛选。
  • 视频处理需要 ffmpeg 在系统的 PATH 环境变量中,用于压缩和生成海报帧。
  • Pixabay 每次查询最多返回 500 个结果,并且每 60 秒允许 100 个请求。Pexels 每小时允许 200 个请求,每月允许 20,000 个。
  • Pixabay 的 largeImageURL 链接大约在 24 小时后过期,不得热链接。请始终将文件下载到项目中。
  • Pixabay 密钥会出现在 URL 查询字符串中。切勿打印或记录 Pixabay 搜索 URL。

当这些限制之一影响了选择时,请告知用户,例如“此 Pixabay 图片为 1280 像素,因此作为全宽英雄图会显得模糊。”

打印 UTF-8,否则脚本会在摄影师姓名处失败

此处的每个 Python 脚本都会打印摄影师和上传者的姓名,而 Windows 上的 Python 将 stdout 输出为 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 在 https://github.com/dtsoden/stock-media-skill)会存储两个密钥,并将位置记录在 ~/.config/stock-media/config.json 中。如果该文件缺失且 scripts/check_keys.py 执行失败,则表示该技能尚未安装:请引导用户进行安装并停止。

切勿打印密钥、将密钥写入项目、读取密钥文件,或要求用户将密钥粘贴到聊天中。使用此技能 scripts/ 文件夹中的辅助程序加载密钥,该辅助程序会先检查环境变量,然后检查操作系统凭据存储。将每个搜索脚本写入一个临时或 scratch 目录(而非项目目录),并按如下方式启动,其中 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 运行)会确认两个密钥都能正常工作,而不会打印它们。如果您的沙盒阻止了网络访问或操作系统凭据存储,请要求用户为此类脚本授权。

Pixabay 密钥会出现在查询字符串中,因此切勿打印或记录 Pixabay 搜索 URL。任一服务的图片和视频 URL 都是安全的,可以打印。

关键:发送浏览器用户代理

Pexels API 和 CDN 位于 Cloudflare 之后,Cloudflare 会阻止默认的 Python-urllib 和机器人用户代理,返回 HTTP 403,错误代码:1010。请在每次向两个服务发出的请求(无论是搜索还是图片下载)中发送真实的浏览器用户代理:

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. 搜索两个服务

一个函数同时查询两者,并以通用结构返回结果,以便来自每个服务的候选项可以并排比较:

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, 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

插图和矢量图

Pixabay 也提供插图和矢量图,因此 search_all(..., image_type='illustration') 可以覆盖重点插图、空状态艺术和带有特色的图标(照片不适用)。插图搜索也会返回矢量项目(type 返回为 vector/svg)。

此密钥无法获取 SVG 源文件。矢量图会下载为 1280 像素的 PNG,通常带有透明背景。download_and_optimize 会在 WebP 中保留透明度。如果用户需要真正的可缩放 SVG,请说明并链接 page_url,以便他们从 Pixabay 手动下载。

color 值在服务之间有所不同。传递一个两者都接受的颜色(red, orange, yellow, green, turquoise, blue, pink, white, gray, black, brown)或分别调用每个函数。

使用多种查询进行几次搜索。一起评估来自两个服务的候选项:

  • 尺寸:max_download_width 必须满足用例需求。Pixabay 最大为 1280 像素,因此对于 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 的一个结果字典
    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. 集成

  • 将图片保存到项目的图片目录(从项目结构中检测,通常是 public/images/、static/、assets/、src/assets/ 等)
  • 在代码中使用适合框架的正确路径引用图片
  • 确保在处理前安装了 Pillow(pip install Pillow)
  • 告知用户每张选定图片来自哪个服务和页面

视频

两个服务都提供库存视频,视频搜索会同时查询两者。用于英雄背景循环和简短的产品或氛围片段。来自任一服务的每个文件都是 MP4 格式。Pixabay 将其视频分为 film(实景素材)和 animation(动态图形、抽象循环)。

测试于 2026-10-15 时看到的尺寸:Pexels 关闭

…

[为翻译截断]

相关技能