LingBot-Vision 批量图像描述生成的 Python 脚本实现

文章导读
处理大量图片时,逐个调用 LingBot-Vision 接口生成描述确实低效,而且一旦网络抖动或接口限流,中间失败的数据很容易丢。建议先做一个带唯一图片 ID 的输入清单,再用线程池控制并发,把每次请求的结果和失败项都落到本地 JSON 或 CSV 里,这样整批任务即使中断,也能从上次断点继续跑。下面给出一个不绑定具体 SDK 的 Python 脚本骨架,所有接口地址、模型名和密钥都留成可替换配置
📋 目录
  1. 读取待处理图片列表与元数据
  2. 构造图像描述请求
  3. 并发调用与限流控制
  4. 结果持久化与异常重试
A A

处理大量图片时,逐个调用 LingBot-Vision 接口生成描述确实低效,而且一旦网络抖动或接口限流,中间失败的数据很容易丢。建议先做一个带唯一图片 ID 的输入清单,再用线程池控制并发,把每次请求的结果和失败项都落到本地 JSON 或 CSV 里,这样整批任务即使中断,也能从上次断点继续跑。下面给出一个不绑定具体 SDK 的 Python 脚本骨架,所有接口地址、模型名和密钥都留成可替换配置。

适用场景:本地或服务器上有一批图片,需要调用 LingBot-Vision 这类 OpenAI 兼容的视觉接口生成描述。操作动作:先整理图片清单,构造 base64 或文件 URL 的请求,用线程池限制并发,并对失败项做带退避的重试。验证方式:先跑 3-5 张图片确认描述内容正确,再放开全量任务;中途中断后重新执行脚本,通过已处理记录跳过已完成项。风险边界:图片过大或并发过高可能导致接口超时,重试次数和最大并发数需要结合具体接口限制调整,不能当作无限提速的手段。

读取待处理图片列表与元数据

批量处理的第一步不是调用接口,而是把输入整理成可追踪的清单。每张图片需要一个稳定的唯一标识,建议用相对路径或文件哈希,避免重名文件互相覆盖。下面的函数会遍历指定目录,过滤出 .jpg.jpeg.png.webp 等常见格式,输出包含 idpath 的字典列表。

import os
import hashlib

IMAGE_EXTS = {'.jpg', '.jpeg', '.png', '.webp', '.bmp'}

def load_images(image_dir):
    items = []
    for root, _, files in os.walk(image_dir):
        for name in sorted(files):
            ext = os.path.splitext(name)[1].lower()
            if ext not in IMAGE_EXTS:
                continue
            path = os.path.join(root, name)
            # 用相对路径做 ID,保证唯一且可读
            rel = os.path.relpath(path, image_dir)
            with open(path, 'rb') as f:
                digest = hashlib.md5(f.read()).hexdigest()[:8]
            items.append({'id': f'{rel}:{digest}', 'path': path})
    return items

if __name__ == '__main__':
    for item in load_images('./images'):
        print(item['id'], item['path'])

这里把文件内容哈希拼进 ID,主要是防止相同路径下图片被替换后结果仍然沿用旧描述。如果图片目录很大,计算 MD5 会占用额外时间,你可以只保留相对路径;但需要确认目录内不会出现同名覆盖的情况。

构造图像描述请求

LingBot-Vision 如果兼容 OpenAI 的 Chat Completions 接口,那么请求体通常包含 modelmessagesmax_tokens。图片有两种常见传法:一是将图片转成 base64 放在 image_url 的 data URL 里;二是传一个可访问的文件 URL。base64 方式适合本地文件,但会增加请求体积;文件 URL 方式更适合图片已上传到对象存储的场景。下面给出可替换的请求构造函数:

LingBot-Vision 批量图像描述生成的 Python 脚本实现
import base64
import requests

def encode_image_base64(path):
    with open(path, 'rb') as f:
        return base64.b64encode(f.read()).decode('utf-8')

def build_payload(image_path, prompt='请详细描述这张图片的内容', model='lingbot-vision'):
    b64 = encode_image_base64(image_path)
    return {
        'model': model,
        'messages': [
            {
                'role': 'user',
                'content': [
                    {'type': 'text', 'text': prompt},
                    {
                        'type': 'image_url',
                        'image_url': {
                            'url': f'data:image/jpeg;base64,{b64}'
                        }
                    }
                ]
            }
        ],
        'max_tokens': 500
    }

需要注意:image_url.url 里是否要加 detail 参数,取决于具体接口实现。有些接口要求 base64 字符串不带 data:image/jpeg;base64, 前缀,有些则要求必须带。建议先用一张小图测试接口返回,再按实际要求调整。如果图片是 PNG 或 WebP,需要把 data URL 里的 MIME 类型对应改成 image/pngimage/webp

并发调用与限流控制

