库存媒体获取(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 关闭
…
[为翻译截断]