Merge pull request #3736: correct LiteLLM website transcription route

Validate the combined website and SenseVoice tree against current main.

Signed-off-by: LauraGPT <[email protected]>
This commit is contained in:
zhifu gao
2026-09-29 14:43:39 +08:00
committed by GitHub
6 changed files with 156 additions and 118 deletions
+2 -2
View File
@@ -266,7 +266,7 @@ High-star feature requests and roadmap issues are earlier in the funnel than PRs
| `deepset-ai/haystack-core-integrations#3572` FunASR-Haystack Python 3.14 installation | Removes an installation scare in Haystack's integration catalog that can prevent new users from trying FunASR | A current Windows CPython 3.14 resolver check succeeds with `funasr 1.3.14`, `umap-learn 0.5.12`, `pynndescent 0.6.0`, `numba 0.66.0`, and `llvmlite 0.48.0`; the reported `llvmlite 0.36.0` path is therefore more consistent with a stale lock or lagging package index than the current dependency graph. Diagnostic commands and requested lock/index evidence are at https://github.com/deepset-ai/haystack-core-integrations/issues/3572#issuecomment-4965365012. Do not change core packaging until the reporter provides a current failing resolution. |
| `crewAIInc/crewAI#5983` FunASR for voice-enabled agents | Routes a 55k-star multi-agent framework toward a provider-neutral voice command path where FunASR/SenseVoice can be a local OpenAI-compatible transcription backend | LauraGPT revived the stale broad request with a concrete `voice.stt.*` config, multipart `/v1/audio/transcriptions` contract, mock endpoint test shape, and agent-command handoff at https://github.com/crewAIInc/crewAI/issues/5983#issuecomment-4901293351. Watch for maintainer direction before opening a code PR. |
| `Significant-Gravitas/AutoGPT#13347` FunASR as an open-source STT backend | Keeps the 185k-star AutoGPT voice-input discussion anchored on a generic OpenAI-compatible transcription contract rather than a heavyweight FunASR-only dependency | LauraGPT mapped the existing hard-coded Copilot Whisper route, then opened PR `Significant-Gravitas/AutoGPT#13500` and linked it back at https://github.com/Significant-Gravitas/AutoGPT/issues/13347#issuecomment-4908286807. The PR later merged, so keep this as a completed high-visibility OpenAI-compatible STT contract win rather than an active operator item. |
| `modelscope/FunASR#3334/#3350/#3361/#3362/#3363/#3366/#3398` owned conversion and link hygiene | Protects owned website, docs, runtime, model_zoo, and first-run troubleshooting surfaces that already send qualified traffic to the four repositories | Merged on 2026-07-23 and hardened again after the donor-page and troubleshooting reviews. `scripts/check_funasr_website_static.py` now patrols 14 public pages across home, ecosystem, donor, CLI tutorial, llama.cpp landing, llama.cpp blog, and launch-story surfaces. It requires the current `35K+` ecosystem proof, visible donor copy saying donations buy servers and the `www.funasr.com` domain, LiteLLM `custom_openai` routing, `funasr >= 1.3.26` CLI guidance, and `runtime-llamacpp-v0.1.9` download paths, including the Windows Vulkan ZIP, while forbidding stale `16K+`, `1.3.10`, and `runtime-llamacpp-v0.1.1` regressions. PRs #3361-#3363 refreshed high-traffic runtime download commands, package/help metadata, model_zoo Hugging Face links, and verified core ModelScope entries from stale `alibaba-damo-academy` / `damo` paths to current `modelscope/FunASR` / `iic` pages. `modelscope/FunASR#3366` then added README-linked install and deployment FAQs at [`docs/troubleshooting.md`](./troubleshooting.md) and [`docs/troubleshooting_zh.md`](./troubleshooting_zh.md), covering `torch` / `torchaudio`, ModelScope versus Hugging Face downloads, `funasr-server` `/v1/audio/transcriptions`, `WebSocket` realtime checks, `llama.cpp` / `GGUF`, and `Deployment Help` issue details. Post-merge validation passed the static contract suite, docs link guards, FAQ guards, real public website contract runs across all 14 pages, and HTTP 200 checks for updated owned/model links. |
| `modelscope/FunASR#3334/#3350/#3361/#3362/#3363/#3366/#3398` owned conversion and link hygiene | Protects owned website, docs, runtime, model_zoo, and first-run troubleshooting surfaces that already send qualified traffic to the four repositories | Historical July 2026 record: these PRs protected 14 public pages, donor usage copy, CLI installation guidance, runtime downloads, model links, and the README-linked [English](./troubleshooting.md) / [Chinese](./troubleshooting_zh.md) troubleshooting guides. Their original static, link, FAQ, and public HTTP checks passed, but the old LiteLLM `custom_openai` assertion was incorrect for transcription. The September 29 correction requires visible `openai/sensevoice` and an explicit self-hosted `api_base`, rejects the old route and checkpoint-ID example, and updates both ecosystem source pages. Source validation is separate from publication; it does not establish that the live website has deployed the correction. |
| `modelscope/FunClip#188/#189`, `QwenAudio/Fun-ASR#151`, `QwenAudio/SenseVoice#327` four-repo community templates | Reduces maintainer follow-up and makes first external contributions easier to review across the four core repositories | Merged on 2026-07-23 after the four-repo template inventory. The four core repositories now have `CONTRIBUTING.md`, issue templates, and PR templates. FunClip gained bug/feature issue templates, a PR template, CONTRIBUTING.md, and template guards; Fun-ASR gained a PR template covering Nano, MLT, streaming, vLLM, GGUF, and HF/ModelScope routing; SenseVoice gained a PR template covering ASR, emotion/event tags, API/web UI/ONNX/Docker, GGUF, and model-card routing. Post-merge validation passed the new template guards plus each repo's lightweight docs/runtime smoke tests. |
| `chatchat-space/Langchain-Chatchat#5479` FunASR/SenseVoice voice input | Puts FunASR in a 38k-star Chinese RAG/Agent app where local audio upload can feed existing Chat and knowledge-base flows | LauraGPT proposed an OpenAI-compatible ASR endpoint slice, including `asr.base_url`, multipart request shape, minimal `{ "text": "..." }` response, and mocked CI endpoint at https://github.com/chatchat-space/Langchain-Chatchat/issues/5479#issuecomment-4901293565. Monitor stale handling and maintainer appetite for a first audio-upload transcription path. |
| `royshil/obs-localvocal#314` SenseVoice/Paraformer engine option | Opens an OBS live-captioning path where SenseVoice/Paraformer can be evaluated through a cleaner ASR-engine boundary instead of a Whisper-only runtime | Maintainer is interested but capacity-limited until mid/late August. LauraGPT mapped current Whisper coupling points and suggested a facade-only first refactor at https://github.com/royshil/obs-localvocal/issues/314#issuecomment-4909662013; wait for a refactor branch or review request before proposing Sherpa-ONNX/FunASR runtime code. |
@@ -302,7 +302,7 @@ High-star feature requests and roadmap issues are earlier in the funnel than PRs
| `zts212653/clowder-ai#1083` Qwen3-ASR service unification | Puts Qwen3-ASR into a user-facing local STT service slot by making it a `whisper-stt` backend variant instead of a separate, easier-to-misconfigure service | Merged on 2026-07-13 as `704bb735cae01be4bcd4089427bc6983c99a43a3` after fixes for Rosetta hardware detection, Qwen3-ASR install/server dispatch, async backend locking, temp WAV cleanup, and stale setup docs. The upstream service slot is already discoverable from the FunASR EN/ZH community integrations pages. |
| `run-llama/llama_index#21958` FunASR endpoint reader | Puts FunASR behind a LlamaIndex reader for OpenAI-compatible transcription endpoints used in RAG and agent pipelines | LauraGPT rechecked head `07a8599deaebe5e7a559d62174e7a872870c2f7e` at https://github.com/run-llama/llama_index/pull/21958#issuecomment-4905345545; author acknowledged the shared-reader pytest caveat at https://github.com/run-llama/llama_index/pull/21958#issuecomment-4905384536. Keep the endpoint contract clear and avoid forcing local `funasr` dependencies into the main package. |
| `run-llama/llama_index#21996` local FunASR reader | Gives LlamaIndex users a local SenseVoice/FunASR reader for private transcription workflows | Keep optional dependencies isolated, verify the reader does not affect default installs, and watch for maintainer guidance on package extras. |
| `BerriAI/litellm-docs#610` FunASR transcription proxy docs | Puts FunASR/SenseVoice behind LiteLLM's existing OpenAI-compatible `custom_openai` audio transcription route, reaching teams that already deploy LiteLLM gateways | Rebased across the upstream `docs/audio_transcription.md` conflict on 2026-07-23; head `1a70fa02` is mergeable with CodeRabbit, Vercel, and Vercel preview comments all green. The docs now keep upstream's `mock_testing_fallbacks` deprecation note for Proxy requests, use `funasr>=1.3.26`, preserve `api_base: http://localhost:8000/v1`, and avoid installing `vllm` in the basic setup path. Local validation passed `git diff --check`, content assertions, and `npm run build`; build warnings are existing site-wide Docusaurus broken links outside `/docs/audio_transcription`. Evidence posted at https://github.com/BerriAI/litellm-docs/pull/610#issuecomment-5049681978. |
| `BerriAI/litellm-docs#610` FunASR transcription proxy docs | Routes self-hosted FunASR/SenseVoice transcription through LiteLLM's `openai/sensevoice` provider/model with an explicit backend `api_base`, reaching teams that deploy LiteLLM gateways | At 2026-09-29 02:26 UTC, signed head `64ab922e497ce0f181565e31c9b29025f48b01e6` includes 883 upstream main commits and corrects the unsupported `custom_openai` transcription provider, checkpoint-ID model name, and weak Proxy master-key example. Native LiteLLM 1.103.0 / OpenAI SDK transport checks, FunASR 1.4.16 ASGI tests with inference stubbed, and the exact-main master-key guard passed 14 focused tests; the documentation build produced 1,357 HTML pages and desktop/mobile browser checks passed. No speech model was downloaded or inferred, and a live Proxy was not booted. The PR remains open: hosted docs checks require approval with zero jobs run, and Vercel deployment is pending. The green July checks and successful preview-comment integration are not evidence of a current successful deployment. [Current PR and validation](https://github.com/BerriAI/litellm-docs/pull/610). |
| `mem0ai/mem0#5571` optional FunASR transcription helper | Adds local FunASR transcription to a high-star memory layer used by agent builders | Keep FunASR optional and make examples clear about local model downloads; the current failing Vercel preview is a Mem0 team authorization gate, not a FunASR code failure, so wait for maintainer-side preview access or review. |
| `Significant-Gravitas/AutoGPT#13500` configurable transcription endpoints | Opens a path from AutoGPT's high-visibility agent platform to local FunASR/SenseVoice gateways through the existing OpenAI-compatible transcription route | Merged on 2026-07-22 after the review and status gates cleared. The merged path keeps transcription provider configuration generic, so FunASR/SenseVoice can fit through an OpenAI-compatible endpoint without adding a heavyweight toolkit dependency to AutoGPT. |
| `Vexa-ai/vexa#810` custom STT endpoint documentation | Makes bring-your-own OpenAI-compatible transcription servers visible in a meeting-bot docs surface, giving SenseVoiceSmall and Fun-ASR-Nano users a clean path to point Vexa at private FunASR/SenseVoice gateways | Delivered via Vexa's community-batch release `#867`, merged to `main` as `e617d839` and shipped in `v0.12.16` with `docs/docs/how-to/custom-stt.mdx`. Maintainer confirmed on 2026-07-22 that #810 is superseded by the delivered release content, so treat the closed #810 PR as completed evidence rather than an active merge-card gate. |
+30 -48
View File
@@ -16,9 +16,7 @@ import xml.etree.ElementTree as ET
BASE_URL = "https://www.funasr.com"
DONOR_PAGE_URLS = frozenset(
(f"{BASE_URL}/donors.html", f"{BASE_URL}/en/donors.html")
)
DONOR_PAGE_URLS = frozenset((f"{BASE_URL}/donors.html", f"{BASE_URL}/en/donors.html"))
@dataclass(frozen=True)
@@ -57,13 +55,18 @@ PAGE_CONTRACTS: dict[str, PageContract] = {
),
f"{BASE_URL}/ecosystem.html": PageContract(
required=(
"36K+",
"37K+",
"/donors.html",
"LiteLLM",
"custom_openai",
"54.3K stars",
),
visible_required=("api_base",),
visible_patterns=(
("openai/sensevoice", r"(?<![\w/])openai/sensevoice(?![\w./+-])"),
(
"http://127.0.0.1:8000/v1",
r"(?<![\w/])http://127\.0\.0\.1:8000/v1(?![\w/?#.:+-])",
),
("FunASR 官方插件 0.1.1", r"FunASR 官方插件 0\.1\.1(?![\w.+-])"),
("最大 25 MB 音频上传", r"最大 (?<!\d)25 MB 音频上传"),
),
@@ -75,17 +78,22 @@ PAGE_CONTRACTS: dict[str, PageContract] = {
"https://github.com/0xShug0/audio.cpp/blob/1778b23a5f6a4951c788e4bb0e7baa04f20012a2/docs/models/fun_asr_nano.md",
"https://github.com/RVC-Boss/GPT-SoVITS/pull/2824",
),
forbidden=("16K+",),
forbidden=("16K+", "custom_openai", "openai/FunAudioLLM/SenseVoiceSmall"),
),
f"{BASE_URL}/en/ecosystem.html": PageContract(
required=(
"36K+",
"37K+",
"/en/donors.html",
"LiteLLM",
"custom_openai",
"54.3K stars",
),
visible_required=("api_base",),
visible_patterns=(
("openai/sensevoice", r"(?<![\w/])openai/sensevoice(?![\w./+-])"),
(
"http://127.0.0.1:8000/v1",
r"(?<![\w/])http://127\.0\.0\.1:8000/v1(?![\w/?#.:+-])",
),
("FunASR plugin 0.1.1", r"FunASR plugin 0\.1\.1(?![\w.+-])"),
("25 MB uploads", r"(?<!\d)25 MB uploads"),
),
@@ -97,7 +105,7 @@ PAGE_CONTRACTS: dict[str, PageContract] = {
"https://github.com/0xShug0/audio.cpp/blob/1778b23a5f6a4951c788e4bb0e7baa04f20012a2/docs/models/fun_asr_nano.md",
"https://github.com/RVC-Boss/GPT-SoVITS/pull/2824",
),
forbidden=("16K+",),
forbidden=("16K+", "custom_openai", "openai/FunAudioLLM/SenseVoiceSmall"),
),
f"{BASE_URL}/donors.html": PageContract(
required=(
@@ -444,9 +452,7 @@ class _VisibleContentCollector(HTMLParser):
tag = tag.lower()
attributes = {name.lower(): value for name, value in attrs}
hidden = (
self._parent_hidden
or tag in _HIDDEN_TAGS
or self._has_hidden_attribute(attributes)
self._parent_hidden or tag in _HIDDEN_TAGS or self._has_hidden_attribute(attributes)
)
if not hidden:
if tag == "img" and attributes.get("src"):
@@ -460,9 +466,7 @@ class _VisibleContentCollector(HTMLParser):
tag = tag.lower()
attributes = {name.lower(): value for name, value in attrs}
hidden = (
self._parent_hidden
or tag in _HIDDEN_TAGS
or self._has_hidden_attribute(attributes)
self._parent_hidden or tag in _HIDDEN_TAGS or self._has_hidden_attribute(attributes)
)
if not hidden and tag == "img" and attributes.get("src"):
self.images.add(attributes["src"])
@@ -541,10 +545,7 @@ class _DirectoryNavigationCollector(HTMLParser):
tag = tag.lower()
attributes = {name.lower(): value for name, value in attrs}
if not self._stack:
is_navigation = (
tag == "div"
and "nav-links" in (attributes.get("class") or "").split()
)
is_navigation = tag == "div" and "nav-links" in (attributes.get("class") or "").split()
if not is_navigation:
return
self.found_navigation = True
@@ -619,9 +620,7 @@ def validate_navigation(pages: dict[str, str]) -> list[str]:
directory_links: list[DirectoryLink] = []
for link in navigation_links:
target = urlparse(urljoin(url, link.href))
is_language_toggle = (
target.netloc == "www.funasr.com" and target.path == alternate_path
)
is_language_toggle = target.netloc == "www.funasr.com" and target.path == alternate_path
is_github_action = target.netloc == "github.com"
if not is_language_toggle and not is_github_action:
directory_links.append(link)
@@ -633,21 +632,14 @@ def validate_navigation(pages: dict[str, str]) -> list[str]:
expected_links = [link for link in navigation_links if link.href == expected]
if not any(link.label in expected_labels for link in expected_links):
label_description = "` or `".join(expected_labels)
failures.append(
f"{url}: `{expected}` must use visible label `{label_description}`"
)
if expected in links and (
not directory_links or directory_links[-1].href != expected
):
failures.append(f"{url}: `{expected}` must use visible label `{label_description}`")
if expected in links and (not directory_links or directory_links[-1].href != expected):
failures.append(f"{url}: `{expected}` must be the last directory link")
if links.count(expected) > 1:
failures.append(
f"{url}: directory navigation contains duplicate `{expected}`"
)
failures.append(f"{url}: directory navigation contains duplicate `{expected}`")
if wrong_language in links:
failures.append(
f"{url}: directory navigation contains wrong-language "
f"`{wrong_language}`"
f"{url}: directory navigation contains wrong-language " f"`{wrong_language}`"
)
return failures
@@ -723,9 +715,7 @@ def validate_assets(assets: dict[str, bytes]) -> list[str]:
return failures
def _fetch_bytes(
url: str, timeout: float, retries: int, require_exact_url: bool = False
) -> bytes:
def _fetch_bytes(url: str, timeout: float, retries: int, require_exact_url: bool = False) -> bytes:
last_error: Exception | None = None
for attempt in range(retries + 1):
try:
@@ -741,17 +731,13 @@ def _fetch_bytes(
raise last_error
def _fetch_url(
url: str, timeout: float, retries: int, require_exact_url: bool = False
) -> str:
def _fetch_url(url: str, timeout: float, retries: int, require_exact_url: bool = False) -> str:
return _fetch_bytes(
url,
timeout=timeout,
retries=retries,
require_exact_url=require_exact_url,
).decode(
"utf-8", errors="replace"
)
).decode("utf-8", errors="replace")
def fetch_pages(timeout: float, retries: int = 3) -> dict[str, str]:
@@ -790,9 +776,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--retries", type=int, default=3)
args = parser.parse_args(argv)
navigation_pages = fetch_navigation_pages(
timeout=args.timeout, retries=args.retries
)
navigation_pages = fetch_navigation_pages(timeout=args.timeout, retries=args.retries)
pages = {}
for url in PAGE_CONTRACTS:
if url in DONOR_PAGE_URLS:
@@ -810,9 +794,7 @@ def main(argv: list[str] | None = None) -> int:
all_navigation_pages.update(pages)
failures = validate_pages(pages)
failures.extend(validate_navigation(all_navigation_pages))
failures.extend(
validate_assets(fetch_assets(timeout=args.timeout, retries=args.retries))
)
failures.extend(validate_assets(fetch_assets(timeout=args.timeout, retries=args.retries)))
if failures:
print("funasr.com static page contract failed:", file=sys.stderr)
for failure in failures:
+120 -64
View File
@@ -50,7 +50,7 @@ def test_website_contract_accepts_current_public_copy():
vLLM Acceleration
""",
"https://www.funasr.com/ecosystem.html": """
<div class="stat-num">36K+</div>
<div class="stat-num">37K+</div>
<a href="/donors.html">功德榜</a>
<a href="https://github.com/BerriAI/litellm">LiteLLM</a>
<a href="https://github.com/modelscope/FunClip/releases/tag/v2.1.1">FunClip v2.1.1</a>
@@ -58,7 +58,7 @@ def test_website_contract_accepts_current_public_copy():
<a href="https://github.com/0xShug0/audio.cpp/pull/155">merged PR</a>
<a href="https://github.com/0xShug0/audio.cpp/blob/1778b23a5f6a4951c788e4bb0e7baa04f20012a2/docs/models/fun_asr_nano.md">pinned guide</a>
<a href="https://github.com/RVC-Boss/GPT-SoVITS/pull/2824">merged Transformers fix</a>
<div>custom_openai</div>
<div>openai/sensevoice api_base: http://127.0.0.1:8000/v1</div>
<div>54.3K stars</div>
<a href="https://marketplace.dify.ai/plugin/langgenius/funasr">
FunASR 官方插件 0.1.1
@@ -66,7 +66,7 @@ def test_website_contract_accepts_current_public_copy():
<div>支持最大 25 MB 音频上传</div>
""",
"https://www.funasr.com/en/ecosystem.html": """
<div class="stat-num">36K+</div>
<div class="stat-num">37K+</div>
<a href="/en/donors.html">Thanks</a>
<a href="https://github.com/BerriAI/litellm">LiteLLM</a>
<a href="https://github.com/modelscope/FunClip/releases/tag/v2.1.1">FunClip v2.1.1</a>
@@ -74,7 +74,7 @@ def test_website_contract_accepts_current_public_copy():
<a href="https://github.com/0xShug0/audio.cpp/pull/155">merged PR</a>
<a href="https://github.com/0xShug0/audio.cpp/blob/1778b23a5f6a4951c788e4bb0e7baa04f20012a2/docs/models/fun_asr_nano.md">pinned guide</a>
<a href="https://github.com/RVC-Boss/GPT-SoVITS/pull/2824">merged Transformers fix</a>
<div>custom_openai</div>
<div>openai/sensevoice api_base: http://127.0.0.1:8000/v1</div>
<div>54.3K stars</div>
<a href="https://marketplace.dify.ai/plugin/langgenius/funasr">
FunASR plugin 0.1.1
@@ -277,9 +277,22 @@ def test_homepage_contract_accepts_current_product_site_build(tmp_path, monkeypa
assert checker.validate_pages(pages) == []
def test_llamacpp_comparison_contract_accepts_current_product_site_build(
tmp_path, monkeypatch
):
def test_ecosystem_contract_accepts_current_product_site_build(tmp_path, monkeypatch):
checker = _load_module()
builder = _load_product_site_builder()
builder.build(tmp_path)
routes = ("ecosystem.html", "en/ecosystem.html")
pages = {
f"https://www.funasr.com/{route}": (tmp_path / route).read_text(encoding="utf-8")
for route in routes
}
monkeypatch.setattr(
checker, "PAGE_CONTRACTS", {url: checker.PAGE_CONTRACTS[url] for url in pages}
)
assert checker.validate_pages(pages) == []
def test_llamacpp_comparison_contract_accepts_current_product_site_build(tmp_path, monkeypatch):
checker = _load_module()
builder = _load_product_site_builder()
builder.build(tmp_path)
@@ -288,9 +301,7 @@ def test_llamacpp_comparison_contract_accepts_current_product_site_build(
"en/blog/funasr-llama-cpp-whisper-cpp-alternative.html",
)
pages = {
f"https://www.funasr.com/{route}": (tmp_path / route).read_text(
encoding="utf-8"
)
f"https://www.funasr.com/{route}": (tmp_path / route).read_text(encoding="utf-8")
for route in routes
}
monkeypatch.setattr(
@@ -326,7 +337,7 @@ def test_ecosystem_contract_requires_current_release_and_native_runtime():
"https://www.funasr.com/en/ecosystem.html",
):
contract = checker.PAGE_CONTRACTS[url]
assert "36K+" in contract.required
assert "37K+" in contract.required
assert evidence_links <= set(contract.required_links)
@@ -336,10 +347,10 @@ def test_ecosystem_contract_requires_live_dify_marketplace():
pages = {
url: """
<body>
<div class="stat-num">36K+</div>
<div class="stat-num">37K+</div>
<a href="/en/donors.html">Thanks</a>
<a href="https://github.com/BerriAI/litellm">LiteLLM</a>
<div>custom_openai</div>
<div>openai/sensevoice api_base: http://127.0.0.1:8000/v1</div>
<div>54.3K stars</div>
<a href="https://github.com/langgenius/dify">Dify</a>
</body>
@@ -350,8 +361,7 @@ def test_ecosystem_contract_requires_live_dify_marketplace():
assert any(
url in failure
and "missing link `https://marketplace.dify.ai/plugin/langgenius/funasr`"
in failure
and "missing link `https://marketplace.dify.ai/plugin/langgenius/funasr`" in failure
for failure in failures
)
assert any(
@@ -359,15 +369,14 @@ def test_ecosystem_contract_requires_live_dify_marketplace():
for failure in failures
)
assert any(
url in failure and "visible text missing `25 MB uploads`" in failure
for failure in failures
url in failure and "visible text missing `25 MB uploads`" in failure for failure in failures
)
def _valid_english_ecosystem_html():
return """
<body>
<div class="stat-num">36K+</div>
<div class="stat-num">37K+</div>
<a href="/en/donors.html">Thanks</a>
<a href="https://github.com/BerriAI/litellm">LiteLLM</a>
<a href="https://github.com/modelscope/FunClip/releases/tag/v2.1.1">FunClip v2.1.1</a>
@@ -375,7 +384,7 @@ def _valid_english_ecosystem_html():
<a href="https://github.com/0xShug0/audio.cpp/pull/155">merged PR</a>
<a href="https://github.com/0xShug0/audio.cpp/blob/1778b23a5f6a4951c788e4bb0e7baa04f20012a2/docs/models/fun_asr_nano.md">pinned guide</a>
<a href="https://github.com/RVC-Boss/GPT-SoVITS/pull/2824">merged Transformers fix</a>
<div>custom_openai</div>
<div>openai/sensevoice api_base: http://127.0.0.1:8000/v1</div>
<div>54.3K stars</div>
<a href="https://marketplace.dify.ai/plugin/langgenius/funasr">
FunASR plugin 0.1.1
@@ -385,6 +394,75 @@ def _valid_english_ecosystem_html():
"""
@pytest.fixture(params=["en/", ""])
def litellm_ecosystem_page(request):
language = request.param
html = _valid_english_ecosystem_html()
if not language:
html = (
html.replace("/en/donors.html", "/donors.html")
.replace("FunASR plugin 0.1.1", "FunASR 官方插件 0.1.1")
.replace("supports 25 MB uploads", "支持最大 25 MB 音频上传")
)
return f"https://www.funasr.com/{language}ecosystem.html", html
def test_ecosystem_accepts_litellm_transcription_route(litellm_ecosystem_page):
checker = _load_module()
url, html = litellm_ecosystem_page
assert not [
failure for failure in checker.validate_pages({url: html}) if failure.startswith(url + ":")
]
@pytest.mark.parametrize("legacy", ["custom_openai", "openai/FunAudioLLM/SenseVoiceSmall"])
def test_ecosystem_rejects_legacy_litellm_routing(litellm_ecosystem_page, legacy):
checker = _load_module()
url, html = litellm_ecosystem_page
failures = checker.validate_pages({url: html + f"<code>{legacy}</code>"})
assert f"{url}: forbidden `{legacy}`" in failures
@pytest.mark.parametrize("suffix", ["Small", "/model", "-extra"])
def test_ecosystem_rejects_wrong_litellm_model_alias(litellm_ecosystem_page, suffix):
checker = _load_module()
url, html = litellm_ecosystem_page
html = html.replace("openai/sensevoice", "openai/sensevoice" + suffix)
failures = checker.validate_pages({url: html})
assert f"{url}: visible text missing `openai/sensevoice`" in failures
@pytest.mark.parametrize("required", ["openai/sensevoice", "api_base", "http://127.0.0.1:8000/v1"])
def test_ecosystem_requires_visible_litellm_configuration(litellm_ecosystem_page, required):
checker = _load_module()
url, html = litellm_ecosystem_page
html = html.replace(required, f"<span hidden>{required}</span>")
failures = checker.validate_pages({url: html})
assert f"{url}: visible text missing `{required}`" in failures
@pytest.mark.parametrize("suffix", ["0", "/audio/transcriptions", "?route=other"])
def test_ecosystem_rejects_wrong_litellm_api_base(litellm_ecosystem_page, suffix):
checker = _load_module()
url, html = litellm_ecosystem_page
api_base = "http://127.0.0.1:8000/v1"
html = html.replace(api_base, api_base + suffix)
failures = checker.validate_pages({url: html})
assert f"{url}: visible text missing `{api_base}`" in failures
def test_ecosystem_source_uses_working_litellm_configuration(litellm_ecosystem_page):
checker = _load_module()
url, _ = litellm_ecosystem_page
relative = url.removeprefix("https://www.funasr.com/")
source = ROOT / "web-pages" / "product-site" / "legacy" / relative
visible = checker.extract_visible_text(source.read_text())
assert "openai/sensevoice" in visible
assert "api_base" in visible
assert "http://127.0.0.1:8000/v1" in visible
assert "custom_openai" not in visible
def test_ecosystem_contract_rejects_longer_dify_version():
checker = _load_module()
url = "https://www.funasr.com/en/ecosystem.html"
@@ -393,9 +471,7 @@ def test_ecosystem_contract_rejects_longer_dify_version():
failures = checker.validate_pages(pages)
assert any(
url in failure and "FunASR plugin 0.1.1" in failure for failure in failures
)
assert any(url in failure and "FunASR plugin 0.1.1" in failure for failure in failures)
def test_ecosystem_contract_rejects_dify_version_suffixes():
@@ -408,9 +484,7 @@ def test_ecosystem_contract_rejects_dify_version_suffixes():
failures = checker.validate_pages(pages)
assert any(
url in failure and "FunASR plugin 0.1.1" in failure for failure in failures
)
assert any(url in failure and "FunASR plugin 0.1.1" in failure for failure in failures)
def test_ecosystem_contract_rejects_larger_dify_upload_limit():
@@ -475,8 +549,7 @@ def test_website_contract_includes_funclip_v210_launch():
in checker.PAGE_CONTRACTS
)
assert (
"https://www.funasr.com/img/funclip-v2-1-0-interface.jpg"
in checker.STATIC_ASSET_CONTRACTS
"https://www.funasr.com/img/funclip-v2-1-0-interface.jpg" in checker.STATIC_ASSET_CONTRACTS
)
@@ -523,17 +596,14 @@ def test_funclip_v210_contract_rejects_hidden_evidence_and_wrong_routes():
for failure in failures
)
assert any(
url in failure
and "missing link `https://github.com/modelscope/FunClip`" in failure
url in failure and "missing link `https://github.com/modelscope/FunClip`" in failure
for failure in failures
)
assert any(
url in failure and "missing link `/en/donors.html`" in failure
for failure in failures
url in failure and "missing link `/en/donors.html`" in failure for failure in failures
)
assert any(
url in failure
and "missing image `/img/funclip-v2-1-0-interface.jpg`" in failure
url in failure and "missing image `/img/funclip-v2-1-0-interface.jpg`" in failure
for failure in failures
)
@@ -573,13 +643,11 @@ def test_funclip_v210_contract_rejects_zero_area_links():
failures = checker.validate_pages(pages)
assert any(
url in failure
and "missing link `https://github.com/modelscope/FunClip`" in failure
url in failure and "missing link `https://github.com/modelscope/FunClip`" in failure
for failure in failures
)
assert any(
url in failure and "missing link `/en/donors.html`" in failure
for failure in failures
url in failure and "missing link `/en/donors.html`" in failure for failure in failures
)
@@ -606,8 +674,7 @@ def test_v1328_contract_rejects_language_swapped_body():
failures = checker.validate_pages(pages)
assert any(
url in failure
and "visible text missing `精确合并源码通过 118 项聚焦回归测试`" in failure
url in failure and "visible text missing `精确合并源码通过 118 项聚焦回归测试`" in failure
for failure in failures
)
@@ -640,8 +707,7 @@ def test_v1328_contract_rejects_css_test_count_and_indirect_repo_link():
for failure in failures
)
assert any(
url in failure
and "missing link `https://github.com/modelscope/FunASR`" in failure
url in failure and "missing link `https://github.com/modelscope/FunASR`" in failure
for failure in failures
)
@@ -670,14 +736,12 @@ def test_v1328_contract_requires_visible_stop_and_sensevoice_copy():
failures = checker.validate_pages(pages)
assert any(
url in failure
and "visible text missing `including STOP final decode`" in failure
url in failure and "visible text missing `including STOP final decode`" in failure
for failure in failures
)
assert any(
url in failure
and "visible text missing `SenseVoice users receive the subtitle alignment fix`"
in failure
and "visible text missing `SenseVoice users receive the subtitle alignment fix`" in failure
for failure in failures
)
@@ -725,16 +789,15 @@ def test_website_contract_reports_stale_runtime_and_star_copy():
pages = {url: "" for url in checker.PAGE_CONTRACTS}
pages["https://www.funasr.com/ecosystem.html"] = "16K+"
pages[
"https://www.funasr.com/blog/funasr-llama-cpp-whisper-cpp-alternative.html"
] = "runtime-llamacpp-v0.1.1"
pages["https://www.funasr.com/blog/funasr-llama-cpp-whisper-cpp-alternative.html"] = (
"runtime-llamacpp-v0.1.1"
)
pages["https://www.funasr.com/llama-cpp.html"] = "runtime-llamacpp-v0.1.1"
failures = checker.validate_pages(pages)
assert any(
"ecosystem.html" in failure and "forbidden `16K+`" in failure
for failure in failures
"ecosystem.html" in failure and "forbidden `16K+`" in failure for failure in failures
)
assert any(
"funasr-llama-cpp-whisper-cpp-alternative.html" in failure
@@ -771,8 +834,7 @@ def test_website_contract_requires_visible_donor_usage_copy():
failures = checker.validate_pages(pages)
assert any(
"donors.html" in failure
and "visible text missing `购买和维护服务器`" in failure
"donors.html" in failure and "visible text missing `购买和维护服务器`" in failure
for failure in failures
)
assert any(
@@ -855,8 +917,7 @@ def test_navigation_contract_requires_donors_as_last_directory_link():
assert not any("ok.html" in failure for failure in failures)
assert any(
"missing.html" in failure and "missing `/en/donors.html`" in failure
for failure in failures
"missing.html" in failure and "missing `/en/donors.html`" in failure for failure in failures
)
assert any(
"not-last.html" in failure and "must be the last directory link" in failure
@@ -867,8 +928,7 @@ def test_navigation_contract_requires_donors_as_last_directory_link():
for failure in failures
)
assert any(
"wrong-language.html" in failure
and "contains wrong-language `/donors.html`" in failure
"wrong-language.html" in failure and "contains wrong-language `/donors.html`" in failure
for failure in failures
)
assert not any("plain.html" in failure for failure in failures)
@@ -903,23 +963,19 @@ def test_navigation_contract_requires_visible_correctly_labeled_donor_link():
failures = checker.validate_navigation(pages)
assert any(
"hidden.html" in failure
and "missing `/donors.html`; link must be visible" in failure
"hidden.html" in failure and "missing `/donors.html`; link must be visible" in failure
for failure in failures
)
assert any(
"wrong-label.html" in failure
and "must use visible label `功德榜`" in failure
"wrong-label.html" in failure and "must use visible label `功德榜`" in failure
for failure in failures
)
assert any(
"hidden-label.html" in failure
and "missing `/donors.html`; link must be visible" in failure
"hidden-label.html" in failure and "missing `/donors.html`; link must be visible" in failure
for failure in failures
)
assert any(
"empty.html" in failure
and "missing `/en/donors.html`; link must be visible" in failure
"empty.html" in failure and "missing `/en/donors.html`; link must be visible" in failure
for failure in failures
)
@@ -53,7 +53,7 @@
"css/index.1682178c.css": "b5f6034ce886c25773928c114e4290d1c491a3d081677b488810198efd76b4e1",
"decoder.js": "f2023a28036b1ff58e5bdabbb72ea427f8faaf60e89acd78ff183c313bdb0a2c",
"donors.html": "eec6b789b29c27d372925ae0ad3e13d4c6e62978b3c38c4da9e3aec129f10934",
"ecosystem.html": "de151aebb8d807b21e719d59a54766f4998945310c97bbabf2ac31dae5c0a283",
"ecosystem.html": "a3cdaf0fefdc10fd51e8f09c3239e338528f4700e0037c2eb9556d650f6faef9",
"en/blog/cantonese-speech-recognition.html": "b9bd146198206f781c37f341c370a690dc83cd7163334e34b4e7f65e69af8ff2",
"en/blog/chinese-speech-recognition.html": "16b68bd2a1122236c580c62275002ae2c0875ca14e32686e88d3b5ccfa9f56ef",
"en/blog/fun-asr-nano-guide.html": "1746c708bd0606188de4266d8255984dd9e14634af33c54ad65a5f05b767be1b",
@@ -91,7 +91,7 @@
"en/blog/voice-activity-detection-python.html": "d12f5d0f4ae07c3434f6188d12f5603b8abb9578413b79133668638d1c5e18a7",
"en/blog/which-funasr-model.html": "1207180aa929a3aa51db136c1098f4edc8f07405e2b56fe033998bf996f8ec7f",
"en/donors.html": "eacb7f9434b1b29e13e9b0175bb9680556ee72ab4dbd155e68f9c912eb0212b1",
"en/ecosystem.html": "81cec9464db159189ef29df884b124c795fce01ef08bfaae03d982696de13701",
"en/ecosystem.html": "38356f6bdb5cd89988304be61f13707f4df9fa84c2a191701aaee475d7587902",
"en/index.html": "d86effc3bf4d218eabd465cbbe026f62c98dd50110ddd58251d9d271a64882a2",
"en/llama-cpp.html": "dbfbd125ebbf07f7d923c91b122818f8b09484ce54c1e80eba66a89d22dfb5b4",
"en/models.html": "9806d01ba1807c2a6a88f7232102e185426fb8458bf51e1d7a8ce872352e1fd1",
+1 -1
View File
@@ -276,7 +276,7 @@ footer a{color:#60a5fa}
<div class="card">
<div class="card-title"><a href="https://github.com/BerriAI/litellm">LiteLLM</a></div>
<div class="card-stars">54.3K stars</div>
<div class="card-desc">OpenAI 兼容 AI Gateway。FunASR / SenseVoice 可通过 <code>custom_openai</code> 作为自托管语音转写端点接入。</div>
<div class="card-desc">OpenAI 兼容 AI Gateway。使用 <code>openai/sensevoice</code>,并显式设置 <code>api_base: http://127.0.0.1:8000/v1</code>,可接入同机自托管的 FunASR 转写服务。<code>sensevoice</code> 是服务模型别名,不是 checkpoint 仓库 ID;容器部署需使用容器内可访问的服务地址。<a href="https://github.com/BerriAI/litellm-docs/pull/610">配置文档 PR</a> 尚待上游合并。</div>
<div><span class="card-tag">AI Gateway</span><span class="card-tag">OpenAI 兼容</span></div>
</div>
<div class="card">
@@ -277,7 +277,7 @@ footer a{color:#60a5fa}
<div class="card">
<div class="card-title"><a href="https://github.com/BerriAI/litellm">LiteLLM</a></div>
<div class="card-stars">54.3K stars</div>
<div class="card-desc">OpenAI-compatible AI Gateway. FunASR / SenseVoice can be routed through <code>custom_openai</code> as a self-hosted transcription endpoint.</div>
<div class="card-desc">OpenAI-compatible AI Gateway. Use <code>openai/sensevoice</code> with an explicit <code>api_base: http://127.0.0.1:8000/v1</code> for a self-hosted FunASR transcription server on the same host. <code>sensevoice</code> is a server model alias, not a checkpoint repository ID; containers need a backend address reachable from inside the container. The <a href="https://github.com/BerriAI/litellm-docs/pull/610">configuration docs PR</a> is awaiting upstream merge.</div>
<div><span class="card-tag">AI Gateway</span><span class="card-tag">OpenAI-compatible</span></div>
</div>
<div class="card">