AI 工具

用 CSV 比较 AI 绘图模型:提示词矩阵、评分与完整样本排名

OpenArt 聚合了很多模型,每个模型消耗的积分和效果都不同。想选出适合自己用途的模型,最好用同一组提示词逐个试、逐个打分。 这篇做 comparesheet.py:先生成“提示词 × 模型”的测试矩阵 CSV,你在 OpenArt 网页上逐格生成、记下积分并打分,再汇总排名。

BINBHO · 图文教程

OpenArt 聚合了很多模型,每个模型消耗的积分和效果都不同。想选出适合自己用途的模型,最好用同一组提示词逐个试、逐个打分。 这篇做 compare_sheet.py:先生成“提示词 × 模型”的测试矩阵 CSV,你在 OpenArt 网页上逐格生成、记下积分并打分,再汇总排名。

先看清成品

  • 生成 3 个提示词 × 3 个模型 = 9 格的测试表;示例分数经 LibreOffice 转 xlsx 再转回 CSV 后仍能正确读取(全角“2”也能识别)。
  • 发现并修复:有一格没打分时,缺了那道难题的模型平均分虚高。
  • 改为只在所有模型都打完分的提示词上排名,并列出被排除的提示词。pytest 3 项通过。

合规提醒:OpenArt 帮助中心写明“目前没有公开 API”(No public API is available currently);服务条款 4.4 规定只能通过 openart.ai 或官方 App 使用生成服务,“只允许真人操作,不允许自动化访问、机器人或脚本”。所以这三篇都不自动操作 OpenArt:生成、下载和打分都在网页上手动完成,脚本只处理你下载到本地的文件。条款还写明免费套餐的输出可能带可见水印;不同模型的商用授权不同,以 OpenArt 页面和条款为准。不上传无授权的他人照片作为参考图。

运行说明:我没有 OpenArt 账号,也不能用脚本操作它。文中的图片是我在 box 上用本地 sd-turbo(Stability AI Community License)生成的替身,用来代替你从 OpenArt 下载的图;“模型A/B/C”的分数是我编的示例。所有脚本和测试都在 box 上真实运行过(2026-10-21,UTC+8)。

做完之后你会得到什么?

成果 说明
compare_sheet.py make 生成测试表,score 汇总
prompts.txt 测试提示词
compare_filled.csv 示例:填好分数的表(分数是我编的)
test_compare.py 3 项测试

需要准备哪些环境?

项目 说明
Python 3.9 以上,只用标准库;测试需要 pytest
表格软件 Excel、WPS 或 LibreOffice,用来填分

关键事实

事实 来源
OpenArt 按积分消耗,不同模型的价格、效果和授权可能不同,以官网为准 OpenArt 帮助中心
生成必须在网页上由真人操作 OpenArt 服务条款 4.4
LibreOffice 导出 CSV 时去掉了 BOM,保留 UTF-8 本文实测(LibreOffice 转换)

文件结构

76-openart-C-src/
├── compare_sheet.py
├── prompts.txt
├── compare_filled.csv
└── test_compare.py

第 1 步:脚本

"""OpenArt 多模型对比表:先生成“提示词 × 模型”的测试矩阵,你在 OpenArt 网页上逐格生成并打分,再汇总排名。
OpenArt 没有公开 API,生成和打分都是人工完成;本脚本只管表格。"""
import argparse, csv, statistics
from pathlib import Path

CRIT = ["prompt_match", "quality", "text_render"]  # 1-5 分:贴合提示词、画面质量、文字渲染

def make(prompts, models, out):
    with open(out, "w", newline="", encoding="utf-8-sig") as f:
        w = csv.writer(f); w.writerow(["id", "prompt", "model", "credits_used"] + CRIT + ["notes"])
        i = 0
        for p in prompts:
            for m in models:
                i += 1; w.writerow([i, p, m, ""] + [""] * len(CRIT) + [""])
    return i

def read_scores(path):
    raw = Path(path).read_bytes()
    for enc in ("utf-8-sig", "gb18030"):  # 表格软件另存时可能改成 GBK
        try: text = raw.decode(enc); break
        except UnicodeDecodeError: continue
    rows = list(csv.DictReader(text.splitlines()))
    return rows

def to_score(v):
    v = (v or "").strip().translate(str.maketrans("0123456789.", "0123456789."))
    if not v: return None
    x = float(v)
    if not 1 <= x <= 5: raise ValueError(f"分数应在 1-5:{v}")
    return x

def summarize(rows):
    """只在“所有模型都打完分”的提示词上排名;否则缺了难题的模型平均分会虚高。"""
    models = sorted({r["model"] for r in rows})
    cell, todo = {}, []
    for r in rows:
        s = [to_score(r[c]) for c in CRIT]
        if None in s: todo.append(r["id"]); continue
        cell[(r["prompt"], r["model"])] = (statistics.mean(s), float(r["credits_used"] or 0))
    prompts = list(dict.fromkeys(r["prompt"] for r in rows))
    full = [p for p in prompts if all((p, m) in cell for m in models)]
    out = []
    for m in models:
        xs = [cell[(p, m)] for p in full]
        if not xs: continue
        out.append({"model": m, "n": len(xs), "mean": round(statistics.mean(x[0] for x in xs), 2),
                    "min": round(min(x[0] for x in xs), 2), "credits": sum(x[1] for x in xs)})
    out.sort(key=lambda r: (-r["mean"], -r["min"]))
    return out, todo, [p for p in prompts if p not in full]

