> ## Documentation Index
> Fetch the complete documentation index at: https://docs.textin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 大版本更新迁移说明

> 从旧版 pdf_to_markdown 接口平滑迁移到新版 xParse API 的完整指南

本文档帮助你从旧版 `pdf_to_markdown` 接口平滑迁移到新版 xParse API，并介绍新增的异步处理能力。

<Note>
  文中所有参数映射、字段名与默认值均以 TextIn 官方接口文档为准。
</Note>

***

## 为什么迁移

新版 xParse API 对旧版 `pdf_to_markdown` 做了全面升级，核心优势：

<CardGroup cols={2}>
  <Card title="更轻量的数据结构" icon="cube">
    标准化的 Element 模型，语义类型清晰（Title / NarrativeText / Table / Image / Formula），JSON 体积更小
  </Card>

  <Card title="标准输入输出协议" icon="code">
    统一的请求配置（config）与响应结构（schema），便于集成与迁移
  </Card>

  <Card title="引擎可替换" icon="gears">
    支持 TextIn / GUI 多引擎，可按场景选最优或对比效果
  </Card>

  <Card title="异步处理能力" icon="rocket">
    新增异步接口，适合大文件（建议 >10 页）和批量场景，避免 HTTP 超时；支持 Webhook 回调
  </Card>

  <Card title="坐标系标准化" icon="compass">
    归一化坐标（0\~1），不再依赖 dpi 参数
  </Card>

  <Card title="更丰富的内嵌元素" icon="layer-group">
    行内公式、手写体、复选框（checkbox）、内嵌图片
  </Card>
</CardGroup>

***

## 接口变更总览

| 维度           | 旧版                                    | 新版                                        |
| ------------ | ------------------------------------- | ----------------------------------------- |
| **URL（同步）**  | `/ai/service/v1/pdf_to_markdown`      | `/api/v1/xparse/parse/sync`               |
| **URL（异步）**  | ❌ 不支持                                 | `/api/v1/xparse/parse/async`              |
| **任务状态查询**   | ❌ 不支持                                 | `GET /api/v1/xparse/parse/async/{job_id}` |
| **文件上传**     | Body 二进制流（`application/octet-stream`） | `multipart/form-data`，字段名 `file`          |
| **URL 文件方式** | Body 文本（`text/plain`）                 | `multipart/form-data`，字段名 `file_url`      |
| **参数传入位置**   | Query String                          | `config` JSON 字段（form-data 内）             |
| **参数开关类型**   | 整数 `0` / `1`                          | Boolean `false` / `true`                  |
| **响应根节点**    | `result`                              | `data`                                    |
| **坐标系**      | 绝对像素（受 dpi 影响）                        | 归一化值（0\~1）                                |
| **页码起始**     | `page_id`（起始值 1-based）                | `page_number`（1-based，第一页为 1）             |

***

## 入参迁移指南

### 请求方式变更

<Tabs>
  <Tab title="旧版：Query String + 二进制 Body">
    ```http theme={null}
    POST https://api.textin.com/ai/service/v1/pdf_to_markdown?parse_mode=auto&table_flavor=html&page_details=1
    Content-Type: application/octet-stream
    x-ti-app-id: <your_app_id>
    x-ti-secret-code: <your_secret>

    [二进制文件内容]
    ```
  </Tab>

  <Tab title="新版：multipart/form-data + config JSON">
    ```http theme={null}
    POST https://api.textin.com/api/v1/xparse/parse/sync
    Content-Type: multipart/form-data
    x-ti-app-id: <your_app_id>
    x-ti-secret-code: <your_secret>

    --boundary
    Content-Disposition: form-data; name="file"; filename="doc.pdf"

    [二进制文件内容]
    --boundary
    Content-Disposition: form-data; name="config"

    {"capabilities":{"table_view":"html","pages":true,"title_tree":true}}
    ```
  </Tab>
</Tabs>

### 新版 config 结构总览

