docs: align deployment matrix recipes and fix copy icon contrast

Signed-off-by: zhifu gao <[email protected]>
This commit is contained in:
zhifu gao
2026-09-07 22:10:59 +00:00
parent 04278203a6
commit cdb6683516
7 changed files with 168 additions and 22 deletions
+13 -6
View File
@@ -10,7 +10,7 @@ Use this page to choose the shortest deployment path for a product, demo, benchm
| Python API | Notebooks, offline jobs, first model evaluation | [README quick start](../README.md#quick-start) | Lowest ceremony; caller owns batching, retries, and files. |
| OpenAI-compatible API | Private speech API, agents, Dify/LangChain/AutoGen-style clients | [OpenAI API example](../examples/openai_api/) | Easiest integration for apps that already support OpenAI audio APIs. |
| Xinference | Teams that already operate Xinference model serving | [Xinference repository](https://github.com/xorbitsai/inference) | Use a Xinference version containing [xorbitsai/inference#5140](https://github.com/xorbitsai/inference/pull/5140) so Fun-ASR-Nano uses packaged `funasr~=1.3.0` instead of the old pinned git commit. |
| Docker Compose API | Reproducible local smoke test or small internal service | [OpenAI API Docker docs](../examples/openai_api/#docker-deployment) | CPU by default; adapt the image before using CUDA in containers. |
| Docker Compose API | Local smoke test or small internal service | [OpenAI API Docker docs](../examples/openai_api/#docker-deployment) | CPU by default; adapt the image before using CUDA in containers. |
| Kubernetes API | Internal speech API for cluster services | [Kubernetes template](../examples/openai_api/kubernetes/) | Starts as private `ClusterIP`; add auth, TLS, network policy, and GPU scheduling before broader exposure. |
| Runtime WebSocket service | Live captions, meetings, call-center streams | [Runtime service docs](../runtime/readme.md) | Use when partial results, endpointing, or long-lived audio streams matter. |
| ONNX/C++ runtime | High-concurrency CPU services or embedded realtime ASR | [ONNX runtime docs](../runtime/onnxruntime/readme.md) | Keep this path when latency/concurrency is already proven; add text post-processing for fixed business terms before moving to GPU LLMs. |
@@ -43,15 +43,22 @@ OpenAI API example above instead.
### I want a repeatable container demo
Use `examples/openai_api/docker-compose.yml` for a CPU-mode smoke test:
From the **FunASR repository root**, with Docker Engine and the Docker Compose plugin available, start a local SenseVoice CPU service. This command does not overwrite an existing `.env`; its explicit settings override inherited port, device and model values for this launch. The host listener is loopback only, not an authentication gateway. Review the [security and gateway guide](../examples/openai_api/SECURITY.md) before sharing it.
```bash
cd examples/openai_api
cp .env.example .env
docker compose up --build
FUNASR_HOST_PORT=127.0.0.1:8000 FUNASR_DEVICE=cpu FUNASR_MODEL=sensevoice \
docker compose -f examples/openai_api/docker-compose.yml up --build
```
Keep CPU mode until you have a CUDA-capable PyTorch/FunASR image. After that, set `FUNASR_DEVICE=cuda` and verify with the same smoke test. Use `python examples/openai_api/smoke_test.py --base-url http://localhost:8000` on systems without bash/curl.
Keep Compose running in that terminal. Open a **second terminal at the same repository root** and use Python 3.10 or newer; this smoke client only needs the standard library, not a host FunASR installation:
```bash
python3 examples/openai_api/smoke_test.py --base-url http://127.0.0.1:8000 --model sensevoice --response-format verbose_json
```
The client downloads a public Chinese sample only if `sample.wav` is missing in the current directory; an existing file is reused. It prints health, model metadata and transcription JSON. Inspect the text yourself: a successful exit does not certify recognition quality or concurrency. The client sends no Authorization, and the security guide's transcription-only gateway denies the metadata routes used by this smoke test. Keep the request on the local endpoint and avoid retaining sensitive audio or unredacted output.
This CPU image copies the example server but installs FunASR from PyPI. It is not a locked dependency environment; record the resolved package versions and image digest before making reproducibility claims. Changing `FUNASR_DEVICE` alone does not add CUDA dependencies or container GPU access. Prepare and validate a GPU image and scheduling separately; use the [complete HTTP deployment guide](../examples/openai_api/README.md#docker-deployment) and the selected model's serving requirements.
### I want an internal Kubernetes service
+12 -5
View File
@@ -48,15 +48,22 @@ OpenAI 互換 API を使います。主な入口は次のとおりです。
### 再現可能な container demo が欲しい
`examples/openai_api/docker-compose.yml` を CPU mode の smoke test として使います。
Docker Engine と Docker Compose plugin を用意し、**FunASR リポジトリのルート**からローカルの SenseVoice CPU service を起動します。このコマンドは既存の `.env` を上書きしません。明示した port、device、model は、この起動に限り環境から継承した値より優先されます。ホスト側は loopback のみで待ち受けますが、認証 gateway ではありません。共有前に [security guide(英語)](../examples/openai_api/SECURITY.md) を確認してください。
```bash
cd examples/openai_api
cp .env.example .env
docker compose up --build
FUNASR_HOST_PORT=127.0.0.1:8000 FUNASR_DEVICE=cpu FUNASR_MODEL=sensevoice \
docker compose -f examples/openai_api/docker-compose.yml up --build
```
CUDA を使う場合は CUDA-capable PyTorch/FunASR image を作成してから `FUNASR_DEVICE=cuda` に変更し、同じ smoke test で確認します。
Compose はそのターミナルで実行したままにします。**別のターミナルを開き、同じリポジトリのルート**で Python 3.10 以降を使って確認します。smoke client は標準ライブラリのみを使うため、ホストへの FunASR のインストールは不要です。
```bash
python3 examples/openai_api/smoke_test.py --base-url http://127.0.0.1:8000 --model sensevoice --response-format verbose_json
```
現在のディレクトリに `sample.wav` がなければ公開の中国語音声をダウンロードし、既存ファイルがあれば再利用します。health、model metadata、文字起こし JSON が表示されます。テキストは自分で確認してください。終了コードの成功だけでは認識精度や同時実行性能は検証できません。client は Authorization を送信せず、security guide の文字起こし専用 gateway はこの smoke が使う metadata route を拒否します。ローカル endpoint で実行し、機密音声や未加工の出力を残さないでください。
CPU image は example server をコピーしますが、FunASR は PyPI からインストールします。依存バージョンは固定されていません。再現性を主張する前に実際の package version と image digest を記録してください。`FUNASR_DEVICE` の変更だけでは CUDA 依存や container の GPU access は追加されません。GPU image と scheduling は別途用意して検証し、[HTTP deployment guide(英語)](../examples/openai_api/README.md#docker-deployment) とモデルの要件を確認してください。
### Streaming または live captioning が必要
+12 -5
View File
@@ -48,15 +48,22 @@ OpenAI 호환 API를 사용하세요. 주요 진입점은 다음과 같습니다
### 재현 가능한 container demo가 필요하다
`examples/openai_api/docker-compose.yml`을 CPU mode smoke test로 사용합니다.
Docker Engine과 Docker Compose plugin을 준비한 뒤 **FunASR 저장소 루트**에서 로컬 SenseVoice CPU service를 시작하세요. 이 명령은 기존 `.env`를 덮어쓰지 않습니다. 명시한 port, device, model 값은 이번 실행에서 상속된 환경 값보다 우선합니다. 호스트 listener는 loopback에만 바인딩되지만 인증 gateway는 아닙니다. 공유하기 전에 [security guide(영문)](../examples/openai_api/SECURITY.md)를 확인하세요.
```bash
cd examples/openai_api
cp .env.example .env
docker compose up --build
FUNASR_HOST_PORT=127.0.0.1:8000 FUNASR_DEVICE=cpu FUNASR_MODEL=sensevoice \
docker compose -f examples/openai_api/docker-compose.yml up --build
```
CUDA를 사용하려면 CUDA-capable PyTorch/FunASR image를 만든 뒤 `FUNASR_DEVICE=cuda`로 바꾸고 같은 smoke test로 확인하세요.
Compose는 해당 터미널에서 계속 실행합니다. **두 번째 터미널을 열고 같은 저장소 루트**에서 Python 3.10 이상으로 확인하세요. smoke client는 표준 라이브러리만 사용하므로 호스트에 FunASR를 설치할 필요가 없습니다.
```bash
python3 examples/openai_api/smoke_test.py --base-url http://127.0.0.1:8000 --model sensevoice --response-format verbose_json
```
현재 디렉터리에 `sample.wav`가 없을 때만 공개 중국어 음성을 다운로드하며, 기존 파일이 있으면 재사용합니다. health, model metadata, 전사 JSON을 출력합니다. 텍스트를 직접 확인하세요. 종료 코드가 성공해도 인식 품질이나 동시 처리 성능을 검증한 것은 아닙니다. client는 Authorization을 보내지 않으며, security guide의 전사 전용 gateway는 이 smoke가 사용하는 metadata route를 거부합니다. 로컬 endpoint로 실행하고 민감한 음성이나 가리지 않은 출력을 남기지 마세요.
CPU image는 example server를 복사하지만 FunASR는 PyPI에서 설치합니다. 의존 버전은 고정되지 않으므로 재현성을 주장하기 전에 실제 package version과 image digest를 기록하세요. `FUNASR_DEVICE`만 변경해도 CUDA 의존성이나 container GPU access가 추가되지는 않습니다. GPU image와 scheduling은 별도로 준비하고 검증해야 합니다. [HTTP deployment guide(영문)](../examples/openai_api/README.md#docker-deployment)와 선택한 모델의 요구 사항을 확인하세요.
### Streaming 또는 live captioning이 필요하다
+13 -6
View File
@@ -10,7 +10,7 @@
| Python API | Notebook、离线任务、首次模型评测 | [README 快速开始](../README_zh.md#快速开始) | 最简单;调用方自己负责批处理、重试和文件管理。 |
| OpenAI 兼容 API | 私有语音 API、Agent、Dify/LangChain/AutoGen 风格客户端 | [OpenAI API 示例](../examples/openai_api/README_zh.md) | 已支持 OpenAI audio API 的应用最容易接入。 |
| Xinference | 已经使用 Xinference 统一管理模型服务的团队 | [Xinference 仓库](https://github.com/xorbitsai/inference) | 使用包含 [xorbitsai/inference#5140](https://github.com/xorbitsai/inference/pull/5140) 的版本或 commit,确保 Fun-ASR-Nano 使用打包发布的 `funasr~=1.3.0`,而不是旧的 git commit pin。 |
| Docker Compose API | 可复现本地 smoke test 或小型内部服务 | [OpenAI API Docker 文档](../examples/openai_api/README_zh.md) | 默认 CPU;容器里使用 CUDA 前需要先适配 CUDA-capable 镜像。 |
| Docker Compose API | 本地 smoke test 或小型内部服务 | [OpenAI API Docker 文档](../examples/openai_api/README_zh.md) | 默认 CPU;容器里使用 CUDA 前需要先适配 CUDA-capable 镜像。 |
| Kubernetes API | 集群内私有语音 API | [Kubernetes 模板](../examples/openai_api/kubernetes/README_zh.md) | 默认私有 `ClusterIP`;对外开放前补齐鉴权、TLS、网络策略和 GPU 调度。 |
| Runtime WebSocket 服务 | 实时字幕、会议、客服流式音频 | [Runtime 服务文档](../runtime/readme_cn.md) | 需要中间结果、断句或长连接音频流时选择。 |
| ONNX/C++ Runtime | 高并发 CPU 服务或嵌入式实时 ASR | [ONNX Runtime 文档](../runtime/onnxruntime/readme.md) | 如果延迟和并发已经验证,不要轻易替换;固定业务词优先做文本后处理。 |
@@ -37,15 +37,22 @@
### 我想要可复现的容器 demo
使用 `examples/openai_api/docker-compose.yml` 跑 CPU smoke test:
在 **FunASR 仓库根目录**,准备好 Docker Engine 和 Docker Compose 插件后,启动本地 SenseVoice CPU 服务。这条命令不会覆盖已有 `.env`,显式设置会在本次启动中覆盖环境里遗留的端口、设备和模型值。主机端口仅绑定 loopback,不是鉴权网关;共享前先阅读[安全与网关指南](../examples/openai_api/SECURITY_zh.md)。
```bash
cd examples/openai_api
cp .env.example .env
docker compose up --build
FUNASR_HOST_PORT=127.0.0.1:8000 FUNASR_DEVICE=cpu FUNASR_MODEL=sensevoice \
docker compose -f examples/openai_api/docker-compose.yml up --build
```
在没有 CUDA-capable PyTorch/FunASR 镜像前保持 CPU 模式。准备好 CUDA 镜像后,再设置 `FUNASR_DEVICE=cuda` 并用同一个 smoke test 验证。没有 bash/curl 时可运行 `python examples/openai_api/smoke_test.py --base-url http://localhost:8000`。
保持该终端中的 Compose 运行。打开**第二个终端,仍位于同一仓库根目录**,使用 Python 3.10 或更高版本运行;smoke 客户端仅依赖标准库,不需要在主机安装 FunASR:
```bash
python3 examples/openai_api/smoke_test.py --base-url http://127.0.0.1:8000 --model sensevoice --response-format verbose_json
```
客户端仅在当前目录不存在 `sample.wav` 时下载公开中文样本,已有文件会被复用。它打印 health、模型 metadata 和转写 JSON;请自行检查文本,退出码成功不代表识别质量或并发验收。客户端不发送 Authorization,安全指南的仅转写网关会拒绝该 smoke 使用的 metadata 路由。请通过本地端点运行,避免保留敏感音频和未脱敏输出。
CPU 镜像复制 example 服务,但从 PyPI 安装 FunASR,依赖环境未锁定。声称可复现前,请记录实际包版本和镜像 digest。单独修改 `FUNASR_DEVICE` 不会增加 CUDA 依赖或容器 GPU 访问能力;GPU 镜像与调度需要另外准备和验证,参见[完整 HTTP 部署指南](../examples/openai_api/README_zh.md#docker-部署)及所选模型的服务要求。
### 我想部署集群内服务
+70
View File
@@ -4,6 +4,7 @@ import argparse
import ast
import importlib.util
import json
import os
from pathlib import Path
import re
import shlex
@@ -1097,5 +1098,74 @@ class KubernetesDocsContract(unittest.TestCase):
self.assertIn(marker, text, path.name)
MATRIX_DOCS = [ROOT / "docs" / ("deployment_matrix" + suffix + ".md")
for suffix in ("", "_zh", "_ja", "_ko")]
class DeploymentMatrixDocsContract(unittest.TestCase):
def test_four_language_shell_recipes_match(self):
expected = shell_commands(MATRIX_DOCS[0].read_text(encoding="utf-8"))
for path in MATRIX_DOCS:
self.assertEqual(shell_commands(path.read_text(encoding="utf-8")), expected)
def test_compose_recipe_overrides_ambient_settings_in_real_shell(self):
expected = ["FUNASR_HOST_PORT=127.0.0.1:8000", "FUNASR_DEVICE=cpu", "FUNASR_MODEL=sensevoice",
"docker", "compose", "-f", "examples/openai_api/docker-compose.yml", "up", "--build"]
with tempfile.TemporaryDirectory() as directory:
docker = Path(directory) / "docker"
docker.write_text('#!/bin/sh\nprintf "%s\\n" "$FUNASR_HOST_PORT" "$FUNASR_DEVICE" '
'"$FUNASR_MODEL" "$PWD" "$@"\n', encoding="utf-8")
docker.chmod(0o700)
env = dict(os.environ, PATH=directory + os.pathsep + os.environ.get("PATH", ""),
FUNASR_HOST_PORT="0.0.0.0:9999", FUNASR_DEVICE="cuda", FUNASR_MODEL="auto")
for path in MATRIX_DOCS:
code = next(code for code in blocks(path.read_text(encoding="utf-8"), "bash")
if "docker compose" in code)
# Only the single reviewed launch is executed; the Docker binary is a capture fixture.
self.assertEqual(shell_commands("```bash\n" + code + "```\n"), [expected])
result = subprocess.run(["sh", "-c", code], cwd=ROOT, env=env,
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(), ["127.0.0.1:8000", "cpu", "sensevoice",
str(ROOT), "compose", "-f", "examples/openai_api/docker-compose.yml",
"up", "--build"])
self.assertTrue((ROOT / expected[-3]).is_file())
self.assertTrue((ROOT / expected[-3]).with_name("Dockerfile").is_file())
def test_second_terminal_smoke_uses_root_path_and_explicit_model(self):
source = ROOT / "examples/openai_api/smoke_test.py"
options = {ast.literal_eval(arg) for node in ast.walk(tree(source))
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)}
expected = ["python3", str(source.relative_to(ROOT)), "--base-url", "http://127.0.0.1:8000",
"--model", "sensevoice", "--response-format", "verbose_json"]
for path in MATRIX_DOCS:
commands = shell_commands(path.read_text(encoding="utf-8"))
self.assertEqual(len(commands), 2)
self.assertEqual(commands[1], expected)
self.assertLessEqual({arg for arg in expected[2:] if arg.startswith("--")}, options)
def test_compose_guidance_explains_scope_and_preserves_owned_links(self):
markers = {
"deployment_matrix.md": ("repository root", "second terminal", "does not overwrite", "PyPI",
"not a locked", "Authorization", "Python 3.10", "sample.wav"),
"deployment_matrix_zh.md": ("仓库根目录", "第二个终端", "不会覆盖", "PyPI", "未锁定",
"Authorization", "Python 3.10", "sample.wav"),
"deployment_matrix_ja.md": ("リポジトリのルート", "別のターミナル", "上書きしません", "PyPI",
"固定されていません", "Authorization", "Python 3.10", "sample.wav"),
"deployment_matrix_ko.md": ("저장소 루트", "두 번째 터미널", "덮어쓰지 않습니다", "PyPI",
"고정되지", "Authorization", "Python 3.10", "sample.wav"),
}
for path in MATRIX_DOCS:
text = path.read_text(encoding="utf-8")
for marker in markers[path.name]:
self.assertIn(marker, text, path.name)
security = "SECURITY_zh.md" if path.stem.endswith("_zh") else "SECURITY.md"
self.assertIn(f"](../examples/openai_api/{security})", text)
self.assertTrue((EXAMPLE / security).is_file())
self.assertNotIn("cp .env.example .env", text)
if __name__ == "__main__":
unittest.main()
@@ -1004,6 +1004,14 @@ td a {
filter: brightness(0) invert(1);
}
.copy-button:hover,
.copy-button:focus-visible,
.command-copy:hover,
.command-copy:focus-visible {
border-color: #718096;
background: #354357;
}
.security-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
@@ -0,0 +1,40 @@
import { expect, test } from '@playwright/test';
for (const prefix of ['', '/en']) {
for (const width of [320, 390, 1440]) {
for (const route of ['/', '/docs/deployment-matrix.html']) {
test(`copy icon contrast ${prefix || '/zh'} ${route} ${width}`, async ({ page, context }) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
await page.setViewportSize({ width, height: 900 });
await page.goto(prefix + route);
const button = page.locator('.copy-button, .command-copy').first();
await button.scrollIntoViewIfNeeded();
const check = async (state: string) => {
const style = await button.evaluate(node => ({
background: getComputedStyle(node).backgroundColor,
filter: getComputedStyle(node.querySelector('img')!).filter,
}));
expect(style.filter).toBe('brightness(0) invert(1)');
const rgb = style.background.match(/[\d.]+/g)!.map(Number);
expect(rgb.length === 3 || rgb[3] === 1).toBeTruthy();
const linear = rgb.slice(0, 3).map(channel => {
const value = channel / 255;
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
});
const luminance = linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722;
expect(1.05 / (luminance + 0.05), `${state}: ${style.background}`).toBeGreaterThanOrEqual(3);
};
await page.mouse.move(0, 0);
await check('normal');
await button.focus();
await expect(button).toBeFocused();
await check('focus');
await button.hover();
await check('hover');
await button.click();
await check('copied');
expect((await page.evaluate(() => navigator.clipboard.readText())).length).toBeGreaterThan(0);
});
}
}
}