docs: restore maintained bilingual Agent integration routes

Signed-off-by: zhifu gao <[email protected]>
This commit is contained in:
zhifu gao
2026-09-07 11:54:11 +00:00
parent e19029adca
commit 9bdce64990
10 changed files with 582 additions and 1 deletions
+1
View File
@@ -44,6 +44,7 @@
Python SDK, Python HTTP, native vLLM HTTP and C++ WebSocket are different
interfaces. Choose the matching protocol before selecting a client.
- [Agent integration](agent_integration.md) / [中文](agent_integration_zh.md): HTTP, MCP, desktop recording and local subtitles
- [HTTP schema](../examples/openai_api/OPENAPI.md), [JavaScript](../examples/openai_api/JAVASCRIPT.md), [workflow integration](../examples/openai_api/WORKFLOWS.md)
- [C++ WebSocket protocol](../runtime/docs/websocket_protocol.md) and [ONNX binary output](../runtime/docs/onnxruntime_binary_output.md)
- [HTTP security](../examples/openai_api/SECURITY.md) and [Kubernetes manifests](../examples/openai_api/kubernetes/README.md)
+224
View File
@@ -0,0 +1,224 @@
# FunASR Agent Integration
[中文](agent_integration_zh.md)
Connect a speech application through the interface it actually needs: an HTTP
file-transcription request, a local MCP tool, desktop recording, or a local
subtitle pipeline. These paths do not share every model, option, or result field.
For direct in-process inference, use the [Python SDK](python_api.md).
## HTTP server
The following source-based starting point includes the example scripts used
later on this page. The commands use a POSIX shell. Use a new checkout and virtual environment; installing the
PyPI package alone does not install these repository examples.
```bash
git clone https://github.com/modelscope/FunASR.git FunASR-agent
cd FunASR-agent
git checkout --detach e19029adca384a06a2f60bd8c18cb98f1a0499aa
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install -e .
python -m pip install fastapi uvicorn python-multipart
python -m pip check
```
This pins the source, not all dependencies or model weights. Use the
[installation guide](installation/installation.md) to prepare the selected
CPU/GPU environment, record resolved package/model versions, and validate a
real request. `pip check` alone is not a CUDA, audio-decoder, or clean-install test.
From that environment, run **one** of these commands. The CPU recipe selects
SenseVoice explicitly; the CUDA alternative needs a working GPU environment.
Keep this terminal running and use another prepared terminal for clients.
```bash
funasr-server --host 127.0.0.1 --device cpu --model sensevoice --port 8000
# Alternative: stop the CPU server before using the same port.
funasr-server --host 127.0.0.1 --device cuda --model sensevoice --port 8000
```
```bash
curl -fsS http://localhost:8000/health
curl -fsS http://localhost:8000/v1/models
```
Use `/v1/audio/transcriptions` for uploaded files, `/openapi.json` for the running
service schema, and `/docs` for its Swagger UI. This service `/docs` is not the
FunASR website's documentation directory. Health or model-list responses do not
replace a successful transcription with the intended model.
The packaged `funasr-server` and [example HTTP server](../examples/openai_api/README.md#api-contract)
have different defaults, aliases, and response schemas. Specify `model` in the
request as well as at startup. `paraformer-en` is an example-server alias, not a
built-in packaged-server alias. SenseVoice HTTP display text has rich tags removed;
it is not a dedicated emotion/event output API. Nano and MLT-Nano language coverage
and timestamp paths must be selected through the [model guide](model_selection.md),
not inferred from the HTTP interface. For a custom packaged-server model, use
`--model-path` with the appropriate `--hub` and request `model=custom`; an
arbitrary model ID is not automatically a built-in `--model` alias.
[MOSS-Transcribe-Diarize](moss_transcribe_diarize.md) is a third-party OpenMOSS
model with its own deployment requirements. Its native anonymous speaker labels
do not require external VAD or an external speaker model; do not add those stages
or assume every client exposes all of its output.
The local server does not authenticate the placeholder SDK key below. Before
network access, add TLS, authentication, upload limits and rate limits according
to the [security guide](../examples/openai_api/SECURITY.md). CORS is not authentication.
## SDK and curl
Install the separate OpenAI HTTP client in the client environment:
```bash
python -m pip install openai
```
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="local-development")
with open("meeting.wav", "rb") as audio:
result = client.audio.transcriptions.create(
model="sensevoice",
file=audio,
response_format="verbose_json",
)
print(result.text)
for segment in getattr(result, "segments", []):
print(segment)
```
```bash
curl -fsS http://localhost:8000/v1/audio/transcriptions \
-F file=@audio.wav \
-F model=sensevoice \
-F response_format=verbose_json
```
`verbose_json` chooses a format; it does not enable diarization, recover rich
tags, or guarantee word alignment. The example server copies available
`sentence_info` into segments, otherwise returning an empty list. The packaged
server can supply coarse fallback segments. Both expose segment times in seconds,
but their `duration` fields differ. Use the [client result contract](../examples/openai_api/CLIENTS.md#response-formats)
before consuming timestamps. `json` and `text` are simpler response choices.
Speaker processing via `spk=true` belongs to the packaged API, not the example's
request schema; see [speaker labels and identity limits](speaker_emotion.md).
## Workflow integrations
Use a multipart HTTP node with method `POST`, the transcription endpoint, a
binary file field named `file`, and text fields `model` and `response_format`.
A file URL in the `file` field is not equivalent to uploading audio bytes.
For Dify/n8n running in a container, `localhost` means that container, not the
FunASR host; configure a reachable service address behind the intended gateway.
- [Dify, n8n and webhook workers](../examples/openai_api/WORKFLOWS.md) provide request wiring examples.
- [JavaScript and TypeScript](../examples/openai_api/JAVASCRIPT.md) cover SDK and multipart clients.
- [Postman](../examples/openai_api/POSTMAN.md) and the [smoke test](../examples/openai_api/smoke_test.py) check a deployed endpoint.
- [Gradio](../examples/openai_api/GRADIO.md) provides a browser upload/microphone example.
- [OpenAPI](../examples/openai_api/OPENAPI.md) distinguishes the checked-in example schema from the deployed packaged schema.
Register transcription as a tool in the host framework using these request and
result boundaries. The URL worker example does not implement a complete secure
downloader: add destination allowlists, private-network blocking, redirect
validation, size limits and timeouts before accepting untrusted URLs. Use the
client result contract above instead of assuming the workflow field table fits
both server implementations. These recipes are not proof that every framework version has
been integrated or that untrusted download URLs are safe.
## MCP server
From the prepared repository root and environment:
```bash
python examples/mcp_server/funasr_mcp.py
```
Prepare PyTorch and a compatible audio-feature backend using the installation
guide before attempting transcription. Installing the toolkit or completing an
MCP handshake alone does not verify model execution; this script does not need
a separate MCP SDK package. An MCP client launches it over stdio, not HTTP.
Configure absolute paths to the prepared Python environment and checkout:
```json
{
"mcpServers": {
"funasr": {
"command": "/path/to/FunASR-agent/.venv/bin/python",
"args": ["/path/to/FunASR-agent/examples/mcp_server/funasr_mcp.py"],
"env": {
"FUNASR_DEVICE": "cpu",
"FUNASR_MODEL": "iic/SenseVoiceSmall"
}
}
}
}
```
`transcribe_audio` accepts an existing local `audio_path` visible to the server,
including a read-only mounted path in a container. It does not accept URLs or
live streams. The first call may download and load weights. The language hints
are `auto`, `zh`, `yue`, `en`, `ja`, and `ko`; setting `FUNASR_MODEL` does not
change that tool schema or guarantee another model is compatible with its VAD path.
The result is formatted MCP `content` with `type=text`, optionally including
segments, not the HTTP response object. Top-level transcript rich tags are
removed; optional segment text is copied from model output. `FUNASR_DEVICE`
defaults to `cpu`, and `FUNASR_MODEL` defaults to `iic/SenseVoiceSmall`.
See the [MCP source and container setup](../examples/mcp_server/README.md) for
client configuration and filesystem mounts. Control which files the assistant
and server can access; a local tool is not a filesystem authorization boundary.
## Desktop voice input
With the HTTP server already running, use a second terminal in the prepared checkout:
```bash
python -m pip install sounddevice numpy pyperclip openai pynput
python examples/voice_input/funasr_input.py --server http://localhost:8000/v1 --model sensevoice
```
The script toggles recording, uploads WAV audio to the HTTP service, and copies
the transcript for pasting. Microphone permissions and audio-device support are
required; macOS may also require accessibility permissions, and Linux automatic
paste uses `xdotool`. Clipboard/paste behavior varies by desktop session.
The current `--lang` option is parsed but not forwarded to the transcription
request, so it is not an effective language control in this path.
A remote `--server` sends the recorded audio to that endpoint. Do not treat this
as an unconditional offline/privacy guarantee or a measured latency promise.
See [configuration](../examples/voice_input/README.md#配置选项) and the
[implementation](../examples/voice_input/funasr_input.py) before deployment.
## Subtitle generation
This is a local `AutoModel` pipeline, not an HTTP or MCP client. From the prepared
checkout, with local input files and a suitable inference environment:
```bash
python examples/subtitle/generate_subtitle.py video.mp4
python examples/subtitle/generate_subtitle.py meeting.wav --spk
python examples/subtitle/generate_subtitle.py podcast.mp3 --format vtt
python examples/subtitle/generate_subtitle.py audio.wav --device cpu
```
The default device is CUDA; the last command explicitly selects CPU. The default
model is SenseVoiceSmall with VAD and punctuation. This fixed pipeline is not a
generic recipe for arbitrary models. `--spk` adds
CAM++ anonymous labels, not verified identities. `--format` selects SRT or VTT;
`--output` chooses a path, and existing output files are overwritten. Choose a
new output path when preserving prior subtitles. `--lang` passes a non-auto language hint to generation.
`--max-single-segment-time` is in milliseconds, with current default `60000`.
`--segment-mode readable` groups display cues without rewriting recognized
text or punctuation; `sentence` retains raw model sentence grouping. Neither
mode fixes punctuation errors or guarantees phonetic boundaries. Check actual
timestamp availability and playback against the original audio: a missing timing
result can fall back to a zero-duration `(0, 0)` interval, not a validated subtitle.
Input decoding,
model/dependency loading and GPU capacity still require environment-specific
validation. See [subtitle options](../examples/subtitle/README.md#options) and
the [speaker guide](speaker_emotion.md) for output interpretation.
+201
View File
@@ -0,0 +1,201 @@
# FunASR Agent 集成
[English](agent_integration.md)
按应用需要选择接口:HTTP 文件转写、本地 MCP 工具、桌面录音或本地字幕流水线。
这些路径的模型、选项和输出字段并不完全相同。直接在进程内推理请使用
[Python SDK](python_api_zh.md)。
## HTTP 服务
以下源码安装起点包含本页后续用到的示例脚本,命令使用 POSIX shell。
使用新目录和虚拟环境;仅安装
PyPI 包不会安装这些仓库示例。
```bash
git clone https://github.com/modelscope/FunASR.git FunASR-agent
cd FunASR-agent
git checkout --detach e19029adca384a06a2f60bd8c18cb98f1a0499aa
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install -e .
python -m pip install fastapi uvicorn python-multipart
python -m pip check
```
这只固定源码,不会锁定所有依赖和模型权重。按照[安装指南](installation/installation_zh.md)
准备 CPU/GPU 环境,记录实际依赖版本和模型版本,再验证真实请求。
`pip check` 不能代替 CUDA、音频解码或全新环境安装测试。
在该环境中选择以下**一条**命令运行。CPU 示例显式选择 SenseVoice;CUDA 方案需要
可用的 GPU 环境。保持服务终端运行,在另一个已准备好的终端执行客户端命令。
```bash
funasr-server --host 127.0.0.1 --device cpu --model sensevoice --port 8000
# 备选:使用相同端口前先停止 CPU 服务。
funasr-server --host 127.0.0.1 --device cuda --model sensevoice --port 8000
```
```bash
curl -fsS http://localhost:8000/health
curl -fsS http://localhost:8000/v1/models
```
上传文件使用 `/v1/audio/transcriptions`,运行中服务的 schema 位于 `/openapi.json`,
Swagger UI 位于 `/docs`。这个服务的 `/docs` 不是 FunASR 官网文档目录。
健康检查或模型列表返回成功,不能代替目标模型的真实转写验收。
打包的 `funasr-server` 与[示例 HTTP 服务](../examples/openai_api/README_zh.md#api-contract)
具有不同的默认值、别名和响应结构。启动和请求时都应显式指定 `model`。
`paraformer-en` 是示例服务的注册别名,不是打包服务内置别名。
SenseVoice 的 HTTP 展示文本已清除富标签,不是专用的情感或事件输出 API。
Nano 与 MLT-Nano 的语种和时间戳能力应按[模型选择指南](model_selection_zh.md)
及实际运行路径区分,不能从 HTTP 接口名称推断。打包服务的自定义模型应通过
`--model-path` 配合正确的 `--hub` 加载,并在请求中指定 `model=custom`;
任意模型 ID 不会自动成为内置 `--model` 别名。
[MOSS-Transcribe-Diarize](moss_transcribe_diarize_zh.md) 是 OpenMOSS 的第三方模型,
有独立部署要求。其原生匿名说话人标签不需要外接 VAD 或说话人模型,不要叠加这些阶段,
也不要假设所有客户端都能呈现其全部输出。
本地服务不会认证下方 SDK 的占位 key。对外提供网络访问前,按
[安全指南](../examples/openai_api/SECURITY_zh.md) 配置 TLS、鉴权、上传大小及速率限制。
CORS 不是身份认证。
## SDK 与 curl
在客户端环境单独安装 OpenAI HTTP 客户端:
```bash
python -m pip install openai
```
```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="local-development")
with open("meeting.wav", "rb") as audio:
result = client.audio.transcriptions.create(
model="sensevoice",
file=audio,
response_format="verbose_json",
)
print(result.text)
for segment in getattr(result, "segments", []):
print(segment)
```
```bash
curl -fsS http://localhost:8000/v1/audio/transcriptions \
-F file=@audio.wav \
-F model=sensevoice \
-F response_format=verbose_json
```
`verbose_json` 只选择格式,不会开启说话人分离、恢复富标签或保证词级对齐。
示例服务把已有 `sentence_info` 转为 segments,没有时返回空列表;打包服务可能生成
粗粒度回退分段。二者的分段时间均为秒,但 `duration` 字段语义不同。
使用时间戳前请阅读[客户端输出契约](../examples/openai_api/CLIENTS.md#response-formats)。
`json` 和 `text` 是更简单的响应格式。`spk=true` 的说话人处理属于打包 API,
不属于示例服务的请求 schema;另见[说话人标签与身份边界](speaker_emotion_zh.md)。
## 工作流集成
HTTP 节点使用 `POST` 转写端点和 multipart 请求体:二进制文件字段为 `file`,
文本字段为 `model` 与 `response_format`。把音频 URL 写入 `file` 字段不等于上传音频字节。
当 Dify/n8n 运行在容器中时,`localhost` 指该容器而非 FunASR 主机,
应配置经过预期网关、工作流实际可达的服务地址。
- [Dify、n8n 与 webhook worker](../examples/openai_api/WORKFLOWS_zh.md) 提供请求连接示例。
- [JavaScript 和 TypeScript](../examples/openai_api/JAVASCRIPT_zh.md) 提供 SDK 和 multipart 客户端。
- [Postman](../examples/openai_api/POSTMAN_zh.md) 与[冒烟测试](../examples/openai_api/smoke_test.py) 检查已部署端点。
- [Gradio](../examples/openai_api/GRADIO_zh.md) 提供浏览器上传及麦克风示例。
- [OpenAPI](../examples/openai_api/OPENAPI_zh.md) 区分仓库内示例 schema 与已部署打包服务的 schema。
在宿主框架中按这些请求和输出边界注册转写工具。URL worker 示例不是完整的安全下载器;
接受不可信 URL 前,应补充目标白名单、私网阻断、重定向校验、大小限制和超时。
响应字段以上方客户端契约为准,不要假设工作流字段表适用于两种服务实现。
这些示例不代表所有框架版本均已完成
集成验证,也不代表任意不可信下载 URL 都是安全的。
## MCP 服务
在已准备好的仓库根目录和环境中运行:
```bash
python examples/mcp_server/funasr_mcp.py
```
执行转写前,应按安装指南准备 PyTorch 和兼容的音频特征后端。
安装工具包或完成 MCP 握手,都不等于模型执行已通过验证;此脚本不需要额外安装 MCP SDK。
MCP 客户端通过 stdio 启动此脚本,它不是 HTTP 监听服务。
配置时使用已准备好的 Python 环境与仓库绝对路径:
```json
{
"mcpServers": {
"funasr": {
"command": "/path/to/FunASR-agent/.venv/bin/python",
"args": ["/path/to/FunASR-agent/examples/mcp_server/funasr_mcp.py"],
"env": {
"FUNASR_DEVICE": "cpu",
"FUNASR_MODEL": "iic/SenseVoiceSmall"
}
}
}
}
```
`transcribe_audio` 接受服务端可见、已存在的本地 `audio_path`,也可以是容器中
只读挂载的路径,不接受 URL 或实时流。首次调用可能下载并加载权重。
语种提示为 `auto`、`zh`、`yue`、`en`、`ja`、`ko`;设置 `FUNASR_MODEL` 不会改变
工具 schema,也不能保证另一模型兼容该工具的 VAD 路径。
结果被格式化为 MCP `content` 中的 `type=text`,可包含分段,而不是 HTTP 响应对象。
顶层转写文本会清除富标签;可选的分段文本来自模型输出。
`FUNASR_DEVICE` 默认为 `cpu`,`FUNASR_MODEL` 默认为 `iic/SenseVoiceSmall`。
[MCP 源码与容器说明](../examples/mcp_server/README.md) 提供客户端配置和文件挂载方法。
控制助手及服务端可访问的文件;本地工具本身不是文件系统权限隔离机制。
## 桌面语音输入
保持 HTTP 服务运行,在已准备好的仓库内打开另一个终端:
```bash
python -m pip install sounddevice numpy pyperclip openai pynput
python examples/voice_input/funasr_input.py --server http://localhost:8000/v1 --model sensevoice
```
脚本切换录音状态,将 WAV 上传至 HTTP 服务,再复制转写结果供粘贴。
需要麦克风权限和音频设备支持;macOS 还可能需要辅助功能权限,Linux 自动粘贴使用
`xdotool`,剪贴板及粘贴行为随桌面会话而异。当前 `--lang` 虽然被解析,却没有传入
转写请求,因此在此路径中不是有效的语种控制项。
远程 `--server` 会把录音发送给该端点,不能无条件宣称完全离线、音频不离开本机或
达到固定延迟。部署前请阅读[配置选项](../examples/voice_input/README.md#配置选项)
及[实现](../examples/voice_input/funasr_input.py)。
## 字幕生成
这是本地 `AutoModel` 流水线,不是 HTTP 或 MCP 客户端。
在已准备好的仓库中,使用本地输入文件及适合的推理环境:
```bash
python examples/subtitle/generate_subtitle.py video.mp4
python examples/subtitle/generate_subtitle.py meeting.wav --spk
python examples/subtitle/generate_subtitle.py podcast.mp3 --format vtt
python examples/subtitle/generate_subtitle.py audio.wav --device cpu
```
默认设备为 CUDA,最后一条命令显式选择 CPU。默认模型为 SenseVoiceSmall,并使用
VAD 和标点模型;这套固定流水线不是任意模型的通用配方。`--spk` 添加 CAM++ 匿名标签,
不验证真实身份。`--format` 选择 SRT/VTT,`--output` 指定输出路径,已有输出文件会被覆盖;
需要保留旧字幕时应指定新路径。`--lang` 将非 auto
语种提示传给推理。`--max-single-segment-time` 的单位是毫秒,当前默认 `60000`。
`--segment-mode readable` 对展示字幕分组,不改写识别文本或标点;`sentence` 保留
原始模型句段分组。两种模式都不修复标点错误,也不保证音素级边界。
应检查实际时间戳是否可用,并对照原音频回放;缺少时间信息时可能回退到零时长 `(0, 0)`
区间,这不是经过验证的字幕。输入解码、模型与依赖加载、GPU 容量
仍需针对环境验证。输出语义见[字幕选项](../examples/subtitle/README.md#options)
及[说话人指南](speaker_emotion_zh.md)。
+2
View File
@@ -69,6 +69,8 @@ entry to these guides. Source Markdown remains in this repository.
:maxdepth: 1
:caption: Applications and Ecosystem
agent_integration
agent_integration_zh
use_case_showcase
use_case_showcase_zh
community_projects
@@ -34,6 +34,7 @@
{"slug": "security", "group": "operate", "zh": "服务安全边界", "en": "Service security boundaries", "source_zh": "examples/openai_api/SECURITY_zh.md", "source_en": "examples/openai_api/SECURITY.md"},
{"slug": "benchmark-method", "group": "operate", "zh": "可复现性能评测", "en": "Reproducible performance tests", "source_zh": "docs/benchmark/rtf_reproducibility.md", "source_en": "docs/benchmark/rtf_reproducibility.md"},
{"slug": "realtime-benchmark", "group": "operate", "zh": "实时 WebSocket 压测", "en": "Realtime WebSocket benchmarking", "source_zh": "docs/benchmark/realtime_ws_benchmark.md", "source_en": "docs/benchmark/realtime_ws_benchmark.md"},
{"slug": "agent-integration", "group": "ecosystem", "zh": "Agent 集成", "en": "Agent integration", "source_zh": "docs/agent_integration_zh.md", "source_en": "docs/agent_integration.md"},
{"slug": "use-cases", "group": "ecosystem", "zh": "应用场景与示例", "en": "Use cases & examples", "source_zh": "docs/use_case_showcase_zh.md", "source_en": "docs/use_case_showcase.md"},
{"slug": "community", "group": "ecosystem", "zh": "社区集成", "en": "Community integrations", "source_zh": "docs/community_projects_zh.md", "source_en": "docs/community_projects.md"},
{"slug": "repositories", "group": "ecosystem", "zh": "四仓职责与贡献入口", "en": "Repositories & contribution paths", "source_zh": "docs/repository_roles_zh.md", "source_en": "docs/repository_roles.md"},
@@ -140,6 +140,22 @@
"checklist": "readiness-checklist",
"help": "when-to-open-an-issue"
},
"agent.html": {
"server": "http-server",
"sdk": "sdk-and-curl",
"workflows": "workflow-integrations",
"mcp": "mcp-server",
"voice": "desktop-voice-input",
"subtitle": "subtitle-generation"
},
"zh/agent.html": {
"server": "http-服务",
"sdk": "sdk-与-curl",
"workflows": "工作流集成",
"mcp": "mcp-服务",
"voice": "桌面语音输入",
"subtitle": "字幕生成"
},
"zh/deployment-matrix.html": {
"matrix": "快速决策表",
"choices": "常见选择",
+1
View File
@@ -17,6 +17,7 @@ ALIASES = {
'moss-transcribe-diarize': 'moss-transcribe-diarize.html',
'vllm': 'vllm.html',
'deployment-matrix': 'deployment-matrix.html',
'agent-integration': 'agent.html',
}
@@ -92,3 +92,37 @@ test('public speech sample is playable and waveform has actual pixels', async ({
await page.locator('audio').evaluate((audio: HTMLAudioElement) => audio.pause());
expect(await page.locator('.hero-image').evaluate((image: HTMLImageElement) => image.naturalWidth)).toBeGreaterThan(1000);
});
for (const prefix of ['', '/en']) {
for (const width of [390, 1440]) {
test(`Agent integration is discoverable and readable ${prefix || '/zh'} ${width}`, async ({ page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
await page.setViewportSize({ width, height: 900 });
const errors: string[] = [];
page.on('pageerror', error => errors.push(error.message));
await page.goto(`${prefix}/docs/`);
await page.locator('[data-doc-search] input').fill('Agent');
const route = `${prefix}/docs/agent-integration.html`;
await page.locator(`.search-results a[href="${route}"]`).click();
await expect(page).toHaveURL(new RegExp(`${route}$`));
await expect(page.locator('[data-source-link]')).toHaveAttribute('href',
new RegExp(`/docs/agent_integration${prefix ? '' : '_zh'}.md$`));
await expect(page.locator('.docs-article h2')).toHaveCount(6);
const block = page.locator('.docs-article pre').first();
const expected = await block.locator('code').innerText();
await block.locator('button').click();
expect((await page.evaluate(() => navigator.clipboard.readText())).trim()).toBe(expected.trim());
for (const heading of await page.locator('.docs-article h2').all()) {
await heading.scrollIntoViewIfNeeded();
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1);
}
const token = page.locator('.docs-article p code').filter({ hasText: 'verbose_json' }).first();
await expect(token).toBeVisible();
expect((await token.boundingBox())!.height).toBeLessThan(40);
const peer = `${prefix ? '' : '/en'}/docs/agent-integration.html`;
await page.locator(`.docs-article a[href="${peer}"]`).click();
await expect(page).toHaveURL(new RegExp(`${peer}$`));
expect(errors).toEqual([]);
});
}
}
@@ -1,7 +1,10 @@
from pathlib import Path
import ast
import json
import re
import shlex
import sys
from urllib.parse import unquote
from urllib.parse import unquote, urlsplit
from bs4 import BeautifulSoup
import pytest
@@ -121,3 +124,88 @@ def test_github_pages_hubs_have_task_navigation_and_search_handoff():
assert soup.select_one('a[href$="moss-transcribe-diarize.html"]')
assert soup.select_one('a[href$="api.html"]')
assert '170x' not in soup.get_text()
@pytest.mark.parametrize('language, prefix, suffix, peer', [
('zh', '', '_zh', '/en/docs/agent-integration.html'),
('en', 'en/', '', '/docs/agent-integration.html'),
])
def test_agent_guide_has_shared_sources_discovery_and_six_sections(output, language, prefix, suffix, peer):
route = '/' + prefix + 'docs/agent-integration.html'
path = output / route.lstrip('/')
assert path.is_file(), route
page = BeautifulSoup(path.read_text(), 'html.parser')
article = page.select_one('.docs-article')
assert len(article.select('h2')) == 6
assert page.select_one('[data-source-link]')['href'].endswith(f'/docs/agent_integration{suffix}.md')
links = {a['href'] for a in article.select('a[href]')}
assert peer in links
routes = {urlsplit(link)._replace(fragment='').geturl() for link in links}
for slug in ('http-server', 'workflows', 'javascript', 'speaker-emotion', 'model-selection', 'security'):
assert '/' + prefix + 'docs/' + slug + '.html' in routes
index = BeautifulSoup((output / prefix / 'docs/index.html').read_text(), 'html.parser')
assert index.select_one(f'a[href="{route}"]')
search = json.loads((output / f'search-{language}.json').read_text())
assert any(row['url'] == route and 'Agent' in row['title'] for row in search)
text = article.get_text(' ', strip=True)
for marker in ('127.0.0.1', '/v1/audio/transcriptions', '/v1/models', '/health',
'verbose_json', 'transcribe_audio', 'FUNASR_DEVICE',
'FUNASR_MODEL', 'sentence_info', '--lang', '--segment-mode', '60000'):
assert marker in text, (language, marker)
assert '31-language LLM-based ASR with timestamps' not in text
assert '31 语种 LLM-based ASR,支持时间戳' not in text
def test_agent_recipes_are_bilingual_and_use_existing_script_options():
root = SITE_ROOT.parents[1]
recipes = []
for suffix in ('', '_zh'):
path = root / f'docs/agent_integration{suffix}.md'
assert path.is_file(), path
source = path.read_text()
blocks = re.findall(r'^```(\w+)\n(.*?)^```', source, re.M | re.S)
commands = []
structured = []
for language, code in blocks:
if language == 'python':
tree = ast.parse(code)
compile(tree, str(path), 'exec')
assert any(isinstance(node, ast.With) for node in ast.walk(tree))
structured.append((language, ast.dump(tree)))
elif language == 'json':
data = json.loads(code)
structured.append((language, data))
config = data['mcpServers']['funasr']
assert config['command'].endswith('/bin/python')
assert config['args'][0].endswith('/examples/mcp_server/funasr_mcp.py')
assert config['env']['FUNASR_DEVICE'] == 'cpu'
elif language == 'bash':
commands.extend(tokens for line in code.replace('\\\n', ' ').splitlines()
if (tokens := shlex.split(line, comments=True)))
recipes.append({'shell': commands, 'structured': structured})
scripts = []
for command in commands:
if command[0] == 'funasr-server':
assert command[command.index('--host') + 1] == '127.0.0.1'
assert command[command.index('--model') + 1] == 'sensevoice'
script = root / 'funasr/bin/server.py'
elif command[0] == 'python' and command[1].startswith('examples/'):
script = root / command[1]
scripts.append(command[1])
else:
continue
assert script.is_file(), script
tree = ast.parse(script.read_text())
options = {arg.value for node in ast.walk(tree)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)
and node.func.attr == 'add_argument' for arg in node.args
if isinstance(arg, ast.Constant) and isinstance(arg.value, str)}
for token in command[1:]:
if token.startswith('--'):
assert token in options, (script, token)
if script.name == 'funasr_input.py':
assert '--lang' not in command
assert set(scripts) == {'examples/mcp_server/funasr_mcp.py',
'examples/voice_input/funasr_input.py',
'examples/subtitle/generate_subtitle.py'}
assert recipes[0] == recipes[1]
@@ -19,6 +19,8 @@ OLD_IDS = {
'moss-transcribe-diarize.html': 'contract server runtimes evidence boundaries',
'vllm.html': 'overview install offline streaming-sdk websocket dynamic-vad performance api-reference faq',
'deployment-matrix.html': 'matrix choices runtime-diagnostics checklist help',
# Independently inventoried from public EN/ZH pages on 2026-09-07.
'agent.html': 'server sdk workflows mcp voice subtitle',
}
PAGES = [prefix + name for name in OLD_IDS for prefix in ('', 'zh/')]
@@ -138,6 +140,17 @@ def test_insertion_is_idempotent_and_keeps_existing_anchors():
assert soup.find(id='existing').name == 'a'
@pytest.mark.parametrize('page, targets', [
('agent.html', ['http-server', 'sdk-and-curl', 'workflow-integrations',
'mcp-server', 'desktop-voice-input', 'subtitle-generation']),
('zh/agent.html', ['http-服务', 'sdk-与-curl', '工作流集成',
'mcp-服务', '桌面语音输入', '字幕生成']),
])
def test_agent_legacy_fragments_keep_their_topics(page, targets):
pages = json.loads((SITE / 'data/legacy_doc_anchors.json').read_text())['pages']
assert pages[page] == dict(zip('server sdk workflows mcp voice subtitle'.split(), targets))
@pytest.mark.parametrize('heading', ('<h2 id="different">Topic</h2>', '<p id="topic">Not a heading</p>'))
def test_missing_or_nonheading_target_fails_explicitly(heading):
from export_docs import insert_legacy_anchors