def main(argv=None):
    ap = argparse.ArgumentParser(); sub = ap.add_subparsers(dest="cmd", required=True)
    a1 = sub.add_parser("make"); a1.add_argument("--prompts", required=True); a1.add_argument("--models", required=True); a1.add_argument("-o", default="compare.csv")
    a2 = sub.add_parser("score"); a2.add_argument("csv")
    a = ap.parse_args(argv)
    if a.cmd == "make":
        prompts = [l.strip() for l in Path(a.prompts).read_text(encoding="utf-8").splitlines() if l.strip()]
        n = make(prompts, [m.strip() for m in a.models.split(",")], a.o); print(f"{a.o}:{n} 格待生成")
    else:
        res, todo, skipped = summarize(read_scores(a.csv))
        print("| 排名 | 模型 | 参与比较的提示词 | 平均分 | 最低一格 | 积分合计 |\n| ---: | :--- | ---: | ---: | ---: | ---: |")
        for i, r in enumerate(res, 1): print(f"| {i} | {r['model']} | {r['n']} | {r['mean']} | {r['min']} | {r['credits']:g} |")
        if todo: print(f"\n还没打完分的格:{', '.join(todo)}")
        for p in skipped: print(f"未参与排名(有模型没打完分):{p[:50]}…")

if __name__ == "__main__":
    main()

第 2 步:测试提示词

a minimalist product photo of a ceramic coffee mug on a wooden table, morning light, soft shadows
a poster with the words "SPRING SALE" in bold letters, flat illustration
the red-haired woman in a yellow raincoat from the reference, standing at a bus stop in the rain

第 3 步:测试

import csv, pytest
import compare_sheet as m

def rows(spec):
    out = []
    for i, (p, mod, sc, cr) in enumerate(spec, 1):
        out.append({"id": str(i), "prompt": p, "model": mod, "credits_used": cr, **dict(zip(m.CRIT, sc))})
    return out

def test_only_complete_prompts_ranked():
    r = rows([("easy", "A", ("4", "4", "4"), "8"), ("easy", "B", ("5", "5", "5"), "12"),
              ("hard", "A", ("2", "2", "2"), "8"), ("hard", "B", ("", "", ""), "12")])
    res, todo, skipped = m.summarize(r)
    assert todo == ["4"] and skipped == ["hard"] and all(x["n"] == 1 for x in res)
    assert res[0]["model"] == "B" and res[0]["credits"] == 12

def test_fullwidth_and_range():
    assert m.to_score("2") == 2.0 and m.to_score(" ") is None
    with pytest.raises(ValueError): m.to_score("6")

def test_make_and_gbk_read(tmp_path):
    p = tmp_path / "c.csv"; assert m.make(["x"], ["A", "B"], p) == 2
    text = p.read_text(encoding="utf-8-sig"); (tmp_path / "g.csv").write_bytes(text.replace("A", "模型甲").encode("gb18030"))
    assert m.read_scores(tmp_path / "g.csv")[0]["model"] == "模型甲"

第 4 步:运行

$ python compare_sheet.py make --prompts prompts.txt --models "模型A,模型B,模型C"
compare.csv:9 格待生成
# 在 OpenArt 网页上逐格生成,填 credits_used 和三项 1-5 分(示例分数是我编的)
$ soffice --headless --convert-to xlsx compare_filled.csv && soffice --headless --convert-to csv compare_filled.xlsx   # 模拟用表格软件编辑后另存
$ python compare_sheet.py score rt/compare_filled.csv
| 排名 | 模型 | 参与比较的提示词 | 平均分 | 最低一格 | 积分合计 |
| ---: | :--- | ---: | ---: | ---: | ---: |
| 1 | 模型B | 2 | 4.17 | 4.0 | 24 |
| 2 | 模型A | 2 | 3.33 | 3.0 | 16 |
| 3 | 模型C | 2 | 2.33 | 2.0 | 8 |

还没打完分的格:8
未参与排名(有模型没打完分):the red-haired woman in a yellow raincoat from the…
$ python -m pytest -q
3 passed

怎么检查结果?

  • “参与比较的提示词”数量相同,排名才有意义。
  • “最低一格”比平均分更能看出模型的短板。
  • 积分合计只算参与排名的格;同一模型不同设置(比例、数量)消耗不同,记录时写清楚。

调试示例

缺一格,排名就不公平。 第一版把每个模型打完分的格都算进平均分。模型B的第 8 格(第三个提示词,角色一致性,最难的一题)还没打分:

| 1 | 模型B | 2 | 4.17 | 4.0 | 12、12 |
| 2 | 模型A | 3 | 3.33 | 3.0 | 8、8、8 |
| 3 | 模型C | 3 | 2.44 | 2.0 | 4、4、4 |

模型B只考了两道简单题,A 和 C 考了三道,平均分不能直接比。修复:只用所有模型都打完分的提示词排名,被排除的提示词单独列出;积分改为合计。修复后 C 的平均分从 2.44 变为 2.33(本例排名顺序没变,但比较的范围一致了)。

常见问题

现象 原因 处理
读取乱码 表格软件另存成了 GBK 脚本会自动尝试 GB18030
分数应在 1-5 填错了 改正后重新 score
某个提示词没参与排名 有模型没打完分 补完那几格

模板规则

  • 同一组提示词测所有模型。
  • 先打完分再排名。
  • 记录每格消耗的积分。
  • 生成和打分都手动完成。

项目资源

用途 地址
OpenArt 帮助中心(官方) https://openart.ai/help
OpenArt 服务条款(官方) https://openart.ai/suite/terms
OpenArt 官网 https://openart.ai

用同一张考卷,才比得出高下。