用 Python + 本地视觉模型(或大模型API),批量为图片文件夹生成描述、标签和SEO关键词,告别手动写 alt 标签的痛苦。
你有没有这种经历?给网站加了 200 张图片,每张图片的 alt 标签和描述都要手动写。写了一张就后悔,不想写了。
或者用 AI 工具一张张生成,效率低得离谱。
今天做一个批量图片描述生成器。一个文件夹扔进去,全自动生成描述文本、关键词标签、适合 SEO 的标题。跑在本地,不用上传到任何云服务。
项目背景
图片 alt 标签对 SEO 很重要。搜索引擎看不懂图片内容,全靠这段文字理解。但手动写 alt 标签很费时——尤其你有几百张图片的时候。
市面上的批量工具要么要上传到云端(隐私风险),要么收费。今天这个项目用 Python + 本地视觉模型,完全本地运行,免费无限制。
技术选型
| 组件 | 选择 | 理由 |
|---|---|---|
| 视觉模型 | Ollama + llava | 本地运行,无需 API 费用 |
| 备选方案 | OpenAI GPT-4V API | 精度更高,有付费需求可选 |
| 语言 | Python 3.10+ | 生态丰富,库多 |
| 图像处理 | Pillow | 图片压缩、格式转换 |
| 输出格式 | JSON + CSV | 方便导入内容管理系统 |
| 批量处理 | concurrent.futures | 多线程并行,速度翻倍 |
Ollama 是最轻量级的选择。拉一个 llava 模型(约 4.7GB),就能在本地做图片理解。如果不想装模型,也可以用 GPT-4V 的兼容 API,按次付费。
实现步骤
第一步:安装 Ollama 和视觉模型
先去 ollama.com 下载安装。装好后:
# 拉取视觉模型(首次约 5 分钟,4.7GB)
ollama pull llava:7b
# 验证安装
ollama list
如果你想用 API 方案,跳过这步,直接装 Python 依赖。
第二步:安装 Python 依赖
pip install pillow requests openai httpx
如果是 API 方案,还需要:
pip install python-dotenv
第三步:编写核心脚本 image_describer.py
"""
AI 图片描述批量生成器
支持本地 Ollama 视觉模型和 OpenAI 兼容 API
"""
import os
import json
import csv
import time
from pathlib import Path
from PIL import Image
import requests
import base64
from concurrent.futures import ThreadPoolExecutor, as_completed
# ============ 配置区 ============
# 模式:'local' 用 Ollama 本地模型,'api' 用云端 API
MODEL_MODE = os.getenv("MODEL_MODE", "local")
# Ollama 配置
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llava:7b")
# OpenAI 兼容 API 配置(可选)
API_KEY = os.getenv("API_KEY", "")
API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
API_MODEL = os.getenv("API_MODEL", "gpt-4o-mini")
# 输出设置
OUTPUT_DIR = "./output"
LANGUAGE = os.getenv("LANGUAGE", "zh") # zh 中文, en 英文
MAX_IMAGE_SIZE = (1920, 1080) # 最大尺寸,超过会自动缩放
# 描述风格配置
DESC_STYLES = {
"seo": "请用中文生成3个SEO友好的图片描述,每个不超过150字,包含核心关键词。",
"alt": "请用简洁的中文描述这张图片的主要内容,适合用作alt标签,不超过50字。",
"tags": "请从这张图片中提取5-8个关键词标签,用逗号分隔,只输出标签本身。",
"title": "请用中文生成一个吸引人的图片标题,不超过20字。",
}
# ============ 核心函数 ============
def compress_image(image_path, max_size=MAX_IMAGE_SIZE):
"""压缩图片,保持宽高比"""
img = Image.open(image_path)
if img.size[0] > max_size[0] or img.size[1] > max_size[1]:
img.thumbnail(max_size, Image.Resampling.LANCZOS)
out_path = image_path.replace(".", "_compressed.")
img.save(out_path, quality=85)
return out_path
return image_path
def image_to_base64(image_path):
"""将图片转为 base64 编码"""
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def describe_with_ollama(image_path, prompt):
"""使用 Ollama 本地模型生成描述"""
img_base64 = image_to_base64(image_path)
payload = {
"model": OLLAMA_MODEL,
"prompt": prompt,
"images": [img_base64],
"stream": False,
}
try:
resp = requests.post(
f"{OLLAMA_HOST}/api/generate",
json=payload,
timeout=120
)
resp.raise_for_status()
return resp.json().get("response", "").strip()
except Exception as e:
return f"[错误] {str(e)}"
def describe_with_api(image_path, prompt):
"""使用 OpenAI 兼容 API 生成描述"""
img_base64 = image_to_base64(image_path)
payload = {
"model": API_MODEL,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{img_base64}"
}
}
]
}
],
"max_tokens": 300,
}
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
try:
resp = requests.post(
f"{API_BASE_URL}/chat/completions",
headers=headers,
json=payload,
timeout=60
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"].strip()
except Exception as e:
return f"[错误] {str(e)}"
def generate_descriptions(image_path, style="seo"):
"""为单张图片生成指定风格的描述"""
# 压缩图片
compressed_path = compress_image(image_path)
prompt = DESC_STYLES.get(style, DESC_STYLES["seo"])
if MODEL_MODE == "local":
result = describe_with_ollama(compressed_path, prompt)
else:
result = describe_with_api(compressed_path, prompt)
# 清理结果
if result.startswith("[错误]"):
return result
# 去除多余空白
return " ".join(result.split())
def process_single_image(img_path, styles=None):
"""处理单张图片,生成多种描述"""
if styles is None:
styles = ["seo", "alt", "tags", "title"]
result = {
"filename": img_path.name,
"path": str(img_path),
"descriptions": {}
}
for style in styles:
desc = generate_descriptions(str(img_path), style)
result["descriptions"][style] = desc
# 避免请求过快
time.sleep(0.3)
return result
def batch_process(input_dir, output_dir=OUTPUT_DIR, max_workers=3):
"""批量处理图片目录"""
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# 支持的图片格式
image_extensions = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"}
images = [
f for f in input_path.iterdir()
if f.suffix.lower() in image_extensions
]
if not images:
print(f"❌ 在 {input_dir} 中未找到图片文件")
return
print(f"📷 找到 {len(images)} 张图片,开始处理...")
print(f"⚙️ 使用模式: {'本地 Ollama' if MODEL_MODE == 'local' else '云端 API'}")
print()
all_results = []
# 多线程并行处理
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {
executor.submit(process_single_image, img): img
for img in images
}
completed = 0
for future in as_completed(futures):
completed += 1
img_name = futures[future].name
try:
result = future.result()
all_results.append(result)
print(f" [{completed}/{len(images)}] ✅ {img_name}")
except Exception as e:
print(f" [{completed}/{len(images)}] ❌ {img_name}: {e}")
# 保存结果
save_results(all_results, output_path)
print(f"\n✅ 处理完成!结果已保存到 {output_path}")
def save_results(results, output_dir):
"""保存结果到 JSON 和 CSV"""
# JSON 格式
json_path = output_dir / "descriptions.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"📄 已保存 JSON: {json_path}")
# CSV 格式(方便导入 Excel/内容管理系统)
csv_path = output_dir / "descriptions.csv"
with open(csv_path, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
writer.writerow(["文件名", "SEO描述", "Alt标签", "关键词", "标题"])
for r in results:
writer.writerow([
r["filename"],
r["descriptions"].get("seo", ""),
r["descriptions"].get("alt", ""),
r["descriptions"].get("tags", ""),
r["descriptions"].get("title", ""),
])
print(f"📊 已保存 CSV: {csv_path}")
# ============ 命令行入口 ============
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("用法:")
print(" python image_describer.py <图片目录> # 批量处理")
print(" python image_describer.py <图片路径> # 单张图片")
print()
print("环境变量:")
print(" MODEL_MODE = local|api (默认: local)")
print(" OLLAMA_HOST = http://localhost:11434")
print(" OLLAMA_MODEL = llava:7b")
print(" API_KEY = 你的API密钥")
print(" API_BASE_URL = https://api.openai.com/v1")
print(" LANGUAGE = zh|en (默认: zh)")
sys.exit(1)
target = sys.argv[1]
target_path = Path(target)
if target_path.is_dir():
batch_process(target)
elif target_path.is_file():
result = process_single_image(target_path)
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(f"❌ 找不到路径: {target}")
第四步:运行测试
准备一个图片文件夹,比如 test_images/:
mkdir test_images
# 把图片复制进去
python image_describer.py test_images/
如果用的是本地 Ollama 模式,首次处理会稍慢(模型需要冷启动),后续图片会快很多。
运行效果
处理完成后的输出目录结构:
output/
├── descriptions.json # 完整数据(所有字段)
└── descriptions.csv # 表格格式(方便导入)
CSV 内容示例:
文件名,SEO描述,Alt标签,关键词,标题
product_01.jpg,这款智能手表具备心率监测、睡眠追踪和7天续航功能,适合运动爱好者日常佩戴。,智能手表 心率监测 运动健身,智能手表,心率监测,运动健身,续航能力强,产品外观时尚,这款智能手表:运动与生活的全能助手
hero_banner.jpg,现代办公环境中的开放式_workspace,配有双显示器和绿植装饰,营造舒适高效的工作氛围。,办公环境 双显示器 绿植 开放式办公,办公环境,双显示器,绿植,开放式办公,效率提升,现代简约办公空间
优化方向
这个基础版本已经能用了,想更实用可以加几样东西:
关键词去重:批量处理时,相同类型的图片会有重复关键词。加一个聚类步骤,合并相似结果。
文件名关联:根据文件名自动推断内容,比如
product_01.jpg自动加"产品"前缀提示。Web 界面:用 Streamlit 搭一个拖拽上传的界面,可视化查看结果,支持手动修改。
集成 CMS:直接导出为 WordPress、Typecho 等平台的导入格式,一键批量上传。
模型对比:同时跑 Ollama 和 API 两种方案,对比效果,选择最适合的。
多语言支持:配置 LANGUAGE 环境变量,一键生成中英文双语描述。
图片 alt 标签这种重复劳动,花半小时写个脚本,以后每次换图片都不用再纠结了。
你的网站有多少张图片需要写描述?打开文件夹数一数就知道了。
这篇文章对你有帮助吗?