docs: maintain Japanese and Korean Agent and benchmark guides

Signed-off-by: zhifu gao <[email protected]>
This commit is contained in:
zhifu gao
2026-09-07 13:59:15 +00:00
parent dd79f9dd5a
commit eb749c21b3
16 changed files with 1774 additions and 3 deletions
+8
View File
@@ -67,6 +67,14 @@ Model weights retain their individual licenses; the FunASR toolkit is MIT licens
## Build the Product Documentation
Japanese and Korean legacy guides have repository-owned sources:
[Agent 日本語](agent_integration_ja.md) / [한국어](agent_integration_ko.md),
[historical ASR 日本語](benchmark/historical_asr_ja.md) / [한국어](benchmark/historical_asr_ko.md).
The `localized_pages` catalogue exports them to their established `ja/agent.html`,
`ko/agent.html`, `ja/benchmark.html` and `ko/benchmark.html` GitHub Pages addresses.
Their old section links are retained. These translations do not create Japanese
or Korean product-site navigation or search; the full product site remains bilingual.
From the repository root:
```sh
+227
View File
@@ -0,0 +1,227 @@
# FunASR Agent 連携
[English](agent_integration.md) | [中文](agent_integration_zh.md) | [한국어](agent_integration_ko.md)
用途に合ったインターフェースを選んでください。HTTP によるファイル文字起こし、
ローカル MCP ツール、デスクトップ録音、ローカル字幕生成では、利用できるモデル、
オプション、出力フィールドが異なります。プロセス内で直接推論する場合は
[Python SDK(英語)](python_api.md)を参照してください。
## HTTP サーバー
以下は、後の節で使うサンプルスクリプトも含めたソースからの準備手順です。
POSIX シェルで、新しいチェックアウトと仮想環境を使ってください。
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.md)に従って CPU/GPU 環境を準備し、
実際のパッケージ・モデルのバージョンを記録して、対象モデルでリクエストを確認してください。
`pip check` だけでは CUDA、音声デコード、新規環境へのインストール成功を検証できません。
準備した環境で、次のコマンドの**どちらか一方**を実行します。
CPU の例では SenseVoice を明示的に指定しています。CUDA の例には動作する GPU 環境が必要です。
サーバーのターミナルを開いたままにし、クライアントは別の準備済みターミナルから実行してください。
```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
```
- ファイルアップロード先は `/v1/audio/transcriptions` です。
- 稼働中のサービスのスキーマは `/openapi.json` にあります。
- Swagger UI は `/docs` です。このパスは FunASR のウェブサイトの文書ディレクトリではありません。
ヘルスチェックやモデル一覧の応答だけでは、対象モデルによる文字起こしの成功は確認できません。
パッケージ付属の `funasr-server` と[サンプル HTTP サーバー(英語)](../examples/openai_api/README.md#api-contract)では、
デフォルト、モデルの別名、応答スキーマが異なります。起動時だけでなく、リクエストにも
`model` を指定してください。`paraformer-en` はサンプルサーバーの別名であり、
パッケージ付属サーバーの組み込み別名ではありません。
カスタムモデルは適切な `--hub` と `--model-path` で指定し、リクエストには
`model="custom"` を使います。任意のモデル ID がそのまま `--model` の組み込み別名になるわけではありません。
SenseVoice の HTTP 表示テキストからはリッチタグが除去されます。
これは感情・イベントを専用フィールドで返す API ではありません。
基本の Fun-ASR-Nano は中国語・英語・日本語と中国語方言・地域アクセントの評価経路であり、
31 言語の Fun-ASR-MLT-Nano は**別の checkpoint**です。
基本 Nano の範囲から韓国語対応を推測したり、HTTP インターフェースだけを根拠に
タイムスタンプを保証したりしないでください。
[モデル選択](model_selection_ja.md)と[デプロイ方式](deployment_matrix_ja.md)で実際の経路を確認してください。
[MOSS-Transcribe-Diarize(英語)](moss_transcribe_diarize.md)は OpenMOSS が提供する第三者モデルで、
独自のデプロイ要件があります。ネイティブの話者ラベルは録音内の匿名ラベルであり、実在する人物の識別ではありません。
外部 VAD や外部話者モデルは不要なので、これらの段階を追加しないでください。
すべてのクライアントがモデル出力を余さず表示するとも限りません。
ローカルサーバーは、以下の SDK の仮の API キーを認証しません。
ネットワークに公開する前に、[セキュリティガイド(英語)](../examples/openai_api/SECURITY.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 を作り、情報がなければ空リストを返します。
パッケージ付属サーバーは粗い代替区間を生成する場合があります。
両方とも segment の時刻は秒単位ですが、`duration` の意味は異なります。
時刻を利用する前に[クライアント応答の契約(英語)](../examples/openai_api/CLIENTS.md#response-formats)を確認してください。
`json` と `text` はより単純な応答形式です。
`spk=true` による話者処理はパッケージ付属 API の機能であり、サンプルサーバーのリクエストスキーマにはありません。
[話者ラベルと人物識別の違い(英語)](speaker_emotion.md)も参照してください。
### ワークフロー連携
Dify/n8n などの HTTP ノードでは、文字起こしエンドポイントに `POST` で multipart を送ります。
バイナリファイルのフィールド名は `file`、テキストフィールドは `model` と `response_format` です。
`file` に音声 URL を入れることは、音声のバイト列をアップロードすることとは異なります。
コンテナ内の `localhost` はそのコンテナ自身を指し、FunASR のホストではありません。
意図したゲートウェイを経由してアクセスできるサービスアドレスを設定してください。
- [Dify、n8n、webhook worker(英語)](../examples/openai_api/WORKFLOWS.md): リクエストの接続例。
- [JavaScript / TypeScript(英語)](../examples/openai_api/JAVASCRIPT.md): SDK と multipart クライアント。
- [Postman(英語)](../examples/openai_api/POSTMAN.md)と[スモークテスト](../examples/openai_api/smoke_test.py): デプロイ済みエンドポイントの確認。
- [Gradio(英語)](../examples/openai_api/GRADIO.md): ブラウザのアップロードとマイク入力。
- [OpenAPI(英語)](../examples/openai_api/OPENAPI.md): リポジトリ内サンプルのスキーマと稼働中のパッケージ付属サービスのスキーマの区別。
ホストフレームワークには、これらのリクエスト・応答の範囲に従って文字起こしツールを登録します。
URL worker の例は完全な安全対策を備えたダウンローダーではありません。
信頼できない URL を受け付ける前に、宛先の許可リスト、プライベートネットワークへのアクセス遮断、
リダイレクトの検証、サイズ制限、タイムアウトを追加してください。
ワークフローのフィールド表が両サーバーにそのまま当てはまるとは限らないため、上記の応答契約を使ってください。
これらの例は、すべてのフレームワークのバージョンで連携を検証した証拠でも、任意の URL の安全性を保証するものでもありません。
## MCP サーバー
準備済みのリポジトリのルートと環境から実行します。
```bash
python examples/mcp_server/funasr_mcp.py
```
文字起こしの前に、インストールガイドに従って PyTorch と互換性のある音声特徴抽出バックエンドを準備してください。
ツールキットのインストールや MCP のハンドシェイク成功だけでは、モデルの実行は検証できません。
このスクリプトに追加の MCP SDK パッケージは不要です。
MCP クライアントは HTTP ではなく stdio でスクリプトを起動します。
準備済み 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` を変更してもツールのスキーマは変わらず、別のモデルとその 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.md)を参照してください。
+222
View File
@@ -0,0 +1,222 @@
# FunASR Agent 연동
[English](agent_integration.md) | [中文](agent_integration_zh.md) | [日本語](agent_integration_ja.md)
애플리케이션에 필요한 인터페이스를 선택하세요. HTTP 파일 전사, 로컬 MCP 도구,
데스크톱 녹음, 로컬 자막 파이프라인은 지원 모델, 옵션, 반환 필드가 모두 같지 않습니다.
프로세스 안에서 직접 추론하려면 [Python SDK(영문)](python_api.md)를 사용하세요.
## HTTP 서버
다음은 뒤에서 사용할 예제 스크립트까지 포함하는 소스 기반 준비 절차입니다.
POSIX 셸에서 새 체크아웃과 가상 환경을 사용하세요.
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.md)에 따라 CPU/GPU 환경을 준비하고,
실제 패키지 및 모델 버전을 기록한 뒤 대상 모델로 요청을 검증하세요.
`pip check`만으로 CUDA, 오디오 디코더, 새 환경 설치가 정상임을 확인할 수는 없습니다.
준비된 환경에서 다음 명령 중 **하나만** 실행하세요. CPU 예제는 SenseVoice를 명시적으로 선택합니다.
CUDA 대안에는 작동하는 GPU 환경이 필요합니다. 서버 터미널을 유지하고,
클라이언트는 별도로 준비한 다른 터미널에서 실행하세요.
```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
```
- 파일 업로드에는 `/v1/audio/transcriptions`를 사용합니다.
- 실행 중인 서비스의 스키마는 `/openapi.json`에 있습니다.
- Swagger UI는 `/docs`에 있습니다. 이 경로는 FunASR 웹사이트의 문서 디렉터리가 아닙니다.
상태 확인이나 모델 목록 응답이 성공해도 대상 모델의 실제 전사가 성공했다는 뜻은 아닙니다.
패키지의 `funasr-server`와 [예제 HTTP 서버(영문)](../examples/openai_api/README.md#api-contract)는
기본값, 별칭, 응답 스키마가 다릅니다. 시작할 때뿐 아니라 요청에도 `model`을 지정하세요.
`paraformer-en`은 예제 서버의 별칭이며 패키지 서버의 내장 별칭이 아닙니다.
사용자 지정 모델은 적절한 `--hub`와 `--model-path`로 설정하고 요청에는 `model="custom"`을 사용하세요.
임의의 모델 ID가 자동으로 `--model`의 내장 별칭이 되는 것은 아닙니다.
SenseVoice의 HTTP 표시 텍스트에서는 리치 태그가 제거됩니다.
이 서비스는 감정이나 이벤트를 전용 필드로 반환하는 API가 아닙니다.
기본 Fun-ASR-Nano는 중국어·영어·일본어 및 중국어 방언·지역 억양의 평가 경로이고,
31개 언어를 다루는 Fun-ASR-MLT-Nano는 **별도 checkpoint**입니다.
기본 Nano의 범위가 한국어 지원을 뜻하지 않으며, HTTP 인터페이스만으로 타임스탬프를 보장할 수도 없습니다.
[모델 선택](model_selection_ko.md)과 [배포 방식](deployment_matrix_ko.md)에서 실제 경로를 확인하세요.
[MOSS-Transcribe-Diarize(영문)](moss_transcribe_diarize.md)는 OpenMOSS가 제공하는 제3자 모델이며
별도의 배포 요건이 있습니다. 네이티브 화자 라벨은 녹음 내 익명 라벨이지 실제 인물의 신원이 아닙니다.
외부 VAD나 외부 화자 모델이 필요하지 않으므로 해당 단계를 추가하지 마세요.
모든 클라이언트가 모델의 전체 출력을 표시한다고 가정해서도 안 됩니다.
로컬 서버는 아래 SDK의 임시 API 키를 인증하지 않습니다.
네트워크에 공개하기 전에 [보안 가이드(영문)](../examples/openai_api/SECURITY.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로
옮기고, 해당 정보가 없으면 빈 목록을 반환합니다. 패키지 서버는 대략적인 대체 구간을 만들 수 있습니다.
두 서버 모두 segment 시각을 초 단위로 반환하지만 `duration` 필드의 의미는 다릅니다.
시각을 사용하기 전에 [클라이언트 응답 계약(영문)](../examples/openai_api/CLIENTS.md#response-formats)을 확인하세요.
`json`과 `text`는 더 단순한 응답 형식입니다.
`spk=true` 화자 처리는 패키지 API에 속하며 예제 서버의 요청 스키마에는 없습니다.
[화자 라벨과 신원 식별의 차이(영문)](speaker_emotion.md)도 참고하세요.
## 워크플로 연동
Dify/n8n 등의 HTTP 노드에서 전사 엔드포인트로 `POST` multipart 요청을 보냅니다.
바이너리 파일 필드 이름은 `file`이고 텍스트 필드는 `model`과 `response_format`입니다.
`file` 필드에 오디오 URL을 넣는 것은 오디오 바이트를 업로드하는 것과 다릅니다.
컨테이너 안의 `localhost`는 FunASR 호스트가 아니라 해당 컨테이너 자신을 가리킵니다.
의도한 게이트웨이를 통해 접근 가능한 서비스 주소를 설정하세요.
- [Dify, n8n, webhook worker(영문)](../examples/openai_api/WORKFLOWS.md): 요청 연결 예제.
- [JavaScript / TypeScript(영문)](../examples/openai_api/JAVASCRIPT.md): SDK 및 multipart 클라이언트.
- [Postman(영문)](../examples/openai_api/POSTMAN.md)과 [스모크 테스트](../examples/openai_api/smoke_test.py): 배포된 엔드포인트 확인.
- [Gradio(영문)](../examples/openai_api/GRADIO.md): 브라우저 업로드 및 마이크 입력.
- [OpenAPI(영문)](../examples/openai_api/OPENAPI.md): 저장소 예제 스키마와 실행 중인 패키지 서비스 스키마의 차이.
이 요청 및 응답 범위에 맞춰 호스트 프레임워크에 전사 도구를 등록하세요.
URL worker 예제는 완전한 보안 기능을 갖춘 다운로더가 아닙니다.
신뢰할 수 없는 URL을 받기 전에 목적지 허용 목록, 사설 네트워크 접근 차단,
리디렉션 검증, 크기 제한과 타임아웃을 추가하세요.
워크플로 필드 표가 두 서버 모두에 그대로 적용된다고 가정하지 말고 위의 응답 계약을 사용하세요.
이 예제는 모든 프레임워크 버전의 연동이 검증되었다는 증거나 임의의 URL이 안전하다는 보장이 아닙니다.
## MCP 서버
준비된 저장소 루트와 환경에서 실행합니다.
```bash
python examples/mcp_server/funasr_mcp.py
```
전사 전에 설치 가이드에 따라 PyTorch와 호환되는 오디오 특징 추출 백엔드를 준비하세요.
툴킷 설치나 MCP 핸드셰이크 성공만으로 모델 실행이 검증되지는 않습니다.
이 스크립트에는 추가 MCP SDK 패키지가 필요하지 않습니다.
MCP 클라이언트가 HTTP가 아닌 stdio로 스크립트를 시작합니다.
준비된 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`을 바꾸어도 도구 스키마가 바뀌지 않으며 다른 모델이 해당 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, 0)` 구간으로 대체될 수 있으며, 이는 유효성이 검증된 자막이 아닙니다.
입력 디코딩, 모델 및 의존 패키지 로딩, GPU 용량은 환경별로 검증해야 합니다.
출력 해석은 [자막 옵션(영문)](../examples/subtitle/README.md#options)과
[화자 가이드(영문)](speaker_emotion.md)를 참고하세요.
+105
View File
@@ -0,0 +1,105 @@
# ASR ベンチマークの過去の記録
[English](historical_asr.md) | [中文](historical_asr_zh.md) | [한국어](historical_asr_ko.md)
このページは、以前の FunASR 比較を参照する読者のために、**出典情報が不完全な過去の記録**を保存したものです。
新しい測定、汎用ランキング、現在の checkpoint・機器・デプロイに対する保証ではありません。
対象データは**中国語の音声**であり、この日本語ページは日本語や韓国語の認識精度を測ったものではありません。
新しい評価を行う場合は[性能測定の方法(英語)](rtf_reproducibility.md)から始めてください。
## 過去の概要
下表は元の日本語ページの表現と数値を保持しています。
「最高」などの表現はその報告の範囲だけを指し、すべてのモデルやハードウェアには適用できません。
| 指標 | 結果 |
| --- | --- |
| データセット | 中国語の長時間音声 184 ファイル、合計 11,539 秒、192.3 分。 |
| GPU | NVIDIA H100 80GB HBM3. |
| 最高 GPU 速度 | SenseVoice-Small: 169.6x realtime in the full benchmark, 211.8x in the initial run. |
| 最高 CPU 速度 | SenseVoice-Small: 17.2x realtime; Paraformer-Large: 15.6x realtime. |
| ベースライン | OpenAI Whisper-large-v3: 13.4x realtime on GPU. |
**全体実行の 169.6x と初回実行の 211.8x は、別々に報告された結果**です。
元のページは測定日を明記していません。**2026-09-07 は出典スナップショットを確認した日であり、測定日ではありません**。
## 過去の結果
この表のすべての数値とメモは元の報告の歴史的な記述であり、**現在の API の機能保証ではありません**。
モデルが生のタグを出力することは、HTTP エンドポイントがそのタグを返すことを意味しません。
過去のタイムスタンプに関する記述も、現在の[モデル選択ガイド](../model_selection_ja.md)に代わるものではありません。
| モデル | デバイス | RTF | 速度 | CER | メモ |
| --- | --- | --- | --- | --- | --- |
| SenseVoice-Small | GPU | 0.005896 | 169.6x | 7.81% | ASR + language / emotion / event tags; CER after tag stripping. |
| Paraformer-Large | GPU | 0.008359 | 119.6x | 10.18% | Fast non-autoregressive Chinese ASR with VAD/punctuation pipeline. |
| Fun-ASR-Nano | GPU | 0.058803 | 17.0x | 8.06% | 中国語・英語・日本語、7つの中国語方言グループ、26の地域アクセントに対応する LLM-based ASR。hotword に対応。信頼できる checkpoint-native timestamp は未対応([#106](https://github.com/QwenAudio/Fun-ASR/issues/106))。 |
| GLM-ASR-Nano | GPU | 0.026974 | 37.1x | 31.07% | LLM-based multilingual ASR. |
| Whisper-large-v3-turbo (OpenAI) | GPU | 0.021708 | 46.1x | 21.71% | OpenAI Whisper implementation. |
| Whisper-large-v3 (OpenAI) | GPU | 0.074694 | 13.4x | 20.02% | ベースライン for large Whisper quality. |
| SenseVoice-Small | CPU | 0.057988 | 17.2x | 7.81% | CPU run from the remaining benchmark script. |
| Paraformer-Large | CPU | 0.064056 | 15.6x | 10.18% | CPU viable for batch jobs. |
| Fun-ASR-Nano | CPU | 0.274318 | 3.6x | 8.06% | LLM-based model is heavier but still above realtime. |
CPU/GPU の行で同じ CER が繰り返されていても、それぞれを独立に採点した証拠にはなりません。
確認した資料には、生の予測、参照テキスト、採点プログラムがありません。
「タグを除去してから CER を計算」という記述は過去の主張として保存しており、今回検証した採点結果ではありません。
数値の桁数や丸められた速度・RTF の組は、再計算せずに保持しています。
## 出典と制約
[元の日本語 HTML](https://github.com/modelscope/FunASR/blob/67d63b80a246dc33749e43904c294e0409cd9183/ja/benchmark.html)は
過去の GitHub Pages のコミットに固定されています。出典確認時に、このファイルは保存済みの公開ページの
スナップショットとバイト単位で一致しました。これは表の出典を示すものであり、
測定の正しさや再現可能性を証明するものではありません。
元の報告では RTF を総推論時間と総音声時間の比、速度をその逆数としています。
速度は RTFx とも呼ばれます。
```text
RTF = total inference time / total audio duration
RTFx = total audio duration / total inference time = 1 / RTF
```
次のコマンドは**過去の記録であり、確認したチェックアウトからそのまま実行することはできません**。
確認した FunASR ソースのリビジョン `386f6f9106684ba5a114e796147db4396a09eab5` には、
参照先の三つのファイルがありません。本ページは代替スクリプトや再現用データを提供しません。
```text
python benchmark/run_full_benchmark.py
python benchmark/run_remaining.py
python benchmark/fix_sensevoice_cer.py
```
元の報告では CPU の型番・スレッド数、データの構成と参照テキストの一覧、正確な checkpoint のリビジョン、
ソフトウェア・ドライバーのバージョン、ファイルごとの予測や計時ログが明らかではありません。
ウォームアップ、I/O、前処理を含むかなど、計時範囲も十分には記録されていません。
これらがないため、この表を直接再現することはできず、CPU と GPU の比較をあらゆる本番環境に一般化できません。
この記録の **11,539 秒**と、[vLLM の測定方法(英語)](rtf_reproducibility.md)に記載された
**11,541 秒**は別々に引用してください。両方が 184 ファイルと述べていても、同じファイル群であるとは限りません。
二つの表を統合したり、2 秒の差を勝手に補正したりしないでください。
## 現在の選び方
以下は**元の推奨表を過去の文脈として保存したもの**です。
新たに検証した推奨や性能ランキングではありません。
| 用途 | 推奨モデル |
| --- | --- |
| 最速の本番書き起こし | SenseVoice-Small または Paraformer-Large。 |
| CPU バッチ書き起こし | まず SenseVoice-Small。中国語の本番 pipeline では Paraformer-Large。 |
| 中国語・英語・日本語、および中国語の方言/アクセントを扱う LLM-style 認識 | Fun-ASR-Nano。31言語が必要な場合は別 checkpoint の [Fun-ASR-MLT-Nano](https://huggingface.co/FunAudioLLM/Fun-ASR-MLT-Nano-2512) を使用し、LLM decode throughput を高める場合は [vLLM](../vllm_guide.md) を使用。 |
| OpenAI 互換ローカル endpoint | [funasr-server](../agent_integration_ja.md) を使い、model alias は `sensevoice`、`paraformer`、または `fun-asr-nano`。 |
現在の判断には、[モデル選択](../model_selection_ja.md)、[Agent のインターフェースと制約](../agent_integration_ja.md)、
[デプロイ方式](../deployment_matrix_ja.md)、[vLLM ガイド(英語)](../vllm_guide.md)を使ってください。
別 checkpoint である MLT-Nano の 31 言語対応を、基本 Fun-ASR-Nano の対応範囲と混同しないでください。
対象言語、手元の音声、実行環境、エンドツーエンドの遅延を評価してから方式を選びます。
公式 native vLLM と split-engine は checkpoint と API が異なるため、相互に置き換えないでください。
新しい計測には[性能測定の方法(英語)](rtf_reproducibility.md)、同時接続を伴うリアルタイムサービスには
[WebSocket ベンチマーク(英語)](realtime_ws_benchmark.md)を参照してください。
現在の[移行用計時ツール](../../examples/migration/benchmark_funasr.py)は手元の音声で FunASR の時間を測るもので、
**CER/WER を計算せず、Whisper も実行せず、欠けている過去のスクリプトを再現しません**。
計時範囲、失敗したファイル、品質評価は分けて明示してください。
+105
View File
@@ -0,0 +1,105 @@
# 과거 ASR 벤치마크 기록
[English](historical_asr.md) | [中文](historical_asr_zh.md) | [日本語](historical_asr_ja.md)
이 페이지는 이전 FunASR 비교를 참고하는 독자를 위해 **출처 정보가 불완전한 과거 기록**을 보존합니다.
새로운 측정, 범용 순위표, 현재 checkpoint·장비·배포에 대한 보장이 아닙니다.
대상 데이터는 **중국어 오디오**이며 이 한국어 페이지가 한국어나 일본어 인식 정확도를 측정한 것은 아닙니다.
새 평가를 시작할 때는 [성능 측정 방법(영문)](rtf_reproducibility.md)을 먼저 참고하세요.
## 과거 평가 요약
아래 표는 원래 한국어 페이지의 표현과 수치를 보존합니다.
"최고"와 같은 표현은 해당 보고서 안에서만 유효하며 모든 모델이나 하드웨어에 적용되지 않습니다.
| 항목 | 결과 |
| --- | --- |
| Dataset | 184개의 중국어 장문 오디오, 총 11,539초, 192.3분. |
| GPU | NVIDIA H100 80GB HBM3. |
| 최고 GPU 속도 | SenseVoice-Small: full benchmark에서 169.6x realtime, initial run에서 211.8x. |
| 최고 CPU 속도 | SenseVoice-Small: 17.2x realtime; Paraformer-Large: 15.6x realtime. |
| Baseline | OpenAI Whisper-large-v3: GPU에서 13.4x realtime. |
**전체 실행의 169.6x와 초기 실행의 211.8x는 따로 보고된 결과**입니다.
원래 페이지는 측정 날짜를 밝히지 않습니다. **2026-09-07은 출처 스냅샷을 확인한 날짜이지 측정일이 아닙니다**.
## 과거 평가 결과
이 표의 모든 수치와 설명은 원래 보고서의 과거 기록이며 **현재 API 기능에 대한 보장이 아닙니다**.
모델의 원시 출력에 태그가 있다는 사실이 HTTP 엔드포인트도 그 태그를 반환한다는 뜻은 아닙니다.
과거 타임스탬프 설명도 현재의 [모델 선택 가이드](../model_selection_ko.md)를 대신하지 않습니다.
| Model | Device | RTF | Speed | CER | Notes |
| --- | --- | --- | --- | --- | --- |
| SenseVoice-Small | GPU | 0.005896 | 169.6x | 7.81% | ASR + language / emotion / event tags; tag 제거 후 CER 계산. |
| Paraformer-Large | GPU | 0.008359 | 119.6x | 10.18% | VAD/punctuation pipeline과 잘 맞는 빠른 non-autoregressive 중국어 ASR. |
| Fun-ASR-Nano | GPU | 0.058803 | 17.0x | 8.06% | 중국어·영어·일본어, 7개 중국어 방언군, 26개 지역 억양을 지원하는 LLM-based ASR. hotword 지원. 신뢰할 수 있는 checkpoint-native timestamp는 미지원 ([#106](https://github.com/QwenAudio/Fun-ASR/issues/106)). |
| GLM-ASR-Nano | GPU | 0.026974 | 37.1x | 31.07% | LLM-based multilingual ASR. |
| Whisper-large-v3-turbo (OpenAI) | GPU | 0.021708 | 46.1x | 21.71% | OpenAI Whisper implementation. |
| Whisper-large-v3 (OpenAI) | GPU | 0.074694 | 13.4x | 20.02% | large Whisper quality 기준 baseline. |
| SenseVoice-Small | CPU | 0.057988 | 17.2x | 7.81% | remaining benchmark script에서 수집한 CPU run. |
| Paraformer-Large | CPU | 0.064056 | 15.6x | 10.18% | CPU batch job에도 활용 가능. |
| Fun-ASR-Nano | CPU | 0.274318 | 3.6x | 8.06% | LLM-based model은 더 무겁지만 realtime보다 빠릅니다. |
CPU/GPU 행에서 같은 CER가 반복된다고 해서 장치별로 독립적인 채점이 이루어졌다고 볼 수는 없습니다.
확인한 자료에는 원시 예측, 참조 텍스트, 채점 프로그램이 없습니다.
"태그 제거 후 CER 계산"은 과거의 주장으로 보존한 것이며 이번에 검증한 채점 결과가 아닙니다.
수치의 자릿수와 반올림된 속도·RTF 쌍은 다시 계산하지 않고 그대로 유지합니다.
## 출처와 한계
[원래 한국어 HTML](https://github.com/modelscope/FunASR/blob/67d63b80a246dc33749e43904c294e0409cd9183/ko/benchmark.html)은
과거 GitHub Pages 커밋에 고정되어 있습니다. 출처 확인 당시 이 파일은 보관된 공개 페이지 스냅샷과
바이트 단위로 같았습니다. 이는 표의 출처를 확인한 것이지 측정의 정확성이나 재현성을 증명한 것은 아닙니다.
원래 보고서는 RTF를 총 추론 시간과 총 오디오 길이의 비율로, 속도를 그 역수로 정의합니다.
속도는 RTFx라고도 합니다.
```text
RTF = total inference time / total audio duration
RTFx = total audio duration / total inference time = 1 / RTF
```
다음 명령은 **과거 기록이며 확인한 체크아웃에서 그대로 실행할 수 없습니다**.
확인한 FunASR 소스 리비전 `386f6f9106684ba5a114e796147db4396a09eab5`에는
참조하는 세 파일이 모두 없습니다. 이 문서는 대체 스크립트나 재현용 데이터를 제공하지 않습니다.
```text
python benchmark/run_full_benchmark.py
python benchmark/run_remaining.py
python benchmark/fix_sensevoice_cer.py
```
원래 보고서는 CPU 모델·스레드 수, 데이터 구성과 참조 텍스트 목록, 정확한 checkpoint 리비전,
소프트웨어·드라이버 버전, 파일별 예측 및 시간 측정 로그를 공개하지 않습니다.
준비 실행, I/O, 전처리를 포함하는지 등 전체 측정 범위도 충분히 기록되어 있지 않습니다.
이 자료가 없으므로 표를 그대로 재현할 수 없으며 CPU와 GPU 비교를 모든 운영 환경에 일반화할 수도 없습니다.
이 기록의 **11,539초**와 [vLLM 측정 방법(영문)](rtf_reproducibility.md)의 **11,541초**는
구분해서 인용하세요. 두 기록이 모두 184개 파일을 언급해도 같은 파일 집합이라는 증거는 아닙니다.
두 표의 결과를 합치거나 2초 차이를 임의로 보정하지 마세요.
## 현재 모델 선택
다음은 **원래 추천 표를 과거의 맥락으로만 보존한 것**입니다.
새롭게 검증한 추천이나 성능 순위가 아닙니다.
| 필요한 것 | 추천 model |
| --- | --- |
| 가장 빠른 production transcription | SenseVoice-Small 또는 Paraformer-Large. |
| CPU batch transcription | 먼저 SenseVoice-Small; 중국어 production pipeline은 Paraformer-Large. |
| 중국어·영어·일본어 및 중국어 방언/억양 LLM-style recognition | Fun-ASR-Nano. 31개 언어는 별도 checkpoint인 [Fun-ASR-MLT-Nano](https://huggingface.co/FunAudioLLM/Fun-ASR-MLT-Nano-2512)를 사용하고, LLM decoding throughput이 중요하면 [vLLM](../vllm_guide.md). |
| OpenAI 호환 local endpoint | [funasr-server](../agent_integration_ko.md)와 model alias `sensevoice`, `paraformer`, `fun-asr-nano`. |
현재의 판단에는 [모델 선택](../model_selection_ko.md), [Agent 인터페이스와 제약](../agent_integration_ko.md),
[배포 방식](../deployment_matrix_ko.md), [vLLM 가이드(영문)](../vllm_guide.md)를 사용하세요.
별도 checkpoint인 MLT-Nano의 31개 언어 범위를 기본 Fun-ASR-Nano에 적용하지 마세요.
대상 언어, 직접 사용할 오디오, 실행 환경, 종단 간 지연 시간을 평가한 뒤 배포 방식을 선택하세요.
공식 native vLLM과 split-engine은 checkpoint와 API가 다르므로 서로 바꾸어 사용하면 안 됩니다.
새로운 측정에는 [성능 측정 방법(영문)](rtf_reproducibility.md)을, 동시 접속 실시간 서비스에는
[WebSocket 벤치마크(영문)](realtime_ws_benchmark.md)를 참고하세요.
현재 [마이그레이션 시간 측정 도구](../../examples/migration/benchmark_funasr.py)는 직접 준비한 오디오에서
FunASR의 실행 시간을 측정할 뿐, **CER/WER를 계산하지 않고 Whisper를 실행하지 않으며
누락된 과거 스크립트를 재현하지도 않습니다**.
측정 범위, 실패한 파일, 품질 평가는 구분해서 명시하세요.
+4
View File
@@ -97,6 +97,10 @@ entry to these guides. Source Markdown remains in this repository.
deployment_matrix_ja
model_selection_ko
deployment_matrix_ko
agent_integration_ja
agent_integration_ko
benchmark/historical_asr_ja
benchmark/historical_asr_ko
Model weights and runtime choices
---------------------------------
@@ -140,6 +140,10 @@ td, th { padding: 14px; }
.docs-sidebar .docs-navigation { margin: 0; }
.docs-navigation > .docs-navigation-toggle { display: none; }
.docs-body { min-width: 0; }
.legacy-doc-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; max-width: 1328px; padding: 18px 24px; margin: 0 auto; border-bottom: 1px solid var(--line); }
.legacy-doc-header nav { display: flex; flex-wrap: wrap; gap: 18px; font-size: 13px; }
.legacy-doc-header a { text-decoration: none; }
.localized-legacy-doc .docs-article td:nth-child(6) { min-width: 260px; }
.breadcrumbs { display: flex; flex-wrap: wrap; gap: 8px; color: var(--muted); font-size: 12px; margin-bottom: 32px; }
.breadcrumbs a { text-decoration: none; }
.docs-title h1 { font-size: 36px; line-height: 1.4; margin: 0 0 16px; }
@@ -1,4 +1,10 @@
{
"localized_pages": [
{"language": "ja", "route": "ja/agent.html", "title": "FunASR Agent 連携", "source": "docs/agent_integration_ja.md"},
{"language": "ko", "route": "ko/agent.html", "title": "FunASR Agent 연동", "source": "docs/agent_integration_ko.md"},
{"language": "ja", "route": "ja/benchmark.html", "title": "ASR ベンチマークの過去の記録", "source": "docs/benchmark/historical_asr_ja.md"},
{"language": "ko", "route": "ko/benchmark.html", "title": "과거 ASR 벤치마크 기록", "source": "docs/benchmark/historical_asr_ko.md"}
],
"groups": [
{"id": "start", "zh": "开始使用", "en": "Get started"},
{"id": "models", "zh": "模型与能力", "en": "Models & capabilities"},
@@ -7,6 +7,10 @@
},
"notes": "Published fragments map to the nearest current topic heading. Consolidated quickstart topics use workflow sections; removed standalone runtime diagnostics use the deployment readiness checklist. Source articles and API reference pages are not rewritten by this compatibility layer.",
"pages": {
"ja/agent.html": {"server": "http-サーバー", "sdk": "sdk-と-curl", "mcp": "mcp-サーバー", "voice": "デスクトップ音声入力", "subtitle": "字幕生成"},
"ko/agent.html": {"server": "http-서버", "sdk": "sdk와-curl", "workflows": "워크플로-연동", "mcp": "mcp-서버", "voice": "데스크톱-음성-입력", "subtitle": "자막-생성"},
"ja/benchmark.html": {"summary": "過去の概要", "table": "過去の結果", "method": "出典と制約", "choose": "現在の選び方"},
"ko/benchmark.html": {"summary": "과거-평가-요약", "table": "과거-평가-결과", "method": "출처와-한계", "choose": "현재-모델-선택"},
"tutorial.html": {
"quick": "1-produce-a-first-transcript",
"install": "1-produce-a-first-transcript",
+15 -2
View File
@@ -35,6 +35,17 @@ def load_catalogue() -> dict:
path = (REPO_ROOT / entry[f'source_{language}']).resolve()
if not path.is_relative_to(REPO_ROOT) or not path.is_file():
raise ValueError(f'Missing documentation source: {path}')
routes, sources = set(), set()
for entry in catalogue.get('localized_pages', []):
language, route, relative = entry['language'], entry['route'], entry['source']
path = (REPO_ROOT / relative).resolve()
if (language not in ('ja', 'ko') or
not re.fullmatch(rf'{re.escape(language)}/[a-z0-9-]+\.html', route) or
route in routes or path in sources or
not path.is_relative_to(REPO_ROOT) or not path.is_file()):
raise ValueError(f'Invalid localized documentation owner: {route}')
routes.add(route)
sources.add(path)
return catalogue
@@ -42,15 +53,17 @@ def doc_route(slug: str, language: str) -> str:
return f'/{"en/" if language == "en" else ""}docs/{slug}.html'
def render_source(entry: dict, language: str, catalogue: dict) -> dict:
def render_source(entry: dict, language: str, catalogue: dict, *, extra_routes: dict | None = None) -> dict:
relative = entry[f'source_{language}']
parser = Markdown(extensions=['extra', 'toc', 'sane_lists'],
extension_configs={'toc': {'slugify': source_slug}})
soup = BeautifulSoup(parser.convert((REPO_ROOT / relative).read_text()), 'html.parser')
local_routes = {}
for variant in (('en' if language == 'zh' else 'zh'), language):
variants = ('en', 'zh') if language == 'zh' else ('zh', 'en')
for variant in variants:
for page in catalogue['pages']:
local_routes[page[f'source_{variant}']] = doc_route(page['slug'], variant)
local_routes.update(extra_routes or {})
for element, attr in [('a', 'href'), ('img', 'src')]:
for node in soup.select(f'{element}[{attr}]'):
value = str(node[attr])
+49
View File
@@ -4,9 +4,13 @@ from __future__ import annotations
import argparse
import json
import posixpath
from pathlib import Path
import shutil
from bs4 import BeautifulSoup
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
from documentation import load_catalogue, render_source
ORIGIN = 'https://www.funasr.com'
ALIASES = {
@@ -63,6 +67,51 @@ def export_documentation(built: Path, output: Path, aliases: dict | None = None)
target = output / target_prefix / filename
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(str(soup), encoding='utf-8')
if aliases is None:
export_localized_documentation(built, output, legacy)
def export_localized_documentation(built: Path, output: Path, legacy: dict) -> None:
"""Publish explicitly owned translations without inventing product locales."""
catalogue = load_catalogue()
entries = catalogue.get('localized_pages', [])
if not entries:
return
# Reuse this build's hashed assets, including the copy icon used by the JS.
shell = BeautifulSoup((built / 'en/docs/agent-integration.html').read_text(), 'html.parser')
styles = [node['href'] for node in shell.select('link[rel=stylesheet]')]
scripts = [node['src'] for node in shell.select('script[src]')]
environment = Environment(loader=FileSystemLoader(Path(__file__).parent / 'templates'),
undefined=StrictUndefined, autoescape=select_autoescape(['html']))
template = environment.get_template('legacy-localized-doc.html')
for entry in entries:
language, route = entry['language'], entry['route']
prefix = posixpath.dirname(route)
extra_routes = {page['source']: posixpath.relpath(page['route'], prefix) for page in entries}
content = render_source({f'source_{language}': entry['source']}, language, catalogue,
extra_routes=extra_routes)
peer_language = 'ko' if language == 'ja' else 'ja'
peer_route = f"{peer_language}/{posixpath.basename(route)}"
peer = next((page for page in entries if page['route'] == peer_route), None)
soup = BeautifulSoup(template.render(
**content, entry=entry, language=language,
canonical='https://modelscope.github.io/FunASR/' + route,
styles=styles, scripts=scripts, copy_icon=shell.body.get('data-copy-icon', ''),
peer_href=posixpath.relpath(peer_route, prefix) if peer else None,
peer_label='한국어' if language == 'ja' else '日本語',
navigation=[{**page, 'href': posixpath.relpath(page['route'], prefix)}
for page in entries if page['language'] == language],
), 'html.parser')
insert_legacy_anchors(soup, legacy['pages'].get(route, {}))
for node in soup.find_all(True):
for attribute in ('href', 'src', 'action', 'data-copy-icon'):
value = node.get(attribute, '')
if isinstance(value, str) and value.startswith('/') and not value.startswith('//'):
node[attribute] = (posixpath.relpath(value.lstrip('/'), prefix)
if value.startswith('/assets/') else ORIGIN + value)
target = output / route
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(str(soup), encoding='utf-8')
if __name__ == '__main__':
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="{{ language }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ entry.title }} | FunASR</title>
<link rel="canonical" href="{{ canonical }}">
{% for href in styles %}<link rel="stylesheet" href="{{ href }}">{% endfor %}
</head>
<body class="localized-legacy-doc" data-language="{{ language }}" data-copy-icon="{{ copy_icon }}">
<a class="skip-link" href="#main">{{ '本文へ' if language == 'ja' else '본문으로' }}</a>
<header class="legacy-doc-header">
<a class="brand" href="https://www.funasr.com/en/"><span class="brand-mark" aria-hidden="true">F</span><span>FunASR</span></a>
<nav aria-label="{{ '言語' if language == 'ja' else '언어' }}">
<a href="https://www.funasr.com/en/docs/">English</a>
<a href="https://www.funasr.com/docs/">中文</a>
{% if peer_href %}<a href="{{ peer_href }}" data-peer-link>{{ peer_label }}</a>{% endif %}
</nav>
</header>
<main id="main" class="docs-layout">
<aside class="docs-sidebar">
<nav aria-label="{{ 'ガイド' if language == 'ja' else '가이드' }}">
{% for page in navigation %}<a href="{{ page.href }}"{% if page.route == entry.route %} aria-current="page"{% endif %}>{{ page.title }}</a>{% endfor %}
</nav>
</aside>
<div class="docs-body">
<header class="docs-title">
<h1>{{ entry.title }}</h1>
<a class="source-link" href="{{ source_url }}" data-source-link>{{ 'GitHub で原稿を読む' if language == 'ja' else 'GitHub 원문' }}</a>
</header>
<article class="docs-article">{{ content_html | safe }}</article>
</div>
<aside class="docs-toc" aria-label="{{ '目次' if language == 'ja' else '목차' }}">{{ toc_html | safe }}</aside>
</main>
{% for src in scripts %}<script src="{{ src }}" defer></script>{% endfor %}
</body>
</html>
@@ -1,5 +1,50 @@
import { expect, test } from '@playwright/test';
for (const language of ['ja', 'ko']) {
for (const filename of ['agent', 'benchmark']) {
for (const width of [390, 1440]) {
test(`Localized Pages ${language}/${filename} ${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));
const response = await page.goto(`/__pages/${language}/${filename}.html`);
expect(response!.status()).toBe(200);
await expect(page.locator('html')).toHaveAttribute('lang', language);
await expect(page.locator('link[rel=canonical]')).toHaveAttribute('href',
`https://modelscope.github.io/FunASR/${language}/${filename}.html`);
expect((await page.locator('.docs-title h1').boundingBox())!.y).toBeLessThan(450);
await page.screenshot({ path: `/tmp/funasr-ja-ko-docs-evidence-20260907/${language}-${filename}-${width}-top.png` });
const ids = filename === 'benchmark' ? ['summary', 'table', 'method', 'choose'] :
(language === 'ja' ? ['server', 'sdk', 'mcp', 'voice', 'subtitle'] : ['server', 'sdk', 'workflows', 'mcp', 'voice', 'subtitle']);
for (const id of ids) {
await expect(page.locator(`[id="${id}"]`)).toHaveCount(1);
await page.evaluate(fragment => { location.hash = fragment; }, id);
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1);
}
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());
if (filename === 'benchmark') {
await expect(page.locator('.docs-article table')).toHaveCount(3);
const notes = page.locator('.docs-article table').nth(1).locator('td:nth-child(6)');
expect(await notes.evaluateAll(nodes => nodes.every(node =>
node.getBoundingClientRect().width >= 260 && node.parentElement!.getBoundingClientRect().height < 240))).toBeTruthy();
for (const table of await page.locator('.docs-article table').all()) {
await table.locator('tr').last().locator('td').last().scrollIntoViewIfNeeded();
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1);
}
}
await page.screenshot({ path: `/tmp/funasr-ja-ko-docs-evidence-20260907/${language}-${filename}-${width}.png`, fullPage: true });
await page.locator('[data-peer-link]').click();
await expect(page).toHaveURL(new RegExp(`/__pages/${language === 'ja' ? 'ko' : 'ja'}/${filename}.html$`));
expect(errors).toEqual([]);
});
}
}
}
test('mobile topic navigation leaves the article in the first viewport', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/docs/training.html');
@@ -18,7 +18,7 @@ export default defineConfig({
colorScheme: 'light',
},
webServer: {
command: `${python} build.py --output /tmp/funasr-product-site-browser && ${python} -m http.server 8770 --bind 127.0.0.1 --directory /tmp/funasr-product-site-browser`,
command: `${python} build.py --output /tmp/funasr-product-site-browser && ${python} export_docs.py --site /tmp/funasr-product-site-browser --output /tmp/funasr-product-site-browser/__pages && ${python} -m http.server 8770 --bind 127.0.0.1 --directory /tmp/funasr-product-site-browser`,
cwd: siteRoot,
url: 'http://127.0.0.1:8770/',
reuseExistingServer: false,
@@ -802,6 +802,806 @@
]
}
]
},
{
"language": "ja",
"requested_url": "https://modelscope.github.io/FunASR/ja/benchmark.html",
"fetched_at_utc": "2026-09-07T11:58:18.583131+00:00",
"sha256": "a46ef17b49cfb14c5181f330a4fa11fc0fdc3ecfcfc6a1862bb83bd07ccebc51",
"tables": [
{
"section_id": "summary",
"rows": [
[
{
"text": "指標",
"links": []
},
{
"text": "結果",
"links": []
}
],
[
{
"text": "データセット",
"links": []
},
{
"text": "中国語の長時間音声 184 ファイル、合計 11,539 秒、192.3 分。",
"links": []
}
],
[
{
"text": "GPU",
"links": []
},
{
"text": "NVIDIA H100 80GB HBM3.",
"links": []
}
],
[
{
"text": "最高 GPU 速度",
"links": []
},
{
"text": "SenseVoice-Small: 169.6x realtime in the full benchmark, 211.8x in the initial run.",
"links": []
}
],
[
{
"text": "最高 CPU 速度",
"links": []
},
{
"text": "SenseVoice-Small: 17.2x realtime; Paraformer-Large: 15.6x realtime.",
"links": []
}
],
[
{
"text": "ベースライン",
"links": []
},
{
"text": "OpenAI Whisper-large-v3: 13.4x realtime on GPU.",
"links": []
}
]
]
},
{
"section_id": "table",
"rows": [
[
{
"text": "モデル",
"links": []
},
{
"text": "デバイス",
"links": []
},
{
"text": "RTF",
"links": []
},
{
"text": "速度",
"links": []
},
{
"text": "CER",
"links": []
},
{
"text": "メモ",
"links": []
}
],
[
{
"text": "SenseVoice-Small",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.005896",
"links": []
},
{
"text": "169.6x",
"links": []
},
{
"text": "7.81%",
"links": []
},
{
"text": "ASR + language / emotion / event tags; CER after tag stripping.",
"links": []
}
],
[
{
"text": "Paraformer-Large",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.008359",
"links": []
},
{
"text": "119.6x",
"links": []
},
{
"text": "10.18%",
"links": []
},
{
"text": "Fast non-autoregressive Chinese ASR with VAD/punctuation pipeline.",
"links": []
}
],
[
{
"text": "Fun-ASR-Nano",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.058803",
"links": []
},
{
"text": "17.0x",
"links": []
},
{
"text": "8.06%",
"links": []
},
{
"text": "中国語・英語・日本語、7つの中国語方言グループ、26の地域アクセントに対応する LLM-based ASR。hotword に対応。信頼できる checkpoint-native timestamp は未対応(#106)。",
"links": [
"https://github.com/QwenAudio/Fun-ASR/issues/106"
]
}
],
[
{
"text": "GLM-ASR-Nano",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.026974",
"links": []
},
{
"text": "37.1x",
"links": []
},
{
"text": "31.07%",
"links": []
},
{
"text": "LLM-based multilingual ASR.",
"links": []
}
],
[
{
"text": "Whisper-large-v3-turbo (OpenAI)",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.021708",
"links": []
},
{
"text": "46.1x",
"links": []
},
{
"text": "21.71%",
"links": []
},
{
"text": "OpenAI Whisper implementation.",
"links": []
}
],
[
{
"text": "Whisper-large-v3 (OpenAI)",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.074694",
"links": []
},
{
"text": "13.4x",
"links": []
},
{
"text": "20.02%",
"links": []
},
{
"text": "ベースライン for large Whisper quality.",
"links": []
}
],
[
{
"text": "SenseVoice-Small",
"links": []
},
{
"text": "CPU",
"links": []
},
{
"text": "0.057988",
"links": []
},
{
"text": "17.2x",
"links": []
},
{
"text": "7.81%",
"links": []
},
{
"text": "CPU run from the remaining benchmark script.",
"links": []
}
],
[
{
"text": "Paraformer-Large",
"links": []
},
{
"text": "CPU",
"links": []
},
{
"text": "0.064056",
"links": []
},
{
"text": "15.6x",
"links": []
},
{
"text": "10.18%",
"links": []
},
{
"text": "CPU viable for batch jobs.",
"links": []
}
],
[
{
"text": "Fun-ASR-Nano",
"links": []
},
{
"text": "CPU",
"links": []
},
{
"text": "0.274318",
"links": []
},
{
"text": "3.6x",
"links": []
},
{
"text": "8.06%",
"links": []
},
{
"text": "LLM-based model is heavier but still above realtime.",
"links": []
}
]
]
},
{
"section_id": "choose",
"rows": [
[
{
"text": "用途",
"links": []
},
{
"text": "推奨モデル",
"links": []
}
],
[
{
"text": "最速の本番書き起こし",
"links": []
},
{
"text": "SenseVoice-Small または Paraformer-Large。",
"links": []
}
],
[
{
"text": "CPU バッチ書き起こし",
"links": []
},
{
"text": "まず SenseVoice-Small。中国語の本番 pipeline では Paraformer-Large。",
"links": []
}
],
[
{
"text": "中国語・英語・日本語、および中国語の方言/アクセントを扱う LLM-style 認識",
"links": []
},
{
"text": "Fun-ASR-Nano。31言語が必要な場合は別 checkpoint の Fun-ASR-MLT-Nano を使用し、LLM decode throughput を高める場合は vLLM を使用。",
"links": [
"https://huggingface.co/FunAudioLLM/Fun-ASR-MLT-Nano-2512",
"vllm.html"
]
}
],
[
{
"text": "OpenAI 互換ローカル endpoint",
"links": []
},
{
"text": "funasr-server を使い、model alias は sensevoice、paraformer、または fun-asr-nano。",
"links": [
"agent.html"
]
}
]
]
}
]
},
{
"language": "ko",
"requested_url": "https://modelscope.github.io/FunASR/ko/benchmark.html",
"fetched_at_utc": "2026-09-07T11:58:18.909945+00:00",
"sha256": "9dbbdf30cecc981412a285184387c099b0cb30f6aa50b5f9ba6248f3cb320e03",
"tables": [
{
"section_id": "summary",
"rows": [
[
{
"text": "항목",
"links": []
},
{
"text": "결과",
"links": []
}
],
[
{
"text": "Dataset",
"links": []
},
{
"text": "184개의 중국어 장문 오디오, 총 11,539초, 192.3분.",
"links": []
}
],
[
{
"text": "GPU",
"links": []
},
{
"text": "NVIDIA H100 80GB HBM3.",
"links": []
}
],
[
{
"text": "최고 GPU 속도",
"links": []
},
{
"text": "SenseVoice-Small: full benchmark에서 169.6x realtime, initial run에서 211.8x.",
"links": []
}
],
[
{
"text": "최고 CPU 속도",
"links": []
},
{
"text": "SenseVoice-Small: 17.2x realtime; Paraformer-Large: 15.6x realtime.",
"links": []
}
],
[
{
"text": "Baseline",
"links": []
},
{
"text": "OpenAI Whisper-large-v3: GPU에서 13.4x realtime.",
"links": []
}
]
]
},
{
"section_id": "table",
"rows": [
[
{
"text": "Model",
"links": []
},
{
"text": "Device",
"links": []
},
{
"text": "RTF",
"links": []
},
{
"text": "Speed",
"links": []
},
{
"text": "CER",
"links": []
},
{
"text": "Notes",
"links": []
}
],
[
{
"text": "SenseVoice-Small",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.005896",
"links": []
},
{
"text": "169.6x",
"links": []
},
{
"text": "7.81%",
"links": []
},
{
"text": "ASR + language / emotion / event tags; tag 제거 후 CER 계산.",
"links": []
}
],
[
{
"text": "Paraformer-Large",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.008359",
"links": []
},
{
"text": "119.6x",
"links": []
},
{
"text": "10.18%",
"links": []
},
{
"text": "VAD/punctuation pipeline과 잘 맞는 빠른 non-autoregressive 중국어 ASR.",
"links": []
}
],
[
{
"text": "Fun-ASR-Nano",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.058803",
"links": []
},
{
"text": "17.0x",
"links": []
},
{
"text": "8.06%",
"links": []
},
{
"text": "중국어·영어·일본어, 7개 중국어 방언군, 26개 지역 억양을 지원하는 LLM-based ASR. hotword 지원. 신뢰할 수 있는 checkpoint-native timestamp는 미지원 (#106).",
"links": [
"https://github.com/QwenAudio/Fun-ASR/issues/106"
]
}
],
[
{
"text": "GLM-ASR-Nano",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.026974",
"links": []
},
{
"text": "37.1x",
"links": []
},
{
"text": "31.07%",
"links": []
},
{
"text": "LLM-based multilingual ASR.",
"links": []
}
],
[
{
"text": "Whisper-large-v3-turbo (OpenAI)",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.021708",
"links": []
},
{
"text": "46.1x",
"links": []
},
{
"text": "21.71%",
"links": []
},
{
"text": "OpenAI Whisper implementation.",
"links": []
}
],
[
{
"text": "Whisper-large-v3 (OpenAI)",
"links": []
},
{
"text": "GPU",
"links": []
},
{
"text": "0.074694",
"links": []
},
{
"text": "13.4x",
"links": []
},
{
"text": "20.02%",
"links": []
},
{
"text": "large Whisper quality 기준 baseline.",
"links": []
}
],
[
{
"text": "SenseVoice-Small",
"links": []
},
{
"text": "CPU",
"links": []
},
{
"text": "0.057988",
"links": []
},
{
"text": "17.2x",
"links": []
},
{
"text": "7.81%",
"links": []
},
{
"text": "remaining benchmark script에서 수집한 CPU run.",
"links": []
}
],
[
{
"text": "Paraformer-Large",
"links": []
},
{
"text": "CPU",
"links": []
},
{
"text": "0.064056",
"links": []
},
{
"text": "15.6x",
"links": []
},
{
"text": "10.18%",
"links": []
},
{
"text": "CPU batch job에도 활용 가능.",
"links": []
}
],
[
{
"text": "Fun-ASR-Nano",
"links": []
},
{
"text": "CPU",
"links": []
},
{
"text": "0.274318",
"links": []
},
{
"text": "3.6x",
"links": []
},
{
"text": "8.06%",
"links": []
},
{
"text": "LLM-based model은 더 무겁지만 realtime보다 빠릅니다.",
"links": []
}
]
]
},
{
"section_id": "choose",
"rows": [
[
{
"text": "필요한 것",
"links": []
},
{
"text": "추천 model",
"links": []
}
],
[
{
"text": "가장 빠른 production transcription",
"links": []
},
{
"text": "SenseVoice-Small 또는 Paraformer-Large.",
"links": []
}
],
[
{
"text": "CPU batch transcription",
"links": []
},
{
"text": "먼저 SenseVoice-Small; 중국어 production pipeline은 Paraformer-Large.",
"links": []
}
],
[
{
"text": "중국어·영어·일본어 및 중국어 방언/억양 LLM-style recognition",
"links": []
},
{
"text": "Fun-ASR-Nano. 31개 언어는 별도 checkpoint인 Fun-ASR-MLT-Nano를 사용하고, LLM decoding throughput이 중요하면 vLLM.",
"links": [
"https://huggingface.co/FunAudioLLM/Fun-ASR-MLT-Nano-2512",
"vllm.html"
]
}
],
[
{
"text": "OpenAI 호환 local endpoint",
"links": []
},
{
"text": "funasr-server와 model alias sensevoice, paraformer, fun-asr-nano.",
"links": [
"agent.html"
]
}
]
]
}
]
}
]
}
@@ -24,6 +24,148 @@ OLD_IDS = {
'benchmark.html': 'summary table method choose',
}
PAGES = [prefix + name for name in OLD_IDS for prefix in ('', 'zh/')]
LOCALIZED_IDS = {
'ja/agent.html': 'server sdk mcp voice subtitle',
'ko/agent.html': 'server sdk workflows mcp voice subtitle',
'ja/benchmark.html': 'summary table method choose',
'ko/benchmark.html': 'summary table method choose',
}
@pytest.mark.parametrize('invalid', ('language', 'escape', 'wrong-prefix', 'duplicate-route', 'duplicate-source'))
def test_localized_catalogue_rejects_ambiguous_or_unsafe_owners(tmp_path, monkeypatch, invalid):
import documentation
catalogue = json.loads((SITE / 'data/documentation.json').read_text())
entry = {'language': 'ja', 'route': 'ja/agent.html', 'title': 'Agent',
'source': 'docs/agent_integration.md'}
catalogue['localized_pages'] = [entry]
if invalid == 'language':
entry['language'] = 'fr'
elif invalid == 'escape':
entry['route'] = '../agent.html'
elif invalid == 'wrong-prefix':
entry['route'] = 'ko/agent.html'
elif invalid == 'duplicate-route':
catalogue['localized_pages'].append({**entry, 'source': 'docs/agent_integration_zh.md'})
else:
catalogue['localized_pages'].append({**entry, 'route': 'ja/benchmark.html'})
(tmp_path / 'data').mkdir()
(tmp_path / 'data/documentation.json').write_text(json.dumps(catalogue))
monkeypatch.setattr(documentation, 'SITE_ROOT', tmp_path)
with pytest.raises(ValueError, match='localized'):
documentation.load_catalogue()
@pytest.fixture(scope='module')
def localized_exported(tmp_path_factory):
from build import build
from export_docs import export_documentation
root = tmp_path_factory.mktemp('localized-legacy-docs')
built, output = root / 'built', root / 'pages'
build(built)
sentinel = output / 'ko/unrelated.html'
sentinel.parent.mkdir(parents=True)
sentinel.write_text('existing localized page')
export_documentation(built, output)
assert sentinel.read_text() == 'existing localized page'
return built, output
@pytest.mark.parametrize('route', LOCALIZED_IDS)
def test_localized_pages_have_owned_sources_and_real_legacy_anchors(localized_exported, route):
from urllib.parse import urlsplit
built, output = localized_exported
path = output / route
assert path.is_file(), route
language, filename = route.split('/')
page = BeautifulSoup(path.read_text(), 'html.parser')
assert page.html['lang'] == language
assert page.select_one('link[rel=canonical]')['href'] == 'https://modelscope.github.io/FunASR/' + route
source = (f'docs/agent_integration_{language}.md' if filename == 'agent.html'
else f'docs/benchmark/historical_asr_{language}.md')
assert page.select_one('[data-source-link]')['href'].endswith('/' + source)
assert (SITE.parents[1] / source).is_file()
ids = [node['id'] for node in page.select('[id]')]
assert len(ids) == len(set(ids))
mapping = json.loads((SITE / 'data/legacy_doc_anchors.json').read_text())['pages'][route]
assert set(mapping) == set(LOCALIZED_IDS[route].split())
for old, target in mapping.items():
anchor = page.select_one('.docs-article').find(id=old)
assert anchor is not None and anchor.name == 'span'
heading = anchor.find_next_sibling()
assert heading.name in ('h2', 'h3') and heading['id'] == target
assert page.select_one('.docs-toc a[href^="#"]')
assets = [(node, attr) for selector, attr in [('link[rel=stylesheet]', 'href'), ('script[src]', 'src')]
for node in page.select(selector)]
assert assets and page.body.get('data-copy-icon')
for node, attr in assets + [(page.body, 'data-copy-icon')]:
assert not urlsplit(node[attr]).scheme and not node[attr].startswith('/')
assert (path.parent / node[attr]).is_file()
# These legacy translations do not create a fictitious four-language product site.
assert not (built / language / 'docs').exists()
@pytest.mark.parametrize('language', ('ja', 'ko'))
def test_localized_historical_cells_and_per_cell_links_are_preserved(localized_exported, language):
_, output = localized_exported
path = output / language / 'benchmark.html'
assert path.is_file()
page = BeautifulSoup(path.read_text(), 'html.parser')
frozen = json.loads((SITE / 'tests/fixtures/historical_asr_tables.json').read_text())
record = next(row for row in frozen['pages'] if row['language'] == language)
tables = page.select('.docs-article table')
assert len(tables) == len(record['tables']) == 3
mapping = {'vllm.html': 'https://www.funasr.com/en/docs/vllm.html', 'agent.html': 'agent.html'}
count = 0
for table, expected in zip(tables, record['tables']):
rows = table.select('tr')
assert len(rows) == len(expected['rows'])
for row, expected_cells in zip(rows, expected['rows']):
cells = row.select('th,td')
assert len(cells) == len(expected_cells)
for cell, original in zip(cells, expected_cells):
assert ' '.join(cell.get_text().split()) == original['text']
assert [a['href'] for a in cell.select('a[href]')] == [mapping.get(a, a) for a in original['links']]
count += 1
assert count == 82
original_url = f"https://github.com/modelscope/FunASR/blob/{frozen['source_commit']}/{language}/benchmark.html"
assert page.select_one(f'a[href="{original_url}"]')
article = page.select_one('.docs-article').get_text(' ', strip=True)
for marker in ('11,539', '11,541', '169.6x', '211.8x', 'RTFx', '2026-09-07'):
assert marker in article
@pytest.mark.parametrize('language', ('ja', 'ko'))
def test_localized_agent_recipes_match_maintained_contract(localized_exported, language):
import ast
import re
import shlex
_, output = localized_exported
path = output / language / 'agent.html'
assert path.is_file()
source = (SITE.parents[1] / f'docs/agent_integration_{language}.md').read_text()
blocks = re.findall(r'^```(\w+)\n(.*?)^```', source, re.M | re.S)
commands = []
for kind, block in blocks:
if kind == 'python':
ast.parse(block)
if kind == 'json':
assert 'mcpServers' in json.loads(block)
for line in block.splitlines():
if line.startswith('funasr-server '):
command = shlex.split(line)
assert command[command.index('--host') + 1] == '127.0.0.1'
assert command[command.index('--model') + 1] == 'sensevoice'
commands.append(command)
assert commands
for marker in ('paraformer-en', 'Fun-ASR-MLT-Nano', 'model="custom"', '--model-path', '--hub',
'verbose_json', 'examples/mcp_server/funasr_mcp.py',
'examples/voice_input/funasr_input.py', 'examples/subtitle/generate_subtitle.py'):
assert marker in source
@pytest.fixture(scope='module')