官方能力:导出项目转写为 txt/markdown/html/rtf/docx/srt。教学客户端对
POST /v1/export/transcript发 JSON(project_id+format),保存响应正文。
先看清成品
- mock 导出含 Speaker 文本与 X-Composition-Id。
- 1 项 pytest 通过。
合规提醒:Overdub / 声音克隆仅限本人或已获明确授权并完成平台验证的声音,禁止冒充他人。API token 绑定单一 Drive,可消耗 media minutes 与 AI credits;耗尽可能返回 402。导入勿用未授权媒体;YouTube URL 不作 import 源(官方限制)。本文不编造套餐价格。
运行说明:无正式
DESCRIPT_API_TOKEN。A 用 mock 导出转写;B 本地口头禅扫描;C Overdub 同意门禁。未调用真实 Descript API。2026-10-23(Asia/Shanghai)pytest 通过。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
export_transcript.py |
导出客户端 |
mock_descript.py |
假服务 |
test_110a.py |
测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.10+ |
| 环境变量 | DESCRIPT_API_TOKEN;可选 DESCRIPT_BASE_URL |
关键事实
| 事实 | 来源 |
|---|---|
| Token 在 App Settings → API tokens 创建,绑定一个 Drive | 官方 Help |
| 导出格式含 srt 等;响应可带 X-Composition-Id | 官方 API 文档 |
| 无发布则不能仅靠 API 下载成片媒体文件 | 官方 Known limitations |
文件结构
110-descript-A-src/
├── export_transcript.py
├── mock_descript.py
└── test_110a.py
第 1 步:客户端
#!/usr/bin/env python3
"""Descript API: export project transcript (txt/markdown/html/rtf/docx/srt)."""
from __future__ import annotations
import argparse, json, os, sys, urllib.error, urllib.request
FORMATS = {"txt", "markdown", "html", "rtf", "docx", "srt"}
def export(base, token, project_id, fmt="txt", speaker_labels="changes"):
if fmt not in FORMATS:
raise SystemExit(f"format 须为 {sorted(FORMATS)}")
payload = {"project_id": project_id, "format": fmt, "include_speaker_labels": speaker_labels}
req = urllib.request.Request(
f"{base.rstrip('/')}/v1/export/transcript",
data=json.dumps(payload).encode(),
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=120) as resp:
data = resp.read()
return {"composition_id": resp.headers.get("X-Composition-Id"), "body": data, "content_type": resp.headers.get("Content-Type")}
except urllib.error.HTTPError as e:
raise SystemExit(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("project_id")
ap.add_argument("--format", default="txt", choices=sorted(FORMATS))
ap.add_argument("-o", "--out", default="transcript.txt")
ap.add_argument("--base", default=os.environ.get("DESCRIPT_BASE_URL", "https://descriptapi.com"))
ap.add_argument("--token", default=os.environ.get("DESCRIPT_API_TOKEN", ""))
args = ap.parse_args(argv)
if not args.token:
print("设置 DESCRIPT_API_TOKEN(App Settings → API tokens,绑定单一 Drive)", file=sys.stderr); return 2
r = export(args.base, args.token, args.project_id, args.format)
with open(args.out, "wb") as f:
f.write(r["body"])
print(json.dumps({"out": args.out, "composition_id": r["composition_id"], "bytes": len(r["body"])}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
第 2 步:Mock
#!/usr/bin/env python3
"""Mock Descript transcript export API."""
from __future__ import annotations
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
TOKEN = "descript-test-token"
class H(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def do_POST(self):
if self.path != "/v1/export/transcript":
self.send_error(404); return
auth = self.headers.get("Authorization", "")
if auth != f"Bearer {TOKEN}":
self.send_response(401); self.end_headers(); return
n = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(n) or b"{}")
fmt = body.get("format") or "txt"
text = "Speaker 1: Hello, welcome to the show.\n\nSpeaker 2: Thanks for having me.\n"
if fmt == "srt":
text = "1\n00:00:00,000 --> 00:00:02,000\nHello, welcome to the show.\n"
elif fmt == "markdown":
text = "# Transcript\n\n**Speaker 1:** Hello\n"
data = text.encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Disposition", "attachment; filename=transcript.txt")
self.send_header("X-Composition-Id", "comp-mock-001")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def main():
import argparse
ap = argparse.ArgumentParser(); ap.add_argument("--port", type=int, default=9111)
args = ap.parse_args()
HTTPServer(("127.0.0.1", args.port), H).serve_forever()
if __name__ == "__main__":
main()
第 3 步:测试
import subprocess, sys, time
from pathlib import Path
HERE = Path(__file__).resolve().parent
def test_export():
port = 19200
proc = subprocess.Popen([sys.executable, str(HERE/"mock_descript.py"), "--port", str(port)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
time.sleep(0.3)
out = HERE / "_t.txt"
r = subprocess.run([sys.executable, str(HERE/"export_transcript.py"), "proj-1", "-o", str(out), "--base", f"http://127.0.0.1:{port}", "--token", "descript-test-token"], capture_output=True, text=True, check=True)
assert out.exists() and "Speaker" in out.read_text()
assert "composition_id" in r.stdout
finally:
proc.terminate(); proc.wait(timeout=5)
if (HERE/"_t.txt").exists():
(HERE/"_t.txt").unlink()
第 4 步:运行
$ pytest export
. [100%]
1 passed in 0.36s
怎么检查结果?
- 输出文件含台词。
- 正式 base/path 以当前 OpenAPI 为准。
调试示例
402。 媒体分钟或 AI credits 用尽。到账户查看用量后再试;导入与 Underlord 都会耗额度。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 端点路径变了 | 对照 dev-docs | 改客户端 |
| docx | 二进制 | 用 -o out.docx |
| 错 Drive | token 绑死 Drive | 换 token |
模板规则
- token 保密。
- 导出稿仍要隐私审查。
- 正式路径随文档更新。
项目资源
| 用途 | 地址 |
|---|---|
| Descript API Help(官方) | https://help.descript.com/api-and-mcp/api |
| API 文档(官方) | https://dev-docs.descriptapi.com/ |
| descriptapi.com | https://descriptapi.com/ |
文字导出通了,文字剪辑工作流才接得上。