新版参数不再是扁平的 Query String，而是放在 form-data 的 `config` 字段里。该字段的值是一个 JSON，内部又分四个区块（注意：其中一个区块也叫 config，下文以「config 字段值 → config 区块」指代，切勿写成 `{"config":{"config":{...}}}` 之外的多层嵌套）：

```json theme={null}
{
  "document": { "password": "..." },
  "capabilities": { "table_view": "html", "pages": true, "...": "..." },
  "scope": { "page_range": "1-10" },
  "config": {
    "force_engine": "textin",
    "engine_params": { 
      "parse_mode": "auto", 
      "formula_level": 0,
      "image_output_type": "url" 
    }
  }
}
```

| 区块               | 作用                                              |
| ---------------- | ----------------------------------------------- |
| **document**     | 文档本身处理参数（如加密 PDF 密码）                            |
| **capabilities** | 解析策略与返回格式开关（决定返回哪些字段、表格格式等）                     |
| **scope**        | 处理范围（如页码区间）                                     |
| **config（区块）**   | 引擎选择（`force_engine`）+ 引擎级自定义参数（`engine_params`） |

<Note>
  **关键认知**：旧版的 `parse_mode`、`formula_level` 等"引擎行为参数"在新版并未消失，而是下沉到了 `config` 区块的 `engine_params` 里。它们与 `capabilities` 里的"返回内容开关"是两个不同维度，迁移时不要混淆。（注：`dpi` 是例外，新版已彻底移除。）
</Note>

### 参数字段映射表（核心）

下表已按官方文档逐项核对。「新版参数」列给出在 `config` 字段值内的相对路径（`engine_params` 即 `config` 区块下的 `engine_params`）。

| 旧版参数<br />（Query）                            | 旧默认      | 新版参数                                                       | 新默认     | 变更说明                                                 |
| -------------------------------------------- | -------- | ---------------------------------------------------------- | ------- | ---------------------------------------------------- |
| `pdf_pwd`                                    | —        | `document.password`                                        | —       | 仅改位置                                                 |
| `page_start` +<br />`page_count`             | 0 / 1000 | `scope.page_range`<br />（如 `"1-10"`）                       | —       | 合并为区间字符串                                             |
| `apply_document_tree`<br />（0/1）             | 1        | `capabilities.include_hierarchy`                           | `true`  | int→bool                                             |
| `table_flavor`<br />（md/html/none）           | html     | `capabilities.table_view`<br />（markdown/html）             | `html`  | "md" → "markdown"<br />"none" 已移除                    |
| `get_image`<br />（none/page/objects/both）    | none     | `capabilities.include_image_data`<br />（是否返回）              | `false` | 详见下方说明                                               |
| `image_output_type`<br />（base64str/default） | default  | `engine_params.image_output_type`<br />（url/base64）        | `url`   | 取值体系不同：<br />旧 base64str/default<br />→ 新 url/base64 |
| `formula_level`<br />（0/1/2）                 | 0        | `engine_params.formula_level`<br />（0/1）                   | 0       | ⚠️ 保留在引擎参数，<br />取值改为 0/1                            |
| `page_details`<br />（0/1）                    | 1        | `capabilities.pages`                                       | `false` | ⚠️ 默认反转，需显式 `true`                                   |
| `char_details`<br />（0/1）                    | 0        | `capabilities.include_char_details`                        | `false` | int→bool                                             |
| `catalog_details`<br />（0/1）                 | 0        | `capabilities.title_tree`                                  | `false` | 改名                                                   |
| `crop_dewarp`<br />（0/1）                     | 0        | `capabilities.crop_dewarp`                                 | `false` | int→bool                                             |
| `remove_watermark`<br />（0/1）                | 0        | `capabilities.remove_watermark`                            | `false` | int→bool                                             |
| `parse_mode`<br />（auto/scan/lite/parse）     | scan     | `engine_params.parse_mode`<br />（auto/scan/parse/lite/vlm） | 见下方说明   | ✅ 保留；新增 vlm 模式                                       |
| `raw_ocr`<br />（0/1）                         | 0        | 近似改用<br />`capabilities.include_char_details`              | `false` | ⚠️ 语义非完全等价                                           |
| `dpi`<br />（72/144/216）                      | 144      | ❌ 已移除                                                      | —       | 新版用归一化坐标，<br />无对应参数                                 |
| `get_excel`                                  | 0        | ❌ 已移除                                                      | —       | 新版不支持 Excel 输出                                       |
| `markdown_details`                           | 1        | ❌ 已移除                                                      | —       | elements 始终返回                                        |