并发能明显缩短总耗时,但盲目提高并发数容易触发接口限流或内存溢出。稳妥的做法是先设置一个较小的最大并发数(比如 4),跑通后再逐步调大。下面使用 ThreadPoolExecutor 实现并发,同时把请求封装成带重试的函数:遇到网络错误或 429/5xx 时,按指数退避等待后重试。

import time
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

MAX_WORKERS = 4
MAX_RETRIES = 3
API_URL = 'https://your-api-endpoint/v1/chat/completions'
API_KEY = 'your-api-key'

def call_vision_api(payload):
    headers = {'Authorization': f'Bearer {API_KEY}'}
    for attempt in range(MAX_RETRIES):
        try:
            resp = requests.post(API_URL, json=payload, headers=headers, timeout=60)
            if resp.status_code == 200:
                return resp.json()
            # 429 限流或 5xx 服务端错误时重试
            if resp.status_code in (429, 500, 502, 503):
                wait = 2 ** attempt
                time.sleep(wait)
                continue
            resp.raise_for_status()
        except requests.RequestException as e:
            if attempt == MAX_RETRIES - 1:
                raise
            time.sleep(2 ** attempt)
    raise RuntimeError(f'Failed after {MAX_RETRIES} retries')

def process_one(item, prompt):
    payload = build_payload(item['path'], prompt)
    result = call_vision_api(payload)
    try:
        description = result['choices'][0]['message']['content']
    except (KeyError, IndexError):
        raise ValueError(f'Unexpected response: {result}')
    return {'id': item['id'], 'path': item['path'], 'description': description}

主控循环里,可以先用一个小列表跑通,再跑全量。下面的示例会遍历输入项,把每个任务提交给线程池,并实时打印完成状态:

def run_batch(items, prompt):
    results = []
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        future_map = {executor.submit(process_one, item, prompt): item for item in items}
        for future in as_completed(future_map):
            item = future_map[future]
            try:
                result = future.result()
                results.append(result)
                print(f'OK: {item["id"]}')
            except Exception as e:
                print(f'FAIL: {item["id"]} - {e}')
    return results

这里把失败打印出来,但还没做持久化。真正的批处理需要把成功和失败分开记录,否则脚本一退出,内存里的结果就丢了。

LingBot-Vision 批量图像描述生成的 Python 脚本实现

结果持久化与异常重试

整批任务执行过程中,网络闪断、单张图片过大都可能导致个别请求失败。如果脚本直接退出,重新跑全量会浪费之前成功的部分。所以建议维护两个文件:一个是结果 JSON(或 CSV),一个是断点记录文件,里面保存已成功处理的图片 ID。每次成功后立刻追加写入,这样中断后再次启动时,直接跳过已完成项。

import json
import os

OUTPUT_JSON = 'results.json'
DONE_IDS_FILE = 'done_ids.json'

def load_done_ids():
    if os.path.exists(DONE_IDS_FILE):
        with open(DONE_IDS_FILE, 'r', encoding='utf-8') as f:
            return set(json.load(f))
    return set()

def append_result(result):
    records = []
    if os.path.exists(OUTPUT_JSON):
        with open(OUTPUT_JSON, 'r', encoding='utf-8') as f:
            records = json.load(f)
    records.append(result)
    with open(OUTPUT_JSON, 'w', encoding='utf-8') as f:
        json.dump(records, f, ensure_ascii=False, indent=2)

def mark_done(item_id):
    done = load_done_ids()
    done.add(item_id)
    with open(DONE_IDS_FILE, 'w', encoding='utf-8') as f:
        json.dump(sorted(done), f, ensure_ascii=False)

def run_batch_resumable(items, prompt):
    done_ids = load_done_ids()
    pending = [item for item in items if item['id'] not in done_ids]
    print(f'Total: {len(items)}, skipped: {len(items) - len(pending)}')
    for item in pending:
        try:
            result = process_one(item, prompt)
            append_result(result)
            mark_done(item['id'])
            print(f'OK: {item["id"]}')
        except Exception as e:
            # 记录失败项到单独文件,也可以直接打印后继续
            print(f'FAIL: {item["id"]} - {e}')
            with open('failed_ids.txt', 'a', encoding='utf-8') as f:
                f.write(item['id'] + '\n')

这个版本是串行循环,便于理解断点逻辑。如果希望同时保留并发和断点,可以把 run_batch_resumable 里的循环改成线程池提交,但每个任务完成后都要调用 append_resultmark_done。需要注意:多线程同时写 JSON 文件会互相覆盖,所以需要给文件写入加锁,或者改用 SQLite 这类支持并发的存储。

断点续跑的验证方式很简单:跑完一部分后直接 Ctrl+C 中断,再重新执行脚本,观察是否跳过已完成项。重试策略和并发数需要结合具体接口的限流阈值调整,没有统一最优值;建议先把 MAX_WORKERS 设为 2-4,确认稳定后再逐步增加。如果能拿到接口的速率限制响应头,可以据此动态调整退避时间,但那些数字需要在你自己的环境里实测确认,不建议照搬别人的经验值。