Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions web-pages/product-site/data/deployments.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"verified": "2026-08-04",
"verified": "2026-08-11",
"deployments": [
{
"id": "vllm",
Expand Down Expand Up @@ -157,16 +157,28 @@
"models": ["SenseVoiceSmall-GGUF", "Paraformer-GGUF", "Fun-ASR-Nano-GGUF", "FSMN-VAD-GGUF"],
"operating_systems": ["Linux", "macOS", "Windows"],
"interfaces": ["CLI", "local HTTP server"],
"tested": {"funasr": "runtime-llamacpp-v0.1.9", "runtime": "pinned llama.cpp backend", "verified": "2026-07-26"},
"tested": {"funasr": "runtime-llamacpp-v0.2.0", "runtime": "llama.cpp@803b7fca", "verified": "2026-08-11"},
"commands": {
"install": ["cmake -B build -DCMAKE_BUILD_TYPE=Release", "cmake --build build -j"],
"launch": ["./build/bin/llama-funasr-sensevoice -m funasr-gguf/sensevoice-small-q8.gguf --vad funasr-gguf/fsmn-vad.gguf -a sample.wav"],
"health": ["./build/bin/llama-funasr-sensevoice --help"],
"smoke": ["./runtime/llama.cpp/tests/run_regression.sh"]
"install": ["curl -fLO https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-linux-x64.tar.gz", "echo \"15e6407143b4fb91d90bb37f2a41c64c4d48ea0fbe6404b88a9b70269c84f240 funasr-llamacpp-linux-x64.tar.gz\" | sha256sum -c -", "mkdir funasr-llamacpp && tar -xzf funasr-llamacpp-linux-x64.tar.gz -C funasr-llamacpp && cd funasr-llamacpp && bash download-funasr-model.sh sensevoice ./funasr-gguf f16"],
"launch": ["cd funasr-llamacpp && ./llama-funasr-sensevoice -m funasr-gguf/sensevoice-small-f16.gguf --vad funasr-gguf/fsmn-vad.gguf -a sample.wav"],
"health": ["cd funasr-llamacpp && ./llama-funasr-sensevoice --help"],
"smoke": ["cd funasr-llamacpp && ./llama-funasr-sensevoice -m funasr-gguf/sensevoice-small-f16.gguf --vad funasr-gguf/fsmn-vad.gguf -a sample.wav | tee transcript.txt && test -s transcript.txt"]
},
"downloads": [
{"operating_system": "Linux", "architecture": "arm64", "backend": "CPU", "archive": "funasr-llamacpp-linux-arm64.tar.gz", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-linux-arm64.tar.gz", "sha256": "c78987b2384c6aef339aea1bcd0e130070455d6394fa7ab7ca26840ead10d5da"},
{"operating_system": "Linux", "architecture": "x64", "backend": "CPU", "archive": "funasr-llamacpp-linux-x64.tar.gz", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-linux-x64.tar.gz", "sha256": "15e6407143b4fb91d90bb37f2a41c64c4d48ea0fbe6404b88a9b70269c84f240"},
{"operating_system": "Linux", "architecture": "x64 AVX2", "backend": "CPU", "archive": "funasr-llamacpp-linux-x64-avx2.tar.gz", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-linux-x64-avx2.tar.gz", "sha256": "02e10e9a46ea76a040c45d431efe51a3324e64f08c24d38e18c8a4d2781490cd"},
{"operating_system": "Linux", "architecture": "x64", "backend": "Vulkan", "archive": "funasr-llamacpp-linux-x64-vulkan.tar.gz", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-linux-x64-vulkan.tar.gz", "sha256": "caf71b8c0b4c3249cebc4175e5406d3c588eb9e8966a00d571d4cc5070405385"},
{"operating_system": "macOS", "architecture": "arm64", "backend": "CPU", "archive": "funasr-llamacpp-macos-arm64.tar.gz", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-macos-arm64.tar.gz", "sha256": "416cbb289e31cb7575365d382155074e922fd061807a37b9ca0247dabd9bc6f9"},
{"operating_system": "Windows", "architecture": "x64", "backend": "CPU", "archive": "funasr-llamacpp-windows-x64.zip", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-windows-x64.zip", "sha256": "297c962346d7e30d7a7c2c860dfaab3ff07d01fddf15e6fc5212ca9545441a51"},
{"operating_system": "Windows", "architecture": "x64 AVX2", "backend": "CPU", "archive": "funasr-llamacpp-windows-x64-avx2.zip", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-windows-x64-avx2.zip", "sha256": "4db0f11f603c324a63545cd7009cdd45bb45576efe282cec22796b5fd42d8ea1"},
{"operating_system": "Windows", "architecture": "x64", "backend": "Vulkan", "archive": "funasr-llamacpp-windows-x64-vulkan.zip", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-windows-x64-vulkan.zip", "sha256": "90b45240c6ccc9177c25490a11848de60a406e129391c8736b14521c0c28cdcb"},
{"operating_system": "Windows", "architecture": "x64", "backend": "CUDA", "archive": "funasr-llamacpp-windows-x64-cuda.zip", "url": "https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/funasr-llamacpp-windows-x64-cuda.zip", "sha256": "7f2f9ef4d7e0291b284a295ec74bbeca9ea635a7f5f42d0ad06eb780c0d6efc1"}
],
"evidence": [
{"label": "llama.cpp runtime", "url": "https://github.com/modelscope/FunASR/blob/main/runtime/llama.cpp/README.md"},
{"label": "runtime release", "url": "https://github.com/modelscope/FunASR/releases/tag/runtime-llamacpp-v0.1.9"},
{"label": "runtime v0.2.0 release", "url": "https://github.com/modelscope/FunASR/releases/tag/runtime-llamacpp-v0.2.0"},
{"label": "nine-platform release workflow", "url": "https://github.com/modelscope/FunASR/actions/runs/31458121788"},
{"label": "regression tests", "url": "https://github.com/modelscope/FunASR/tree/main/runtime/llama.cpp/tests"}
],
"benchmarks": [
Expand Down Expand Up @@ -200,27 +212,27 @@
"translations": {
"zh": {
"name": "llama.cpp / GGUF 独立运行",
"summary": "使用跨平台预编译包或源码构建,在 CPU、桌面 GPU 和边缘设备上运行 FunASR GGUF 模型。",
"fit": ["不依赖 Python ML 环境", "桌面应用和离线边缘部署", "需要 Linux、macOS、Windows 发布包"],
"summary": "使用 v0.2.0 的九个预编译包或源码构建,在 CPU、Vulkan、CUDA 和边缘设备上运行 FunASR GGUF 模型。",
"fit": ["不依赖 Python ML 环境", "桌面应用和离线边缘部署", "需要 Linux、macOS、Windows 的 CPU、Vulkan 或 CUDA 发布包"],
"not_fit": ["需要 vLLM 式大批量 GPU 调度", "未验证目标 GPU 架构的通用预编译 CUDA 包", "必须使用完整 Python 模型生态的流程"],
"selection_reason": "GGUF 运行时和独立二进制优先满足可移植、离线和低依赖部署。",
"primary_limitation": "预编译 GPU 包只覆盖标注的后端和架构;其他设备需要从源码构建并实机验证。",
"primary_limitation": "预编译 GPU 包只覆盖标注的后端和架构;Windows AMD Vulkan 的崩溃修复仍需问题报告者实机复测,其他设备也必须在目标硬件验证。",
"status_label": "生产验证",
"operations": ["按发布资产 SHA-256 校验下载", "模型和二进制使用同一发布清单", "保留旧二进制和模型目录用于回滚"],
"operations": ["按页面列出的 SHA-256 校验九个发布资产", "模型和二进制使用同一发布清单", "保留旧二进制和模型目录用于回滚"],
"security": ["默认只读取本地音频和模型", "HTTP 服务绑定内网地址并限制上传大小", "不要从不可信地址加载 GGUF"],
"troubleshooting": ["CPU 路径先用 q8 模型验证", "CUDA 和 Vulkan 必须匹配本机驱动", "用仓库回归脚本区分模型问题与构建问题"]
"troubleshooting": ["CPU 路径先用 q8 模型验证", "CUDA 和 Vulkan 必须匹配本机驱动", "Windows AMD Vulkan 异常时记录 GPU、驱动、GGML_VK_MAX_NODES_PER_SUBMIT 和 GGML_VK_SERIALIZE_SUBMISSIONS 后回报"]
},
"en": {
"name": "llama.cpp / GGUF standalone",
"summary": "Use cross-platform release packages or source builds to run FunASR GGUF models on CPUs, desktop GPUs, and edge devices.",
"fit": ["No Python ML environment", "Desktop applications and offline edge deployment", "Linux, macOS, and Windows release packages"],
"summary": "Use the nine v0.2.0 release packages or source builds to run FunASR GGUF models on CPU, Vulkan, CUDA, and edge devices.",
"fit": ["No Python ML environment", "Desktop applications and offline edge deployment", "CPU, Vulkan, or CUDA packages for Linux, macOS, and Windows"],
"not_fit": ["vLLM-style large GPU batch scheduling", "A universal prebuilt CUDA package for an unverified GPU architecture", "Workflows that require the full Python model ecosystem"],
"selection_reason": "GGUF runtimes and standalone binaries prioritize portability, offline use, and low dependency count.",
"primary_limitation": "Prebuilt GPU packages cover only the documented backend and architecture; other devices require a source build and hardware validation.",
"primary_limitation": "Prebuilt GPU packages cover only the documented backend and architecture; the Windows AMD Vulkan crash fix still awaits reporter hardware confirmation, and every other target also requires hardware validation.",
"status_label": "Production verified",
"operations": ["Verify release assets with SHA-256", "Keep models and binaries on the same release manifest", "Retain the previous binary and model directory for rollback"],
"operations": ["Verify all nine release assets against the listed SHA-256 values", "Keep models and binaries on the same release manifest", "Retain the previous binary and model directory for rollback"],
"security": ["Read local audio and models by default", "Bind the HTTP server to a private address and limit uploads", "Do not load GGUF files from untrusted sources"],
"troubleshooting": ["Validate the q8 CPU path first", "Match CUDA or Vulkan to the local driver", "Use repository regression scripts to separate model and build failures"]
"troubleshooting": ["Validate the q8 CPU path first", "Match CUDA or Vulkan to the local driver", "For Windows AMD Vulkan failures, record the GPU, driver, GGML_VK_MAX_NODES_PER_SUBMIT, and GGML_VK_SERIALIZE_SUBMISSIONS before reporting"]
}
}
},
Expand Down
26 changes: 26 additions & 0 deletions web-pages/product-site/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
Expand Down Expand Up @@ -45,6 +46,7 @@
'source',
'verified',
)
DOWNLOAD_FIELDS = ('operating_system', 'architecture', 'backend', 'archive', 'url', 'sha256')


def load_registry(path: Path) -> dict[str, Any]:
Expand Down Expand Up @@ -161,6 +163,30 @@ def validate_registry(data: dict[str, Any]) -> list[str]:
f'{label}: production-verified entry requires {field_name}'
)

downloads = entry.get('downloads', [])
if not isinstance(downloads, list):
errors.append(f'{label}: downloads must be a list')
else:
for download_index, download in enumerate(downloads):
if not isinstance(download, dict):
errors.append(f'{label}: download {download_index} must be an object')
continue
for field in DOWNLOAD_FIELDS:
if not download.get(field):
errors.append(
f'{label}: download {download_index} requires {field}'
)
if not _is_https_url(download.get('url')):
errors.append(
f'{label}: download URL must use https (item {download_index})'
)
digest = download.get('sha256')
if not isinstance(digest, str) or not re.fullmatch(r'[0-9a-f]{64}', digest):
errors.append(
f'{label}: download SHA-256 must be 64 lowercase hex characters '
f'(item {download_index})'
)

benchmarks = entry.get('benchmarks', [])
if not isinstance(benchmarks, list):
errors.append(f'{label}: benchmarks must be a list')
Expand Down
33 changes: 33 additions & 0 deletions web-pages/product-site/templates/deploy-detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,39 @@ <h3>{{ '不适合' if language == 'zh' else 'Not a fit' }}</h3>
</div>
</section>

{% set downloads = entry.get('downloads', []) %}
{% if downloads %}
<section class="section content-section" data-section="downloads">
<div class="section-heading compact-heading">
<p class="eyebrow">{{ '发布资产' if language == 'zh' else 'Release assets' }}</p>
<h2>{{ '按操作系统和后端下载已校验的预编译包' if language == 'zh' else 'Download a verified package for your operating system and backend' }}</h2>
<p>{{ 'SHA-256 来自公开 GitHub Release;下载后先核对摘要,再在目标硬件完成 smoke test。' if language == 'zh' else 'SHA-256 values come from the public GitHub Release. Verify the digest, then run a smoke test on the target hardware.' }}</p>
</div>
<div class="table-wrap download-table">
<table>
<thead><tr>
<th>{{ '系统' if language == 'zh' else 'OS' }}</th>
<th>{{ '架构' if language == 'zh' else 'Architecture' }}</th>
<th>{{ '后端' if language == 'zh' else 'Backend' }}</th>
<th>{{ '下载' if language == 'zh' else 'Download' }}</th>
<th>SHA-256</th>
</tr></thead>
<tbody>
{% for item in downloads %}
<tr data-download-asset>
<td>{{ item.operating_system }}</td>
<td>{{ item.architecture }}</td>
<td>{{ item.backend }}</td>
<td><a href="{{ item.url }}">{{ item.archive }}</a></td>
<td><code data-field="sha256">{{ item.sha256 }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endif %}

<section id="commands" class="section content-section command-section" data-section="commands">
<div class="section-heading compact-heading">
<p class="eyebrow">{{ '运行路径' if language == 'zh' else 'Run path' }}</p>
Expand Down
39 changes: 39 additions & 0 deletions web-pages/product-site/tests/browser/product-site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,45 @@ test('selector, language peers, copy, and compatibility routes work', async ({ b
await context.close();
});

for (const viewport of [
{ name: 'mobile', width: 390, height: 844 },
{ name: 'desktop', width: 1440, height: 900 },
]) {
test(`llama.cpp v0.2.0 download matrix is stable at ${viewport.name}`, async ({ page }, testInfo) => {
await page.setViewportSize(viewport);
await page.goto('/deploy/llama-cpp.html');

const section = page.locator('[data-section="downloads"]');
await expect(section.locator('[data-download-asset]')).toHaveCount(9);
await expect(section.locator('a[href*="runtime-llamacpp-v0.2.0"]')).toHaveCount(9);
await expect(page.getByText('Windows AMD Vulkan', { exact: false }).first()).toBeVisible();

await section.evaluate((node) => node.scrollIntoView({ block: 'start' }));

const layout = await page.evaluate(() => {
const tableWrap = document.querySelector<HTMLElement>('.download-table');
const header = document.querySelector<HTMLElement>('.site-header');
const heading = document.querySelector<HTMLElement>('[data-section="downloads"] .section-heading');
return {
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
tableClientWidth: tableWrap?.clientWidth ?? 0,
tableScrollWidth: tableWrap?.scrollWidth ?? 0,
headerBottom: header?.getBoundingClientRect().bottom ?? 0,
headingTop: heading?.getBoundingClientRect().top ?? 0,
};
});
expect(layout.overflow).toBeLessThanOrEqual(1);
expect(layout.tableClientWidth).toBeGreaterThan(0);
expect(layout.tableScrollWidth).toBeGreaterThanOrEqual(layout.tableClientWidth);
expect(layout.headingTop).toBeGreaterThanOrEqual(layout.headerBottom + 8);

await page.screenshot({
path: testInfo.outputPath(`llama-cpp-downloads-${viewport.name}.png`),
fullPage: true,
});
});
}

for (const viewport of [
{ name: 'mobile', width: 390, height: 844 },
{ name: 'desktop', width: 1440, height: 900 },
Expand Down
19 changes: 19 additions & 0 deletions web-pages/product-site/tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ def test_detail_commands_come_from_registry(built_site):
assert command in rendered


@pytest.mark.parametrize(
('relative', 'boundary'),
(
('deploy/llama-cpp.html', 'Windows AMD'),
('en/deploy/llama-cpp.html', 'Windows AMD'),
),
)
def test_llama_cpp_pages_render_v020_download_matrix(built_site, relative, boundary):
soup = read_soup(built_site / relative)
section = soup.select_one('[data-section="downloads"]')

assert section
rows = section.select('[data-download-asset]')
assert len(rows) == 9
assert all(row.select_one('a[href*="runtime-llamacpp-v0.2.0"]') for row in rows)
assert all(len(row.select_one('[data-field="sha256"]').get_text(strip=True)) == 64 for row in rows)
assert boundary in soup.get_text(' ', strip=True)


def test_benchmark_rows_have_complete_conditions(built_site):
registry = load_registry(SITE_ROOT / 'data' / 'deployments.json')
records = [record for entry in registry['deployments'] for record in entry['benchmarks']]
Expand Down
45 changes: 45 additions & 0 deletions web-pages/product-site/tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,51 @@ def test_sensevoice_tensorrt_contract_tracks_merged_native_runtime(valid_registr
assert 'tensorrt version' in limitation


def test_llama_cpp_contract_tracks_v020_release_assets(valid_registry):
entry = next(item for item in valid_registry['deployments'] if item['id'] == 'llama-cpp')

assert entry['tested'] == {
'funasr': 'runtime-llamacpp-v0.2.0',
'runtime': 'llama.cpp@803b7fca',
'verified': '2026-08-11',
}
assert len(entry['downloads']) == 9
assert {item['archive'] for item in entry['downloads']} == {
'funasr-llamacpp-linux-arm64.tar.gz',
'funasr-llamacpp-linux-x64.tar.gz',
'funasr-llamacpp-linux-x64-avx2.tar.gz',
'funasr-llamacpp-linux-x64-vulkan.tar.gz',
'funasr-llamacpp-macos-arm64.tar.gz',
'funasr-llamacpp-windows-x64.zip',
'funasr-llamacpp-windows-x64-avx2.zip',
'funasr-llamacpp-windows-x64-vulkan.zip',
'funasr-llamacpp-windows-x64-cuda.zip',
}
assert all(item['url'].startswith(
'https://github.com/modelscope/FunASR/releases/download/runtime-llamacpp-v0.2.0/'
) for item in entry['downloads'])
assert all(len(item['sha256']) == 64 for item in entry['downloads'])
assert any('actions/runs/31458121788' in item['url'] for item in entry['evidence'])
assert any(
'download-funasr-model.sh sensevoice ./funasr-gguf f16' in command
for command in entry['commands']['install']
)
assert 'sensevoice-small-f16.gguf' in entry['commands']['launch'][0]
assert 'AMD' in entry['translations']['en']['primary_limitation']


def test_download_assets_require_https_and_sha256(valid_registry):
data = copy.deepcopy(valid_registry)
entry = next(item for item in data['deployments'] if item['id'] == 'llama-cpp')
entry['downloads'][0]['url'] = 'http://example.com/runtime.tar.gz'
entry['downloads'][1]['sha256'] = 'not-a-sha256'

errors = validate_registry(data)

assert 'llama-cpp: download URL must use https (item 0)' in errors
assert 'llama-cpp: download SHA-256 must be 64 lowercase hex characters (item 1)' in errors


def test_production_entry_requires_evidence(valid_registry):
data = copy.deepcopy(valid_registry)
entry = next(item for item in data['deployments'] if item['maturity'] == 'production-verified')
Expand Down
Loading