<Note>
  **get\_image 迁移注意**：旧版 `get_image` 有 none/page/objects/both 四档语义（整页图 / 子图 / 两者）。新版通过组合 `capabilities.pages` 和 `capabilities.include_image_data` 实现：

  * 当 `pages=true` 时，`include_image_data=true/false` 对应 `get_image=both/page`
  * 当 `pages=false` 时，`include_image_data=true/false` 对应 `get_image=objects/none`

  图片格式由 `engine_params.image_output_type`（url/base64）控制。
</Note>

<Tip>
  * **parse\_mode 在新版的默认值**：旧版默认 `scan`；新版 parse/sync 接口默认值以官方文档为准
  * **force\_engine 是新增的"引擎选择"维度**（textin/textin\_gui），不是 `parse_mode` 的替代品；在textin引擎下，`parse_mode`可同时配置
</Tip>

### 新增能力（仅新版支持）

| 参数                                     | 默认       | 说明                                                |
| -------------------------------------- | -------- | ------------------------------------------------- |
| `capabilities.include_inline_objects`  | `false`  | 返回文本内的细粒度行内对象：公式、手写、复选框、内嵌图片                      |
| `capabilities.include_table_structure` | `false`  | 返回表格结构化数据（行列数、单元格坐标、跨行跨列、单元格内容类型）                 |
| `config.force_engine`                  | `textin` | 指定引擎：`textin`（默认）/ `textin_gui`。各引擎适用场景与限制以官方文档为准 |
| `config.engine_params`                 | —        | 引擎自定义参数（专家模式），不同引擎支持的参数不同                         |

***

## 出参迁移指南

### 顶层结构变化

<Tabs>
  <Tab title="旧版">
    ```json theme={null}
    {
      "code": 200,
      "message": "success",
      "result": { "...": "主体数据" },
      "version": "v1.0",
      "duration": 1234
    }
    ```
  </Tab>

  <Tab title="新版">
    ```json theme={null}
    {
      "code": 200,
      "message": "success",
      "data": {
        "schema_version": "1.3.0",
        "file_id": "doc_7f3a2b",
        "job_id": "job_x9k2m",
        "success_count": 10,
        "metadata": { "...": "文件名/类型/页数" },
        "markdown": "...",
        "elements": [],
        "pages": [],
        "title_tree": [],
        "summary": {
          "duration_ms": 972
        }
      }
    }
    ```
  </Tab>
</Tabs>

### 主体数据字段映射

| 旧版 `result.*`       | 新版 `data.*`                                          | 变更说明         |
| ------------------- | ---------------------------------------------------- | ------------ |
| `markdown`          | `markdown`                                           | 不变           |
| `detail`            | `elements`                                           | 改名，结构重组（见下节） |
| `pages`             | `pages`                                              | 字段名相同，内部结构重组 |
| `catalog`（含 `toc`）  | `title_tree`                                         | 改名，结构变为节点数组  |
| `total_page_number` | `metadata.page_count`                                | 下沉至 metadata |
| `valid_page_number` | `success_count`                                      | 改名           |
| `excel_base64`      | ❌ 已移除                                                | 新版不支持 Excel  |
| （无）                 | `schema_version` / `file_id` / `job_id` / `metadata` | 新增字段         |

### 元素结构变化（detail → elements）

旧版 `detail[]` 与新版 `elements[]` 的字段对照：

