Inkling 批量处理截图时 / 先把图序和输出字段在脚本里对齐

文章导读
几十张截图一次性提交后,结果是一串文本,原图却按文件名排序,麻烦的地方不是识别错,而是对不上号:某条结论来自哪张图,事后查不回来。可行的做法是放量之前先在脚本里把三件事固定下来——输入目录与命名规则、图与问题的映射表、一图一行的输出记录。这样即使中途有失败重试,结果仍能按文件名回到原图。
📋 目录
  1. 一 先定好输入目录和文件命名规则
  2. 二 在脚本里维护图与问题的映射表
  3. 三 请求与返回写成一对应的记录
  4. 四 失败和超时单独落一份清单
  5. 五 用三条已知答案的样本验证对齐
A A

几十张截图一次性提交后,结果是一串文本,原图却按文件名排序,麻烦的地方不是识别错,而是对不上号:某条结论来自哪张图,事后查不回来。可行的做法是放量之前先在脚本里把三件事固定下来——输入目录与命名规则、图与问题的映射表、一图一行的输出记录。这样即使中途有失败重试,结果仍能按文件名回到原图。

适用场景:一次提交多张截图、且每张图可能配不同问题。操作动作:进入流程前给文件编号命名,用 CSV 或 JSON 映射表把文件名与问题绑定,输出逐行写回文件名和问题编号。验证方式:先用三张人工已知答案的样本跑一遍,逐行核对文件名、问题文本与返回结果是否一致。风险边界:并发数、超时与重试策略需结合所用接口的文档和环境确认,不要沿用别处默认值。

先定好输入目录和文件命名规则

截图进入流程之前就应该有唯一标识,否则后面所有对齐都无从下手。建议按“批次目录 + 固定位数序号”组织,不要直接把几十张截图堆在下载目录里跑。

shots/
  20240612-refund/
    001_order-list.png
    002_order-detail.png
    003_refund-form.png
    mapping.csv
    out/
      results.jsonl
      failures.jsonl

命名建议用「三位序号 + 业务标识」的写法:001_ 保证文件在目录里按预期排序,order-list 这类业务标识说明这张图是什么。序号位数固定,避免 2 和 10 在排序时打架;一张图配一个问题就存成一个文件,不要把多个问题塞进一张长截图,否则后面拆不开。

不要依赖接口返回结果的先后顺序来对齐。常见情况是:并发提交时返回顺序被打乱;某一条超时重试后晚到;返回体里不一定带原始文件名。只要顺序成为唯一线索,中间一条失败就会让后面全部错位,而且错得很安静——结果看着正常,只是对应错了图。

在脚本里维护图与问题的映射表

把「哪张图问什么」从临场决定变成可查的配置。建议一个批次一份 mapping.csv,和图片放在同一目录,跟着批次一起归档。

file,qid,question
001_order-list.png,Q1,这张订单列表里一共有几条待发货记录
002_order-detail.png,Q2,订单详情页显示的支付时间是什么
003_refund-form.png,Q3,退款表单里必填项有哪些

字段含义:file 是图片文件名,必须与目录中的实际文件名逐字一致,包括扩展名与大小写;qid 是问题编号,用于在输出和失败清单里引用,避免每行重复长文本;question 是这一行要提交的问题原文,写成完整句子,不要用「同上」这类依赖上下文的写法。同一张图要问多个问题时,让这张图在表里出现多行,file 相同、qid 不同,输出时按 file + qid 定位。

映射表建议存成 UTF-8,读入后先做一次存在性检查:表里有、目录里没有的文件直接报错退出,不要静默跳过。

Inkling 批量处理截图时 / 先把图序和输出字段在脚本里对齐

请求与返回写成一对应的记录

骨架很短:读映射表、逐条提交、把结果写回带文件名的那一行。发送函数先用占位实现,参数和返回结构以实际接口文档为准,替换时只改 send_one 内部即可。

import csv, json, pathlib

