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

# 文件处理

> Mirobody 如何把上传的文档转成 LLM 可消费的片段：PDF、Excel/CSV、OCR，以及视觉模型内联。

## 概览

Mirobody 能摄取健康文档 —— 检验报告、电子表格、扫描记录 —— 并把它们转成一次聊天回合可以携带的内容。这项工作由 `src/transcode/` 中的**文档转码器**（[`document.hpp`](https://github.com/thetahealth/mirobody/blob/main/src/transcode/document.hpp) / `document.cpp`）完成，它是图像转码器在文档侧的对应物。其策略是**文本优先，图像（及 OCR）兜底**。

<Info>
  转码器把文档拆解为文本与图像片段；具备视觉能力的 LLM 在对话轮次中读取它们。
</Info>

## 支持的格式

容器格式由起始的 magic bytes 嗅探得出，再路由到对应的处理器：

| 格式                | 库            | 构建开关                               | 输出                   |
| ----------------- | ------------ | ---------------------------------- | -------------------- |
| **PDF**           | PDFium       | `MIROBODY_ENABLE_PDF`（默认**关闭**）    | 逐页文本；扫描页 → 图像（+ OCR） |
| **.xlsx**         | xlnt         | 找到 xlnt 时开启（Windows 上经 vcpkg 始终开启） | 每个工作表一张 Markdown 表格  |
| **.xls**（旧版 BIFF） | 内置的 libxls   | `MIROBODY_ENABLE_XLS`（默认**关闭**）    | 每个工作表一张 Markdown 表格  |
| **.csv**          | 内建（RFC 4180） | 始终可用                               | 单张 Markdown 表格       |

当某开关关闭时，输入仍会被嗅探，但 `process()` 会抛出 `DocumentError("... not built")` —— 不会有任何内容被悄悄错误处理。

<Note>
  单独上传的图像由姊妹**图像转码器**（`src/transcode/image.hpp`）处理，它会把图像适配到目标视觉模型的输入上限。旧文档中的音频与基因文件处理**不属于**这个 C++ 转码器。*(verify)*
</Note>

## 流水线

<Steps>
  <Step title="嗅探格式">
    `detect_format()` 读取 magic bytes：`%PDF-` → PDF，`PK\x03\x04` → xlsx（任意 OOXML zip），OLE2 签名 → 旧版 .xls，其余非空内容 → CSV。
  </Step>

  <Step title="PDF —— 文本层优先">
    对每一页，先把内嵌的**文本层提取**为一个文本片段。若某页可提取的文本低于一个很小的阈值（即扫描页），则改按图像处理。
  </Step>

  <Step title="扫描页 —— 栅格化 → 符合视觉模型要求的图像">
    扫描页会被栅格化（默认 150 dpi），重新打包为 PNG，并经 `image::Transcoder` 处理，使其满足目标视觉模型的上限（默认 Qwen-VL；也提供 Gemini / GPT 的上限）。它会作为一个**图像片段**发出，由聊天回合内联给视觉模型。
  </Step>

  <Step title="OCR（可选）">
    在 `MIROBODY_ENABLE_OCR` 构建中（还额外要求 `MIROBODY_ENABLE_PDF`），同一张栅格图还会经过 **Tesseract**，并把识别出的文本追加为一个文本片段 —— 于是一张扫描页会同时产出图像及其 OCR 文本。OCR 语言默认为 `eng`（混排文档可用 `eng+chi_sim`）。
  </Step>

  <Step title="电子表格 —— 始终为文本">
    每个工作表（xlsx / xls）或整个 CSV 都会渲染为一张 **GitHub 风格的 Markdown 表格**；电子表格处理绝不产生图像片段。
  </Step>
</Steps>

结果是一个 `Document`：一个有序的 `Part` 列表，每个片段要么是 **Text**（UTF-8 / Markdown），要么是 **Image**（一张已符合视觉模型要求的图像），并标注其从 1 起算的来源页 / 工作表。`max_pages` 上限可为超大文档设界；触发时会追加一条尾注，从而不会有内容被悄悄丢弃。`Transcoder::to_markdown()` 会把整份文档渲染为单个 Markdown 块，供想要单一字符串的调用方（以及 CLI）使用。

## 单位归一化到 UCUM

从文档与图表中提取的数值，由 `src/fhir/units/` 中的术语引擎归一化：一段自由文本的"数值 + 单位"字符串会变成一个规范的 **UCUM** 单位，外加用于消歧的 LOINC PROPERTY 族。这是纯本地计算 —— 无数据库、无 embedding API。

```
"MG/DL"              → "mg/dL"
"毫摩尔每升"          → "mmol/L"
"mmHg"               → "mm[Hg]"
"<5.6 mg/dL"         → value 5.6, unit "mg/dL", comparator "<"
```

支持多语言输入（en、zh、ja、ko、ru、de、fr、es）。

## 数据的去向

转码后的片段在聊天回合中由 agent 读取：上传的文件按用户存储，agent 通过 `list_files` 与 `read_file` 这两个 MCP 工具列出 / 读取它们，并把文本片段和符合视觉模型要求的图像片段喂给 LLM。

<Warning>
  **要理解文档，至少需要一个 LLM key（或设备本地的 Gemma）** —— 上传并存储文件无需 key 即可完成，但从 PDF / 图像中提取含义需要一个模型。纯文本格式（CSV / xlsx）渲染为 Markdown 无需模型。
</Warning>

<Note>
  把提取出的检验值作为带编码的 `Observation` 写回 FHIR 存储（文档 → 指标 → FHIR 流水线）尚在**进行中**：UCUM 单位归一化与 RESTful FHIR 存储已上线；术语解析（SNOMED CT / LOINC / RxNorm）与自动写回尚在计划中。*(verify against `src/fhir/README.md`.)*
</Note>

## 测试

转码器由 Catch2 测试覆盖：`tests/transcode/document_test.cpp` 与 `tests/transcode/image_test.cpp`；单位引擎由 `tests/fhir/units_test.cpp` 覆盖。参见[测试](/zh/development/testing)。

## 下一步

<CardGroup cols={2}>
  <Card title="数据流" icon="diagram-project" href="/zh/concepts/data-flow">
    上传如何与 vendor 及设备本地数据并存
  </Card>

  <Card title="文件处理实现" icon="code" href="/zh/development/file-processing">
    深入了解转码器 API 与构建开关
  </Card>

  <Card title="Vendor 系统" icon="plug" href="/zh/concepts/providers">
    服务端到服务端的健康数据源
  </Card>

  <Card title="API 参考" icon="book" href="/zh/api-reference/overview">
    兼容 OpenAI 的 `/v1` 接口
  </Card>
</CardGroup>