| 旧版 `detail[]`                     | 新版 `elements[]`                       | 变更说明                                                     |
| --------------------------------- | ------------------------------------- | -------------------------------------------------------- |
| `paragraph_id`（整数）                | `element_id`（字符串，如 `el_001`）          | ID 类型由整数改为字符串                                            |
| `type`（"paragraph" / "table" 等）   | `type`（语义字符串）                         | 改为 Title / NarrativeText / Table / Image / Formula 等语义类型 |
| `text`                            | `text`                                | 不变                                                       |
| `position`（绝对像素）                  | `coordinates`（归一化 0\~1）               | ⚠️ 坐标系变化，需换算                                             |
| `page_id`                         | `page_number`                         | 页码起始位置相同（具体说明见下节）                                        |
| `outline_level`                   | `metadata.category_depth`             | 层级信息下沉至 metadata                                         |
| `cells`（表格单元格）                    | `table_structure.cells`               | 下沉至 `table_structure` 子对象                                |
| `caption_id`                      | `metadata.ref_element_id`             | 关联关系下沉至 metadata                                         |
| `tags`<br />（formula/handwritten） | `objects[]`（内嵌对象）                     | 改为结构化的行内对象数组                                             |
| （无）                               | `metadata.parent_id` / `children_ids` | 新增：父子层级关系                                                |
| （无）                               | `image_data`                          | 新增：图片 URL / MIME / base64                                |
| （无）                               | `char_details`                        | 新增：字符级详情                                                 |

### 新版元素示例

```json theme={null}
{
  "element_id": "el_001",
  "type": "Title",
  "text": "检验结果",
  "page_number": 1,
  "coordinates": [0.1, 0.12, 0.32, 0.12, 0.32, 0.16, 0.1, 0.16],
  "metadata": {
    "category_depth": 0,
    "children_ids": ["el_002", "el_003"],
    "is_continuation": false
  }
}
```

### 坐标系与页码换算

#### 坐标换算

```python theme={null}
# 旧版：绝对像素坐标（受 dpi 影响），字段名 position
# position = [x1, y1, x2, y2, x3, y3, x4, y4]  单位：像素

# 新版：归一化坐标（范围 0~1），字段名 coordinates
# 如需换算为像素，从 data.pages[] 取该页宽高：
page_width  = page["page_width"]
page_height = page["page_height"]
pixel_coords = [
    (coords[i] * page_width, coords[i + 1] * page_height)
    for i in range(0, 8, 2)
]

```

#### 页码说明

* **旧版**：`page_id`，1-based（第一页为 1）
* **新版**：`page_number`，1-based（第一页为 1）

两者起始值相同，均为 1-based，遍历/定位页面的逻辑可直接迁移。

<Note>
  归一化 `coordinates` 8 个值的点序为：左上 → 右上 → 右下 → 左下。
</Note>

***

## 异步接口使用指南

旧版不支持异步。新版异步接口适合大文件（建议 >10 页）或批量处理，避免 HTTP 连接超时。

### 接口概览

| 接口          | HTTP | URL                                   | 说明                     |
| ----------- | ---- | ------------------------------------- | ---------------------- |
| **提交异步任务**  | POST | `/api/v1/xparse/parse/async`          | 上传文件，返回 `job_id`       |
| **查询状态/结果** | GET  | `/api/v1/xparse/parse/async/{job_id}` | 轮询状态，完成后含 `result_url` |

#### 任务状态枚举

| 状态            | 含义                                   |
| ------------- | ------------------------------------ |
| `pending`     | 排队等待中                                |
| `in_progress` | 处理中                                  |
| `completed`   | 已完成（响应含 `result_url`，下载该 URL 获取完整结果） |
| `failed`      | 处理失败（响应含 `message` 错误说明）             |

<Note>
  异步提交成功仅返回 `{"data": {"job_id": "..."}}`，完整解析结果需通过 `result_url` 二次下载。`result_url` 返回的数据结构与同步接口 `data` 一致。
</Note>

### 异步调用完整流程（Python）

