Gamma 的
pages数组可以一次生成一个多页文件(最多 50 页),配合publish还可以直接发布成网站。这篇把一个目录里的多个 Markdown 文件做成pages请求,并在提交前完成本地校验:
- 先用从官方文档页里提取的 OpenAPI Schema 做 JSON Schema 校验,检查类型、枚举和长度;
- 再补上文档里写了、但 Schema 没有表达的规则:页数 1–50、numCards 不超过 75、path 不能重复,以及 inputTextBreaks 模式下 numCards 不起作用的提醒。
先看清成品(box 上真实运行)
- 真实抓取了官方 Schema,共 11 个结构。
- 3 个文件生成 3 页、5 张卡片,校验通过,提交给模拟服务器后状态为 completed。
- 两个中文文件名最初被生成了相同的 path,已被检查出来,并修正了生成规则。
- pytest 4 项通过。
合规提醒:调用 Gamma API 需要 Pro、Ultra、Teams、Business 或 Enterprise 计划的 API Key(
sk-gamma-…),Key 只放在服务器端。exportUrl在大约一周内任何拿到链接的人都能下载,要当作密钥来保管。输入的文字和图片必须是你有权使用的;开启网络图片来源时,要注意图片的授权条款,商用场景优先选webFreeToUseCommercially或noImages。在中国境内公开发布 AI 生成的内容,要遵守《生成式人工智能服务管理暂行办法》,并按《人工智能生成合成内容标识办法》加上 AI 标识。Gamma 按额度(credits)计费,本文不列价格。
运行说明:box 上没有 Gamma API Key,所以没有真正生成过文稿。流程跑在我自己写的模拟服务器
fake_gamma.py上,导出的 PDF 是 Pillow 画的,额度数字也是模拟器编的。用错误的 Key 请求真实接口确实运行过,返回401 {"message":"Invalid API key","statusCode":401}。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
fetch_schema.py |
从官方文档页提取 OpenAPI,保存为 gamma_schema.json |
build_pages.py |
目录 → pages 请求 → 校验 → 写出 request.json |
submit.py |
提交 request.json(复用 A 篇的客户端) |
site/ |
3 个示例页面 |
test_pages.py |
测试 Schema 校验和补充规则 |
需要准备哪些环境?
pip install requests jsonschema pytest;提交时需要的环境和 A 篇相同。
规则来源(官方文档)
pages最多 50 个,每项必须有inputText,可以单独设置title、path(不超过 500 字符)、numCards、format等。提供了pages时,顶层的同名字段不再生效。numCards:付费计划可设 1–75。按 Schema 的说明,它只在cardSplit=auto时决定卡片数量。- 手动分隔卡片:在文本里插入
\n---\n。
文件结构
53-gamma-B-src/
├── fetch_schema.py
├── gamma_schema.json
├── build_pages.py
├── submit.py
├── gamma_deck.py
├── fake_gamma.py
├── site/
│ ├── 01-home.md
│ ├── 02-vendors.md
│ └── 03-faq.md
└── test_pages.py
第 1 步:取官方 Schema
"""从 Gamma 官方文档页(Markdown 版)里取出内嵌的 OpenAPI,保存其中的 components.schemas,供本地校验用。"""
import json, re, sys, requests
URL = "https://developers.gamma.app/generations-classic/create-generation.md"
t = requests.get(URL, timeout=30, headers={"User-Agent": "binbho-tutorial-gamma/1.0"}).text
spec = json.loads(re.search(r'\{"openapi".*', t).group(0).splitlines()[0])
json.dump({"source": URL, "info": spec["info"], "components": spec["components"]},
open("gamma_schema.json", "w"), ensure_ascii=False, indent=1)
print("已保存 gamma_schema.json:", ", ".join(spec["components"]["schemas"]))
$ python fetch_schema.py
已保存 gamma_schema.json: Generation, PageGeneration, TextOptions, ImageOptions, ImageModel, CardOptions, HeaderFooter, HeaderFooterElement, SharingOptions, EmailOptions, CreateGenerationResponse
第 2 步:构建与校验
"""把一个目录里的多个 Markdown 文件做成一次多页生成请求(pages,1–50 页),提交前在本地校验:
1)用官方 OpenAPI 的 Generation 结构做 JSON Schema 校验(类型、枚举、长度);
2)补充文档里写了但 Schema 没写的规则:pages 1–50 个、付费计划 numCards 1–75、path 不重复,
以及 cardSplit=inputTextBreaks 时 numCards 不起作用(Schema 说 numCards 只在 auto 时决定卡片数)。"""
import json, pathlib, re, sys
from jsonschema import Draft202012Validator
SCHEMA = json.load(open(pathlib.Path(__file__).with_name("gamma_schema.json"), encoding="utf-8"))
ROOT = {"$ref": "#/components/schemas/Generation", "components": SCHEMA["components"]}
def slug(stem):
"""'02-vendors' → '/vendors';中文文件名转不出英文 slug 时,用编号兜底:'04-交通' → '/page-04'。"""
num, _, rest = stem.partition("-") if re.match(r"^\d+-", stem) else ("", "", stem)
s = re.sub(r"[^a-z0-9-]+", "-", rest.lower()).strip("-")
return "/" + (s or f"page-{num or 'x'}")
def build(folder, export="pdf"):
pages = []
for f in sorted(pathlib.Path(folder).glob("*.md")):
text = f.read_text(encoding="utf-8").strip()
first = text.splitlines()[0].lstrip("# ").strip()
pages.append({"inputText": text, "title": first, "path": slug(f.stem),
"cardSplit": "inputTextBreaks", "textMode": "preserve", "format": "webpage"})
return {"pages": pages, "exportAs": export, "textOptions": {"language": "zh-cn"}}
def validate(body):
errs = [f"{'/'.join(map(str, e.absolute_path)) or '(根)'}: {e.message[:120]}" for e in Draft202012Validator(ROOT).iter_errors(body)]
pages = body.get("pages", [])
if not 1 <= len(pages) <= 50:
errs.append(f"pages: 需要 1–50 页,现在 {len(pages)} 页")
paths = [p.get("path") for p in pages if p.get("path")]
for dup in {x for x in paths if paths.count(x) > 1}:
errs.append(f"pages: path 重复 {dup}")
warns = []
for i, p in enumerate(pages):
n = p.get("numCards")
if n is not None and n > 75:
errs.append(f"pages/{i}/numCards: {n} 超过付费计划上限 75")
if n is not None and p.get("cardSplit") == "inputTextBreaks":
warns.append(f"pages/{i}: cardSplit=inputTextBreaks 时 numCards 不决定卡片数,实际按 --- 切出 {p['inputText'].count(chr(10)+'---'+chr(10))+1} 张")
return errs, warns
if __name__ == "__main__":
body = build(sys.argv[1])
errs, warns = validate(body)
print(f"共 {len(body['pages'])} 页:" + ",".join(f"{p['path']}({p['inputText'].count(chr(10)+'---'+chr(10))+1} 张卡)" for p in body["pages"]))
for w in warns: print("⚠", w)
for e in errs: print("✗", e)
if errs: sys.exit(1)
pathlib.Path("request.json").write_text(json.dumps(body, ensure_ascii=False, indent=1), encoding="utf-8")
print("✓ 校验通过,已写出 request.json")
"""提交 build_pages.py 写出的 request.json(复用 A 篇 gamma_deck.py 的 create / wait)。"""
import json, sys
import gamma_deck as g
body = json.load(open(sys.argv[1] if len(sys.argv) > 1 else "request.json", encoding="utf-8"))
s = g.session()
gid = g.create(s, body); print("generationId:", gid)
d = g.wait(s, gid, interval=float(sys.argv[2]) if len(sys.argv) > 2 else 5)
print(d["status"], d.get("gammaUrl"), "消耗", d.get("credits", {}).get("deducted"), "导出", g.mask(d.get("exportUrl")))
第 3 步:测试
import json, pathlib
from build_pages import build, validate, slug
def test_build_ok(tmp_path):
body = build(pathlib.Path(__file__).with_name("site"))
errs, warns = validate(body)
assert errs == [] and [p["path"] for p in body["pages"]] == ["/home", "/vendors", "/faq"]
def test_schema_catches_enum_and_length():
body = {"pages": [{"inputText": "x", "textMode": "rewrite", "title": "t" * 501}], "exportAs": "docx"}
errs, _ = validate(body)
txt = " ".join(errs)
assert "rewrite" in txt and "docx" in txt and "title" in txt
def test_extra_rules():
body = {"pages": [{"inputText": "a", "path": "/x", "numCards": 80}, {"inputText": "b", "path": "/x"}]}
errs, _ = validate(body)
assert any("重复" in e for e in errs) and any("75" in e for e in errs)
assert validate({"pages": []})[0]
_, warns = validate({"pages": [{"inputText": "a\n---\nb", "cardSplit": "inputTextBreaks", "numCards": 5}]})
assert "2 张" in warns[0]
def test_chinese_filename_slug():
assert slug("04-交通") == "/page-04" and slug("02-vendors") == "/vendors"
第 4 步:运行
$ python build_pages.py site
共 3 页:/home(2 张卡),/vendors(2 张卡),/faq(1 张卡)
✓ 校验通过,已写出 request.json
$ GAMMA_API_BASE=http://127.0.0.1:18531 GAMMA_API_KEY=sk-gamma-demo python submit.py request.json 1
generationId: 9d2dd7ebcf86448d8d1d
completed https://gamma.app/docs/mock-9d2dd7ebcf86448d8d1d 消耗 20 导出 http://127.0.0.1:18531/…(已隐藏)
$ python -m pytest -q
4 passed in 0.04s
调试示例
两个中文文件名生成了相同的 path。 我往目录里加了 04-交通.md 和 05-联系.md。第一版的 slug 规则只保留英文字母和数字,中文全部被去掉,这两页就都退化成了 /page:
共 5 页:/home(2 张卡),/vendors(2 张卡),/faq(1 张卡),/page(1 张卡),/page(1 张卡)
✗ pages: path 重复 /page
好在“path 不重复”这条补充规则把问题拦了下来,没有提交出去。之后修改了 slug 规则,转不出英文时用文件编号兜底,生成 /page-04 和 /page-05,再次运行就通过了。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| Schema 报枚举错误 | 字段值写错了(比如 textMode=rewrite) |
照报错信息改成枚举中的值 |
| 设置了 numCards 但不生效 | 用的是 inputTextBreaks 模式 | 改用 --- 控制卡片数,或者改成 auto 模式 |
| 校验通过,接口却返回 400 | 服务端还有本地 Schema 没覆盖到的规则 | 以接口返回为准,并把这条规则补进 validate() |
| Schema 过时了 | 官方更新了接口 | 重新运行 fetch_schema.py |
模板规则
- 提交前用官方 Schema 校验,再补上文档里的额外规则。
- path 必须唯一,中文文件名要有兜底规则。
- Schema 的来源 URL 写进文件里,便于定期更新。
项目资源
| 用途 | 地址 |
|---|---|
| Gamma 开发者文档 | https://developers.gamma.app/ |
| POST /generations(创建生成) | https://developers.gamma.app/generations-classic/create-generation |
| GET /generations/{id}(轮询状态) | https://developers.gamma.app/generations-classic/get-generation-status |
| 参数说明(含 pages) | https://developers.gamma.app/guides/generate-api-parameters-explained |
| jsonschema(Python 库) | https://pypi.org/project/jsonschema/ |
本地校验能挡住大多数会被接口返回 400 的错误,额度只花在通过校验的请求上。