用 Python 批量处理商品图:抠图、高清放大与图生视频(同步 API 完整示例)

商品图片批量处理通常包含三个阶段:去背景、提高分辨率,再选择部分图片生成展示视频。本文用 requests 和线程池实现一条可选阶段的处理流水线,重点讨论同步请求、有限并发和失败后的恢复。

一、同步接口如何串联

示例使用 ModelFold 的正式站接口:图片处理调用 POST https://ai.modelfold.online/v1/images/generations,视频调用 POST https://ai.modelfold.online/v1/videos/generations。两者都使用 Authorization: Bearer API_KEY 鉴权,请求体包含 model、prompt 和 params。

客户端等待当前请求处理完成,成功响应的 ok 为 true,从 data0.url 取得结果地址;usage.credits 是本次扣除的积分。HTTP 错误或 ok 不为 true 都不能当作成功。这里不使用任务 ID、轮询或回调。串联时把上一阶段返回的 URL 交给下一阶段,每阶段独立计费。

二、输入必须是可访问的图片地址

准备一个 UTF-8 文本文件 urls.txt,每行一个公网 HTTP(S) 图片 URL。地址应当允许服务端直接下载,不需要浏览器登录、Cookie 或防盗链授权。本地路径不能直接填入图片字段;本示例不包含文件上传接口。不要使用包含私人凭据的地址,也不要提交无权处理的素材。

阶段 model params 字段
抠图 recraft/remove-background image:单个图片 URL
放大 recraft/crisp-upscale image:单个图片 URL
图生视频 grok-imagine/image-to-video image_urls:URL 数组;mode、duration、resolution

prompt 位于请求体顶层;图片工具也传入非空 prompt,以满足网关校验。视频示例选择单图、normal、6 秒、480p。不同模型的图片字段并不相同,不能把 image 与 image_urls 随意互换。输入尺寸、格式和体积应符合对应模型文档。

三、完整代码

要求 Python 3.9 及以上。保存为 batch_images.py,通过环境变量提供 Key,勿将真实值写进源码或提交仓库。每次使用新的输出目录。下载按响应 Content-Type 选择文件后缀,不进行格式转换。

bash 复制代码
python -m pip install requests
# 先在当前终端设置 MODELFOLD_API_KEY
python batch_images.py urls.txt --steps remove --workers 1 --output results-remove
# 单张验收并确认预算后,再选择串联;视频会另行计费
python batch_images.py urls.txt --steps remove,upscale,video --workers 2 --output results-pipeline
python 复制代码
import argparse
import json
import os
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

import requests

BASE = 'https://ai.modelfold.online'
MODELS = {'remove': 'recraft/remove-background',
          'upscale': 'recraft/crisp-upscale',
          'video': 'grok-imagine/image-to-video'}


def generate(session, step, url, prompt, timeout):
    params = {'image': url}
    if step == 'video':
        params = {'image_urls': [url], 'mode': 'normal',
                  'duration': 6, 'resolution': '480p'}
    endpoint = 'videos' if step == 'video' else 'images'
    # 同步 POST 不自动重试,超时不代表服务端未处理。
    response = session.post(
        BASE + '/v1/' + endpoint + '/generations',
        json={'model': MODELS[step], 'prompt': prompt, 'params': params},
        timeout=(10, timeout))
    response.raise_for_status()
    body = response.json()
    if body.get('ok') is not True:
        raise RuntimeError('generation failed')
    result = body['data'][0]['url']
    if not isinstance(result, str) or not result.startswith(('https://', 'http://')):
        raise ValueError('invalid result URL')
    return result, body.get('usage', {})