```python theme={null}
import requests, time, json

APP_ID  = "your_app_id"
SECRET  = "your_secret_code"
HEADERS = {"x-ti-app-id": APP_ID, "x-ti-secret-code": SECRET}


def submit_async_job(file_path: str, config: dict = None) -> str:
    """提交异步解析任务，返回 job_id"""
    url = "https://api.textin.com/api/v1/xparse/parse/async"
    with open(file_path, "rb") as f:
        files = {"file": (file_path, f, "application/pdf")}
        data  = {"config": json.dumps(config)} if config else {}
        resp  = requests.post(url, headers=HEADERS, files=files, data=data)
    resp.raise_for_status()                       # 先校验 HTTP 状态
    result = resp.json()
    if result.get("code") != 200:                 # 再校验业务 code，避免 KeyError
        raise RuntimeError(f"提交失败: {result.get('message')}")
    return result["data"]["job_id"]


def poll_result(job_id: str, interval: int = 3, max_wait: int = 300) -> dict:
    """轮询任务状态，完成后返回完整解析结果"""
    url     = f"https://api.textin.com/api/v1/xparse/parse/async/{job_id}"
    elapsed = 0
    while elapsed < max_wait:
        resp = requests.get(url, headers=HEADERS)
        resp.raise_for_status()
        job_data = resp.json()["data"]
        status   = job_data["status"]
        if status == "completed":
            # result_url 鉴权要求待确认，此处带 headers 以求稳妥
            return requests.get(job_data["result_url"], headers=HEADERS).json()
        elif status == "failed":
            raise RuntimeError(f"任务失败: {job_data.get('message')}")
        print(f"  {status}，已等待 {elapsed}s ...")
        time.sleep(interval)
        elapsed += interval
    raise TimeoutError(f"超时（{max_wait}s），job_id={job_id}")


config = {
    "capabilities": {
        "include_hierarchy":  True,
        "table_view":         "html",
        "include_image_data": True,   # ⚠️ 默认 false，需显式开启
        "pages":              True,   # ⚠️ 默认 false，需显式开启
        "title_tree":         True,
    }
}
job_id = submit_async_job("large_document.pdf", config)
result = poll_result(job_id)
print(result["data"]["markdown"])
```

### Webhook 回调（推荐生产环境）

提交任务时附带 `webhook` 参数，任务完成/失败后系统主动 POST 推送，无需轮询：

```python theme={null}
# 注意:file_url 同样走 multipart/form-data 提交。
# requests 用 files= 触发 multipart;下方用 data= 会变成 urlencoded;
# 服务端通常也能解析表单字段,但若严格遵循 multipart 协议,建议统一用 files=。
fields = {
    "file_url": (None, "https://your-storage.com/document.pdf"),
    "webhook": (None, "https://your-server.com/callback/textin"),
    "config": (None, json.dumps({"capabilities": {"table_view": "html"}})),
}

resp = requests.post(
    "https://api.textin.com/api/v1/xparse/parse/async",
    headers=HEADERS,
    files=fields
)
resp.raise_for_status()
job_id = resp.json()["data"]["job_id"]

# 你的服务器收到的 Webhook Body（Method: POST, Content-Type: application/json）：
# {
#   "job_id": "xxx",
#   "status": "completed",
#   "result_url": "https://..."
# }
```

***

## 完整代码迁移示例