def send_one(image_path, question, timeout=60):
    # 占位实现:按实际接口文档替换
    # 返回 {'ok': True, 'text': '...'} 或 {'ok': False, 'error': '...'}
    raise NotImplementedError

def run(batch_dir):
    batch_dir = pathlib.Path(batch_dir)
    rows = list(csv.DictReader(open(batch_dir / 'mapping.csv', encoding='utf-8')))
    out = open(batch_dir / 'out' / 'results.jsonl', 'a', encoding='utf-8')
    for r in rows:
        img = batch_dir / r['file']
        if not img.exists():
            continue
        rec = {'file': r['file'], 'qid': r['qid'], 'question': r['question']}
        try:
            resp = send_one(img, r['question'])
            rec['ok'] = resp.get('ok', False)
            rec['text'] = resp.get('text', '')
            rec['error'] = resp.get('error', '')
        except Exception as e:
            rec['ok'] = False
            rec['error'] = type(e).__name__
        out.write(json.dumps(rec, ensure_ascii=False) + '\n')
        out.flush()
    out.close()

关键点是每一行输出都带 file 和 qid,而不是只存一段文本。写 JSONL 而不是一个大 JSON 数组,是为了中途中断也能留下可读的部分结果;每行 flush 一次,长批次跑一半失败不至于全部丢失。

失败和超时单独落一份清单

失败被静默吞掉是最常见的隐患:脚本没报错、结果条数却对不上。建议把失败单独写成一份清单,字段至少包含文件名、问题编号、失败原因、已重试次数,同时明确跳过条件。

MAX_RETRY = 3                                  # 单条最多重试次数,按接口实际情况调整
TIMEOUT = 60                                   # 单次请求超时秒数
SKIP_ON = {'FileNotFound', 'InvalidImage'}     # 这几类不重试,直接记失败

def submit_with_retry(fn, img, question):
    last = 'unknown'
    attempt = 0
    for attempt in range(1, MAX_RETRY + 1):
        try:
            resp = fn(img, question, timeout=TIMEOUT)
            if resp.get('ok'):
                return {'ok': True, 'text': resp.get('text', ''), 'retry': attempt - 1}
            last = resp.get('error', 'unknown')
            if last in SKIP_ON:
                break
        except TimeoutError:
            last = 'timeout'
        except Exception as e:
            last = type(e).__name__
            if last in SKIP_ON:
                break
        time.sleep(2 ** attempt)   # 简单退避
    return {'ok': False, 'error': last, 'retry': attempt - 1}

失败清单建议写到同一批次目录下的 out/failures.jsonl,和 results.jsonl 并列,方便一起归档。复查方式是:拿失败清单里的 file 与 mapping.csv 做差集,确认没有哪个文件既不成功、也不在失败清单里;失败条目修好后可以单独组一个补充批次重跑,qid 保持不变,再和原结果合并。

用三条已知答案的样本验证对齐

放量之前先拿三张人工已知结果的图跑一遍,数量不用多,但必须是你自己一眼能对出答案的截图。

  1. 挑选样本:建议一张内容简单(如单一列表)、一张含表格或数字、一张排版较乱。人工把答案写在临时文件里,作为核对基准。
  2. 单独跑:把这三行放进一个临时 mapping.csv,用同一个脚本跑,输出写到单独的 out/smoke/ 目录,不要和正式批次混在一起。
  3. 逐行核对三件事:results.jsonl 里的 file 是否就是该行对应的图;question 是否与 mapping.csv 中该行文本一致;text 是否与人工答案对得上。再打开原图确认一次,防止文件名相似导致看错。
  4. 数量核对:输出行数是否等于映射表行数,失败清单是否为空或每条都有明确原因。

不通过时的排查顺序:先查 mapping.csv 里的文件名与目录实际文件名是否逐字一致(大小写、扩展名、全角空格);再查脚本是不是并发提交却按返回顺序写行;然后查发送函数有没有把图片路径和问题参数接反;最后才怀疑识别本身。实际操作中,对不上号多半出在前两处,而不是识别质量。