def process(item, args):
    index, source = item
    folder = Path(args.output) / str(index)
    folder.mkdir(parents=True, exist_ok=False)
    record = {'source': source, 'stages': []}
    journal = folder / 'result.json'
    try:
        with requests.Session() as session:
            session.headers['Authorization'] = 'Bearer ' + os.environ['MODELFOLD_API_KEY']
            url = source
            for step in args.steps:
                url, usage = generate(session, step, url, args.prompt, args.timeout)
                record['stages'].append({'step': step, 'url': url, 'usage': usage})
                journal.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding='utf-8')
                # 独立请求下载,不把 API Key 发送给文件存储域名。
                with requests.get(url, stream=True, timeout=(10, 120)) as response:
                    response.raise_for_status()
                    mime = response.headers.get('Content-Type', '').split(';')[0]
                    ext = {'image/png': '.png', 'image/jpeg': '.jpg',
                           'image/webp': '.webp', 'video/mp4': '.mp4'}.get(mime, '.bin')
                    target = folder / (step + ext)
                    partial = target.with_suffix(target.suffix + '.part')
                    with partial.open('wb') as file:
                        for chunk in response.iter_content(1024 * 1024):
                            file.write(chunk)
                partial.replace(target)
        record['status'] = 'success'
        return True
    except Exception as exc:
        record['status'] = 'error'
        record['errorType'] = type(exc).__name__
        return False
    finally:
        journal.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding='utf-8')
        print(index, record['status'])


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('input', type=Path, help='UTF-8 文件,每行一个公网图片 URL')
    parser.add_argument('--output', default='results')
    parser.add_argument('--steps', default='remove')
    parser.add_argument('--workers', type=int, default=1)
    parser.add_argument('--timeout', type=int, default=1200)
    parser.add_argument('--prompt', default='Slow camera push in, stable product shape.')
    args = parser.parse_args()
    args.steps = args.steps.split(',')
    if (any(s not in MODELS for s in args.steps) or args.workers < 1
            or args.timeout < 1 or not os.environ.get('MODELFOLD_API_KEY')):
        parser.error('check steps, workers, timeout and MODELFOLD_API_KEY')
    if 'video' in args.steps[:-1]:
        parser.error('video must be the last step')
    urls = [s.strip() for s in args.input.read_text(encoding='utf-8').splitlines() if s.strip()]
    if not urls or any(not s.startswith(('https://', 'http://')) for s in urls):
        parser.error('provide public HTTP(S) image URLs')
    # 每次使用新的输出目录,避免误覆盖旧结果。
    Path(args.output).mkdir(parents=True, exist_ok=False)
    with ThreadPoolExecutor(max_workers=args.workers) as pool:
        results = list(pool.map(lambda item: process(item, args), enumerate(urls, 1)))
    print('success:', sum(results), 'failed:', len(results) - sum(results))
    raise SystemExit(0 if all(results) else 1)


if __name__ == '__main__':
    main()

四、超时后为什么不自动重发

同步接口会在模型处理期间占用连接,因此需要足够长的读取超时。requests 的读取超时表示等待响应数据的时间,不是严格的整条流水线总时限。客户端超时或断开也不能证明服务端停止处理。

POST 响应丢失时,服务端可能已经受理并扣费,立即重发可能重复处理。示例不自动重试生成请求。遇到超时,应先在控制台核对调用记录与扣费,再决定是否重新调用。参数错误、图片不可访问或鉴权失败,应先修正原因。

每阶段成功后先把结果 URL 和 usage 写入 result.json,再下载文件。如果下载失败,优先复用已记录的 URL 下载,不要重新调用模型。下载使用 .part 临时文件,写完后改名;中断的临时文件不可当作完整结果。该示例没有自动恢复功能,整批重跑仍会再次生成、再次计费。

五、控制并发与保护 Key

workers=2 表示最多两条图片流水线同时执行,一张图片内部按阶段顺序处理。线程池限制并发,不等同于每秒请求限额;遇到 429 应降低并发并检查平台限制,不应无限重试。每个线程使用独立 Session,鉴权只发给 API 域名;结果下载使用独立的无鉴权请求。

成功获取结果后及时下载,结果 URL 可能过期。日志中的素材地址与输出地址同样属于业务数据,不应直接公开。结果还需要人工验收:透明通道、细节、文字与商品形状都可能发生变化,接口成功不等于素材可以直接发布。

首次仅跑一张抠图并检查 result.json、实际积分和下载结果,再扩大数量。本文抠图函数已于 2026-10-25 单次实测,成功取得结果 URL,实际扣除 2 积分,结果下载返回 HTTP 200;视频示例只核对接口与字段,不代表已经实测出片。生产环境还应限制下载体积、检查实际文件类型,并将阶段状态持久化到数据库。

接口说明:同步接口入门。模型输入约束可参考抠图参数、放大参数、图生视频参数。模型参数页中的上游任务协议与本文同步网关不同,对外调用路径和响应格式以同步接口说明及本文示例为准。

相关推荐
小小张说故事2 小时前
Python 拼字符串为什么别在循环里用 +=?拆开 join 的两遍账:191.8 微秒与 6.8 微秒
python
CodeGL2 小时前
15、Python 访问数据库
数据库·python
用户298698530142 小时前
Python 实现 Markdown 与 Word 文档互转
python·api·markdown
EnCi Zheng2 小时前
AI Agent工具调用防护层怎么写-一份能直接跑的Python四层拦截实现
大数据·开发语言·python
2601_949847752 小时前
Cookie 跨端三方库仓颉适配全流程
python·django·flask
万粉变现经纪人2 小时前
如何解决 pip install 安装报错 检测到 yanked 版本需更换可用版本 问题
人工智能·python·scrapy·numpy·pandas·pyqt·pip
香蕉大香蕉(06敬畏市场)2 小时前
高频八股 · Java 并发编程(10):异步、ThreadLocal 与虚拟线程(收官)
java·开发语言·网络·jvm·python·ai·实习
JustNow_Man2 小时前
【Harness】本地文本向量化搜索解决方案--qmd
git·python·算法
小林ixn2 小时前
用 FastAPI + SQLAlchemy 从零手搓一个博客系统:一套能落地、能讲清楚的 RESTful 后端
后端·python·fastapi