<Tabs>
  <Tab title="旧版（pdf_to_markdown）">
    ```python theme={null}
    import requests

    def parse_pdf_old(file_path, app_id, secret):
      url = "https://api.textin.com/ai/service/v1/pdf_to_markdown"
      params = {
          "parse_mode": "auto",
          "table_flavor": "html",
          "get_image": "objects",
          "page_details": 1,
          "catalog_details": 1,
          "apply_document_tree": 1,
      }
      headers = {
          "x-ti-app-id": app_id,
          "x-ti-secret-code": secret,
          "Content-Type": "application/octet-stream",
      }
      with open(file_path, "rb") as f:
          resp = requests.post(url, headers=headers,
                               params=params, data=f)
      resp.raise_for_status()
      data = resp.json()
      if data.get("code") != 200:
          raise RuntimeError(data.get("message"))
      return {
          "markdown":    data["result"]["markdown"],
          "total_pages": data["result"]["total_page_number"],
          "elements":    data["result"]["detail"],
          "catalog":     data["result"]["catalog"],
          "pages":       data["result"]["pages"],
      }
    ```
  </Tab>

  <Tab title="新版（xparse 同步）——等效替换">
    ```python theme={null}
    import requests, json

    def parse_pdf_new(file_path, app_id, secret):
      url = "https://api.textin.com/api/v1/xparse/parse/sync"
      config = {
          "capabilities": {
              "include_hierarchy":  True,   # 原 apply_document_tree=1
              "table_view":         "html", # 原 table_flavor=html
              "include_image_data": True,   # 原 get_image  ⚠️需显式
              "pages":              True,   # 原 page_details=1 ⚠️需显式
              "title_tree":         True,   # 原 catalog_details=1
          },
          "config": {
              "force_engine": "textin",
              "engine_params": {"parse_mode": "auto"},  # 原 parse_mode
          },
      }
      headers = {"x-ti-app-id": app_id, "x-ti-secret-code": secret}
      with open(file_path, "rb") as f:
          resp = requests.post(
              url, headers=headers,
              files={"file": (file_path, f, "application/pdf")},
              data={"config": json.dumps(config)})
      resp.raise_for_status()
      data = resp.json()
      if data.get("code") != 200:
          raise RuntimeError(data.get("message"))
      d = data["data"]
      return {
          "markdown":    d["markdown"],
          "total_pages": d["metadata"]["page_count"],
          "elements":    d["elements"],     # type 现为字符串
          "title_tree":  d.get("title_tree"),
          "pages":       d.get("pages"),
      }
    ```
  </Tab>
</Tabs>

***

## 迁移检查清单

<Steps>
  <Step title="更新请求 URL">
    `/ai/service/v1/pdf_to_markdown` → `/api/v1/xparse/parse/sync`
  </Step>

  <Step title="更改请求方式">
    Query String + 二进制 Body → `multipart/form-data` + `config` JSON
  </Step>

  <Step title="转换参数类型">
    整数开关（0/1）全部改为 boolean（false/true）
  </Step>

  <Step title="显式声明必要参数">
    ⚠️ 显式声明 `capabilities.include_image_data: true`（默认已反转）\
    ⚠️ 显式声明 `capabilities.pages: true`（默认已反转）
  </Step>

  <Step title="迁移引擎参数">
    `parse_mode` / `formula_level` / `image_output_type` 迁移到 `config` 区块的 `engine_params`（非 capabilities）
  </Step>

  <Step title="评估引擎选择">
    评估是否需要 `force_engine` 选择特定引擎（新增能力，textin引擎下可同时配置 `parse_mode` ）
  </Step>

  <Step title="更新响应解析">
    * 响应根节点：`result` → `data`
    * `total_page_number` → `data.metadata.page_count`
    * `valid_page_number` → `data.success_count`
    * `detail` → `data.elements`（`paragraph_id` → `element_id`；`type` 由 "paragraph" 等改为语义字符串）
    * `catalog` → `data.title_tree`
  </Step>

  <Step title="转换坐标系统">
    坐标：`position`（绝对像素）→ `coordinates`（归一化，× page\_width/height 换算）
  </Step>

  <Step title="更新表格参数">
    `table_flavor='md'` → `table_view='markdown'`；'none' 已移除，需调整逻辑
  </Step>

  <Step title="更新图片参数">
    `image_output_type`：取值由 base64str/default 改为 url/base64
  </Step>

  <Step title="处理已移除参数">
    * 如用 `get_excel`：新版不支持 Excel 输出，需另寻替代
    * 如用 `raw_ocr`：近似改用 `capabilities.include_char_details`（语义非完全等价，需验证）
    * `dpi`：新版无对应参数，坐标改为归一化输出
  </Step>

  <Step title="增强错误处理">
    所有响应在读取 `["data"]` 前先校验 HTTP 状态与业务 `code`，避免鉴权/参数错误变成 KeyError
  </Step>

  <Step title="考虑异步接口">
    大文件（建议 >10 页）评估迁移至异步接口
  </Step>

  <Step title="配置 Webhook">
    生产环境异步场景配置 Webhook 回调替代轮询
  </Step>
</Steps>
