From d7777e2e2f5c0d641f887ec91ed971043c71a55a Mon Sep 17 00:00:00 2001 From: zhifu gao Date: Wed, 9 Sep 2026 09:49:46 +0000 Subject: [PATCH] docs: clarify long-audio segmentation and acceptance Signed-off-by: zhifu gao --- .../product-site/content/legacy-manifest.json | 8 +- .../blog/funasr-transcribe-long-audio.html | 112 ++++--- web-pages/product-site/legacy/blog/index.html | 2 +- .../en/blog/funasr-transcribe-long-audio.html | 112 ++++--- .../product-site/legacy/en/blog/index.html | 2 +- .../tests/browser/long-audio-blog.spec.ts | 52 +++ .../tests/test_long_audio_blog_contracts.py | 303 ++++++++++++++++++ 7 files changed, 501 insertions(+), 90 deletions(-) create mode 100644 web-pages/product-site/tests/browser/long-audio-blog.spec.ts create mode 100644 web-pages/product-site/tests/test_long_audio_blog_contracts.py diff --git a/web-pages/product-site/content/legacy-manifest.json b/web-pages/product-site/content/legacy-manifest.json index 84cedc044..2070a60b7 100644 --- a/web-pages/product-site/content/legacy-manifest.json +++ b/web-pages/product-site/content/legacy-manifest.json @@ -11,7 +11,7 @@ "blog/funasr-llama-cpp-whisper-cpp-alternative.html": "3e1a8901c3f7f74e310b8050e2f6722c736ce0b37155805b85786d5b5455ae8f", "blog/funasr-realtime-streaming-asr.html": "7b6c34ace6c4c782f5af9c3f52181c1b1ef08d32c4ea3fafab63b1f30a9e72db", "blog/funasr-speaker-diarization.html": "451273993769f38de014160788581529788db8e4ca9fb56773d7723c51d96a52", - "blog/funasr-transcribe-long-audio.html": "4f7f593c114dd54b11e3cf2b2e7ddcfea6af29326034ce4a76c690d30f41ff28", + "blog/funasr-transcribe-long-audio.html": "3cb00a3b23eeb5990b639fde779ef738a58a08af8976f86a52860ac052537160", "blog/funasr-v1-3-26-openai-vllm-llama-cpp.html": "304453b66c09d65aaa35dfe679c7368c6c69d65ab68bae7d85c536aadecf44b1", "blog/funasr-v1-3-27-language-metadata-vllm-fallback.html": "f1dbbc70fdd630c8a3953bda3b3a1697412b3c4bc25fc3969385583cd47874d2", "blog/funasr-v1-3-28-realtime-websocket-subtitles.html": "46a8d4c310e7f76da632d558d057484f48b345d9616873e1416fc68fcb9e9633", @@ -24,7 +24,7 @@ "blog/funclip-v2-1-0-video-clipping-release.html": "88f6c44e5332d1746c4db0fc97d755ef82f12e46d8f1c1c0ad9351152ff9dfc1", "blog/funclip-v2-2-0-moss-speaker-clipping.html": "0d50747d3992301fb3062ad2cdf903bd427bec3db1f1e4fab1f06de207928bc9", "blog/generate-subtitles-srt-vtt-from-audio-video.html": "f1133235673d441cc654310581f59a319f182314ab7c0b0824af87da9f4a0591", - "blog/index.html": "c3edf2411f377beb2bf01a0486349b189b1548f6babfc4794c16ab43f49754b4", + "blog/index.html": "f966d5ad5c713a4180df05ff84216fd5ad65f831c57968b9feb7e3f4712bc136", "blog/japanese-speech-recognition.html": "399f5ce84e68ac00854bdf70b52b1cde6795efca7c800fd6492035a37a7c1b68", "blog/lightweight-speech-recognition-cpu.html": "d6270066222ed5baef0df108e6a4155f4a87bb5482169f23f218794d371f3281", "blog/meeting-transcript-acceptance.html": "3e35d1f283c7005977ad8cbd079e38c51e14b51f04f50cedb5636e2d4baf38bd", @@ -53,7 +53,7 @@ "en/blog/funasr-llama-cpp-whisper-cpp-alternative.html": "3808ee7a014f63e3befd83c886eca3fd96abea7d4de68a90a8bd23befe579282", "en/blog/funasr-realtime-streaming-asr.html": "65c7353fcdc393fc0adec3969d9762edc8f30dc15d20e360a2b11508d8c60a91", "en/blog/funasr-speaker-diarization.html": "db6511de2f356b7bf5e3f1f89b33991500a293c9d3bd2420d8a6cd6cf6cf3997", - "en/blog/funasr-transcribe-long-audio.html": "6d486c9cee4a8c339e09e60ecee579b7cd650ce512d1ac8fe25789e2489b87fb", + "en/blog/funasr-transcribe-long-audio.html": "cb9b03a87487b3c551db030f66c16b8a55c042977d5d96cf6012af4cb4a1e273", "en/blog/funasr-v1-3-26-openai-vllm-llama-cpp.html": "665c9c66c9af0e649cc3020859e3af65f6ec595dfdbf36923058b97dc5e2512e", "en/blog/funasr-v1-3-27-language-metadata-vllm-fallback.html": "ffaca54d75c561bda82b6063b27f2a81d810f4f0e89030b6be7a64696ada44b4", "en/blog/funasr-v1-3-28-realtime-websocket-subtitles.html": "233534cb4306387f1a9a695886a5f515679dff591d04b90827f41a668d7f7ada", @@ -66,7 +66,7 @@ "en/blog/funclip-v2-1-0-video-clipping-release.html": "229baf59adf2c3290541d9b3c8a6243992406ba84b712714e9d14599205cb1d4", "en/blog/funclip-v2-2-0-moss-speaker-clipping.html": "644159267f0fa4e7aab9d0eda98f68cd2b6365a9985038b639c626a5ea5ea7e8", "en/blog/generate-subtitles-srt-vtt-from-audio-video.html": "1530d4b9e94820a0b60d801e8d1c19b7aeb031b2f2b76c092657392e47706c6d", - "en/blog/index.html": "8fccbb7bf21902521cfe78cdf054abf75c5b6fe7ed571f25bfe4a04468c565f8", + "en/blog/index.html": "363b5b77b4702ed1f5d7f62fa2eeadb7e4bfa1a56b86ca9c22527d6c7666bec7", "en/blog/japanese-speech-recognition.html": "c476adcc2be1ed7c19e2345cc91b8ee6a0e04efd794b9b6476dd5d9b3a2c8b04", "en/blog/lightweight-speech-recognition-cpu.html": "0e02c0e0853b12613d32c250f593c05c2052f18231207b63ee01d6e6600a86f7", "en/blog/meeting-transcript-acceptance.html": "3183682d2cae0ce18f68e3bd33e22b9effb4de699f6a4d821938cc03dd6835bf", diff --git a/web-pages/product-site/legacy/blog/funasr-transcribe-long-audio.html b/web-pages/product-site/legacy/blog/funasr-transcribe-long-audio.html index f12b3b1bb..fb6d8f105 100644 --- a/web-pages/product-site/legacy/blog/funasr-transcribe-long-audio.html +++ b/web-pages/product-site/legacy/blog/funasr-transcribe-long-audio.html @@ -1,12 +1,12 @@ -用 FunASR 转写超长音频:1 小时录音也能一次搞定 | FunASR - +长录音转写:VAD 分段、批处理与结果验收 | FunASR +
-

用 FunASR 转写超长音频:1 小时录音也能一次搞定

- -

转写一段 1 小时的播客、讲座或会议录音,用 Whisper 会很痛苦:它一次只处理 30 秒,你得自己把音频切片、逐片识别、再把结果拼起来,还要处理切片边界把词切断的问题。

-

FunASR 不用这么麻烦。它内置 VAD(语音端点检测),一次 generate() 调用就能吃下任意时长的音频——自动切句、批量解码、拼回完整文本,你一行切片代码都不用写。

-

安装

-
pip install -U funasr modelscope
-

完整代码(可直接运行)

-
from funasr import AutoModel
+

长录音转写:VAD 分段、批处理与结果验收

+ +

播客、课程和会议录音的难点,不只是把文件交给模型,还包括资源预算、停顿与重叠语音、以及末尾是否漏识别。本文以 FunASR 1.4.15 的离线 SenseVoice + FSMN-VAD 路径为例,说明一次文件级调用内部做了什么,以及上线前该检查什么。

+ +

用一个明确的离线配置开始

+
+

前提是已经准备好的独立 FunASR 1.4.15 环境,包括 PyTorch、可用的音频解码依赖和模型权重访问。先按安装与环境验证指南准备平台依赖,再核对Python SDK 指南。下方版本检查只检查已安装包的元数据,不是完整安装或声学推理证明。

+

把你有权处理的本地文件放在 meeting.wav。先用短录音验证环境,再逐步测试业务长录音;首次加载可能联网下载模型。输出包含转写文本,不要把私人内容放进公开日志。代码固定工具包版本,但未锁定全部依赖和模型 revision,正式部署应另行记录并固定它们。

+
+
from importlib.metadata import version
+from pathlib import Path
+
+installed = version("funasr")
+if installed != "1.4.15":
+    raise RuntimeError(f"Expected FunASR 1.4.15, found {installed}")
+
+audio = Path("meeting.wav")
+if not audio.is_file():
+    raise FileNotFoundError(audio)
+
+from funasr import AutoModel
 
 model = AutoModel(
-    model="iic/SenseVoiceSmall",   # or "paraformer-zh"
-    vad_model="fsmn-vad",          # built-in voice activity detection
+    model="iic/SenseVoiceSmall",
+    vad_model="fsmn-vad",
     vad_kwargs={"max_single_segment_time": 30000},
+    device="cpu",
 )
-
-# One call on a full 1-hour file - VAD segments and batches it internally.
-res = model.generate(
-    input="podcast_1hour.wav",
-    batch_size_s=300,   # dynamic batching of VAD segments (throughput)
+result = model.generate(
+    input=str(audio),
+    batch_size_s=300,
 )
-print(res[0]["text"])
-

实测:13 分钟音频 4.3 秒转完

-
long clip: 791s = 13.2 min
-transcribed 791s in 4.3s -> RTFx=186
-output chars: 2104   # full transcript, head to tail
-

我们把一段 13.2 分钟(791 秒)的录音丢进单次 generate(),4.3 秒就转写完毕(186x 实时),输出完整覆盖从开头到结尾。换成 1 小时的文件,做法完全一样——不用改一行代码。

-

为什么能处理任意时长

+if not result or not isinstance(result[0], dict): + raise RuntimeError("No result record; inspect audio and model logs") +text = result[0].get("text", "") +if not isinstance(text, str) or not text.strip(): + raise RuntimeError("Empty transcript; inspect audio, silence and VAD output") +print(text)
+

这里明确使用 CPU,按 VAD 片段逐个处理;batch_size_s 不是 CPU 并行参数,也不是速度推荐。空结果需要检查:可能是静音、VAD 未选中语音、输入问题或推理异常,不能一律当成成功或模型错误。示例故意停止并要求人工检查,不提供伪造的转写输出。

+

一次调用背后的资源路径

+
+

这条离线路径会读取完整音频波形到 CPU 内存,再使用 VAD 区域组织 ASR 输入;它不是只从磁盘读取当前片段的有界内存流式处理。分段可以改变某一批推理的工作量,但整段波形、解码缓冲、分段信息、输出和并发请求仍占用资源。

+

仅按 16 kHz、单声道、float32 波形计算,一小时数据约 230 MB;这只是数据量估算,不是进程 RAM 或 GPU 峰值内存实测,也不包含解码副本和模型。不能据此承诺任意文件都能处理,或一小时与一分钟文件显存相同。可接受的时长需要在所选硬件、模型、音频格式和并发条件下验证。

+

查看整段加载与分段排序、音频解码入口。这些源码能说明数据路径,不能替代峰值内存测量。

+
+

两个参数,两个不同的边界

+
+ + + +
参数含义不能保证什么
max_single_segment_time=30000FSMN-VAD 的端点阈值,单位毫秒(ms)。它参与帧级语音分段决策。不是整文件时长限制,不保证严格等长片段或恰好切在一个词、句子的边界。
batch_size_s=300VAD 包装器的批预算,单位秒(s),内部转为毫秒;组批判据涉及最长片段时长乘以片段数量。不是所有片段时长简单求和,也不是硬内存上限;较长的单段仍可能单独进入推理。
+

CPU 例外:当前包装器在 device="cpu" 时关闭上述组批预算,按片段逐个处理。因此调整示例中的 batch_size_s 不能当作 CPU 批吞吐优化已经生效。换用经过配置和验证的 GPU 环境时,应重新测量,而不是套用相同内存或速度结论。实现依据:批预算单位、CPU 分支与组批判据。

+
+

长文件上线前怎么验收

+
+
    +
  1. 先验证输入:检查文件可解码、实际时长、声道和采样率。不要把读取成功或非空文本视为完整处理证明。
  2. +
  3. 覆盖业务难点:准备短录音、长静音、多人重叠、连续长发言,以及末尾有明确语音的样本。重点复听切分处、关键实体和尾部;时间戳空隙也可能是静音。
  4. +
  5. 记录资源和时延:固定模型与依赖版本、硬件、设备、音频身份和并发;分开记录初始化与文件调用耗时,说明是否预热,测量 RAM 和 GPU 峰值,保留失败与空结果。
  6. +
  7. 再扩大负载:逐步增加录音长度和并发,确定服务的准入、超时、队列与重试策略。一次成功不能证明所有时长都可用;客户端超时也不等于后台推理被取消。
  8. +
+

更多思路见会议转写验收清单与可复现性能评测。前者附带的 MOSS sentence_info 检查器有特定输入契约,不可直接套在任意 SenseVoice 返回值上;结构检查也不证明文字准确率或语音覆盖完整。

+
+

模型和服务应分开选择

    -
  • VAD 自动切句:fsmn-vad 先把长音频按停顿切成一句句语音段,max_single_segment_time 控制单段最长时长。
  • -
  • 动态批处理:batch_size_s=300 把多段语音按总时长打包成批,一次性高吞吐解码。
  • -
  • 内存可控:因为是分段处理,显存占用与文件总长无关,1 小时和 1 分钟占用相近。
  • +
  • 本文是明确配置外部 FSMN-VAD 的 SenseVoice 离线示例。Paraformer 的配置与输出应另行核对;SenseVoice 富文本标签不是经过验证的情绪准确率。
  • +
  • 第三方 MOSS-Transcribe-Diarize 有自己的整录音转写与原生说话人分离路径,不需要照搬本文的外部 VAD 或 CAM++。匿名说话人标签不等于实名身份识别。
  • +
  • 需要实时麦克风转写时,按部署矩阵选择明确支持的流式模型与服务;缩小离线批次并不会把它变成流式协议。更多能力差异见模型选型指南。
-

进阶

- -

典型场景

-
    -
  • 播客 / 有声书:整集一次转写、生成文稿。
  • -
  • 会议 / 讲座 / 庭审:长录音批量转文字 + 存档检索。
  • -
  • 客服通话:整通录音转写做质检与挖掘。
  • -
-

FunASR 是通义实验室开源的工业级语音识别工具包,长音频、流式、多语种全覆盖。

在 GitHub 上 Star FunASR ★
+

本文依据固定版本源码解释行为,没有新增长录音性能或准确率实测。旧页缺乏完整复现条件的耗时和全程覆盖宣传已撤下;历史版本保留在仓库中,不能作为当前部署承诺。

相关文章

    @@ -118,4 +146,4 @@ output chars: 2104 # full transcript, head to tail
FunASR · 通义实验室开源语音识别 · GitHub
- \ No newline at end of file + diff --git a/web-pages/product-site/legacy/blog/index.html b/web-pages/product-site/legacy/blog/index.html index e3d3c70b0..e1f629934 100644 --- a/web-pages/product-site/legacy/blog/index.html +++ b/web-pages/product-site/legacy/blog/index.html @@ -51,5 +51,5 @@ footer a{color:#94a3b8}
2026-08-31 · FunClip v2.2.0

MOSS 多说话人转写进入 FunClip:从一句话到带说话人的片段

通过经真实双说话人音频验证的 vLLM 路径,把第三方 MOSS 模型的段级时间戳和说话人标签直接用于 SRT 与说话人片段导出。

查看部署与剪辑边界 →
-

FunASR 技术博客

2026-06-23

语音识别带时间戳(字级 timestamps)Python 实战:每个字精确到毫秒

FunASR Paraformer 原生字级时间戳:每个字带 [起始毫秒,结束毫秒],一次调用即有。可做逐字高亮、点击跳转、字幕对齐。附真实实测+配对代码。

2026-06-23

标点恢复 Python 实战:给无标点文本/ASR 结果自动加标点

FunASR ct-punc 开源标点恢复,中英双语,3 行 Python 补全 ,。?(英文还做句首大写),也能一行挂到 ASR 上。附真实实测。

2026-06-22

中文语音识别(普通话)Python 实战:用 FunASR 又快又准

FunASR 专为中文打造:默认旗舰 Fun-ASR-Nano(CER 8.06%),CPU 用 SenseVoice(7.81%)/ Paraformer(10.18%,时间戳/热词),都远好于 Whisper(~20%)。3 行 Python。

2026-06-22

自托管语音转文字:Google / AWS / Azure 云语音 API 的免费开源替代

开源免费(MIT)、本地推理、不按分钟计费、数据不出内网、中文强,OpenAI 兼容改 base_url 即可迁移。附实测代码 + FunASR vs 云 API 对比 + 成本分析。

2026-06-22

Python 语音活动检测(VAD):检测语音、去静音、按停顿切分音频

FunASR fsmn-vad 3 行代码返回每段语音的起止毫秒。实测 13s 录音 0.12s 切成 2 段、去除 18% 静音;可去静音、切分长录音、给 Whisper 等做预处理防幻觉。

2026-06-22

日语语音识别:SenseVoice 一个模型搞定日语转写+标点+情感

同一段日语音频实测:SenseVoice 写对同音字 転売、自动加标点,Whisper-small 误为 天売、无标点。原生 ja 支持 + 自动语种识别 + 情感事件,3 行 Python。

2026-06-23

从 Deepgram / AssemblyAI 迁移到自托管 FunASR:接入与验收清单

评估自托管转写迁移:映射请求和结果契约,检查鉴权、返回格式、模型质量与运维成本。提供 FunASR 本地 JSON 示例,不承诺厂商 API 或 SDK 直接替换。

2026-06-23

选哪个 FunASR 模型?Nano vs MLT-Nano vs SenseVoice vs Paraformer

模型选型表 + 场景代码:中英日及中文方言/口音选旗舰 Nano,31 语种选独立 MLT-Nano,CPU 再选 SenseVoice 或 Paraformer。

2026-06-22

轻量语音识别:CPU 上约 250MB 跑中文 ASR

单二进制 + 254MB q8 模型,无需 GPU/Python,CPU 0.16s,中文 CER 7.99%——比 whisper.cpp small 还小且准 3 倍。

2026-06-21

粤语语音识别:SenseVoice 原生支持粤语口语(Whisper 会转成普通话)

同一段粤语音频实测:SenseVoice 保留 呢/唔/嘅,Whisper 转成普通话书面语。原生 yue 支持 + 自动语种识别,3 行 Python。

2026-06-21

FunASR vs faster-whisper:中文与粤语实测对比

粤语被 faster-whisper 误判为普通话、日语同音字错;SenseVoice 原生支持粤语+语种识别,中文 CER 低约 2.7 倍。实测。

2026-06-20

FunASR 跑进 llama.cpp:中文 ASR 的 whisper.cpp 替代品(CPU/零 Python)

单个自包含二进制、内置 VAD、吃任意音频,下载即用转写中文;中文 CPU 上比 whisper.cpp 准约 2.7 倍。3 步实测。

2026-06-18

Python 语音转文字:用 FunASR 本地免费转写音频

几行 Python 把音频转成文本,带时间戳/说话人/批量;本地、免费、无 API key、中文强。

2026-06-18

用 FunASR 自动生成字幕:音频/视频一键出 SRT 和 VTT

一行命令出 SRT,Python 同时导出 VTT,带说话人和真实时间戳;本地、免费、中文强。

2026-06-18

自托管 OpenAI Whisper API 替代:FunASR 接入与安全边界

用 FunASR 部署本地文件转写:显式 loopback 监听、基本 JSON 调用、SDK 接入、格式限制与网关安全。区分部分接口兼容与完整云服务替换。

2026-06-18

用 FunASR 命令行转写音频:文本/JSON/SRT 字幕

一行命令出文字/字幕/JSON,--spk 带说话人;还能 funasr-server 起 OpenAI 兼容 API。

2026-06-17

用 FunASR 转写超长音频:1 小时一次搞定

Whisper 限 30 秒,FunASR 内置 VAD 一次吃下任意时长;实测 13 分钟 4.3 秒转完(186x)。

2026-06-17

用 FunASR 实现实时流式语音识别(边说边出字)

600ms 级低延迟流式 ASR:分块+cache 边说边出字,含 2-pass(流式+离线)最佳实践。

2026-06-17

超越转写:用 SenseVoice 识别语言、情感与声学事件

一次非自回归前向同时输出转写+语种+情感+音频事件,Whisper 做不到的四合一。

2026-06-17

用 FunASR 做说话人分离:谁在何时说了什么

一次 generate 调用同时输出转写+说话人标签+时间戳,替代 pyannote+Whisper,无需 HF 授权。

2026-06-16

FunASR vs Whisper 实测对比:谁更快更准

184 中文文件 H100 实测:SenseVoice 169.6x、CER 7.81%,完整速度+准确率数据。

2026-06-16

Fun-ASR-Nano 使用指南:800M 端到端语音识别大模型

主力旗舰,中英日 + 7 大中文方言/26 种口音,热词/流式/说话人分离;31 语种请用 MLT-Nano。

2026-06-16

SenseVoice 部署指南:五语种识别、情感与音频事件

3 行代码跑通多语言识别,含语种/情感/事件检测、VAD、GPU/CPU。

更多:快速上手 · 模型

+

FunASR 技术博客

2026-06-23

语音识别带时间戳(字级 timestamps)Python 实战:每个字精确到毫秒

FunASR Paraformer 原生字级时间戳:每个字带 [起始毫秒,结束毫秒],一次调用即有。可做逐字高亮、点击跳转、字幕对齐。附真实实测+配对代码。

2026-06-23

标点恢复 Python 实战:给无标点文本/ASR 结果自动加标点

FunASR ct-punc 开源标点恢复,中英双语,3 行 Python 补全 ,。?(英文还做句首大写),也能一行挂到 ASR 上。附真实实测。

2026-06-22

中文语音识别(普通话)Python 实战:用 FunASR 又快又准

FunASR 专为中文打造:默认旗舰 Fun-ASR-Nano(CER 8.06%),CPU 用 SenseVoice(7.81%)/ Paraformer(10.18%,时间戳/热词),都远好于 Whisper(~20%)。3 行 Python。

2026-06-22

自托管语音转文字:Google / AWS / Azure 云语音 API 的免费开源替代

开源免费(MIT)、本地推理、不按分钟计费、数据不出内网、中文强,OpenAI 兼容改 base_url 即可迁移。附实测代码 + FunASR vs 云 API 对比 + 成本分析。

2026-06-22

Python 语音活动检测(VAD):检测语音、去静音、按停顿切分音频

FunASR fsmn-vad 3 行代码返回每段语音的起止毫秒。实测 13s 录音 0.12s 切成 2 段、去除 18% 静音;可去静音、切分长录音、给 Whisper 等做预处理防幻觉。

2026-06-22

日语语音识别:SenseVoice 一个模型搞定日语转写+标点+情感

同一段日语音频实测:SenseVoice 写对同音字 転売、自动加标点,Whisper-small 误为 天売、无标点。原生 ja 支持 + 自动语种识别 + 情感事件,3 行 Python。

2026-06-23

从 Deepgram / AssemblyAI 迁移到自托管 FunASR:接入与验收清单

评估自托管转写迁移:映射请求和结果契约,检查鉴权、返回格式、模型质量与运维成本。提供 FunASR 本地 JSON 示例,不承诺厂商 API 或 SDK 直接替换。

2026-06-23

选哪个 FunASR 模型?Nano vs MLT-Nano vs SenseVoice vs Paraformer

模型选型表 + 场景代码:中英日及中文方言/口音选旗舰 Nano,31 语种选独立 MLT-Nano,CPU 再选 SenseVoice 或 Paraformer。

2026-06-22

轻量语音识别:CPU 上约 250MB 跑中文 ASR

单二进制 + 254MB q8 模型,无需 GPU/Python,CPU 0.16s,中文 CER 7.99%——比 whisper.cpp small 还小且准 3 倍。

2026-06-21

粤语语音识别:SenseVoice 原生支持粤语口语(Whisper 会转成普通话)

同一段粤语音频实测:SenseVoice 保留 呢/唔/嘅,Whisper 转成普通话书面语。原生 yue 支持 + 自动语种识别,3 行 Python。

2026-06-21

FunASR vs faster-whisper:中文与粤语实测对比

粤语被 faster-whisper 误判为普通话、日语同音字错;SenseVoice 原生支持粤语+语种识别,中文 CER 低约 2.7 倍。实测。

2026-06-20

FunASR 跑进 llama.cpp:中文 ASR 的 whisper.cpp 替代品(CPU/零 Python)

单个自包含二进制、内置 VAD、吃任意音频,下载即用转写中文;中文 CPU 上比 whisper.cpp 准约 2.7 倍。3 步实测。

2026-06-18

Python 语音转文字:用 FunASR 本地免费转写音频

几行 Python 把音频转成文本,带时间戳/说话人/批量;本地、免费、无 API key、中文强。

2026-06-18

用 FunASR 自动生成字幕:音频/视频一键出 SRT 和 VTT

一行命令出 SRT,Python 同时导出 VTT,带说话人和真实时间戳;本地、免费、中文强。

2026-06-18

自托管 OpenAI Whisper API 替代:FunASR 接入与安全边界

用 FunASR 部署本地文件转写:显式 loopback 监听、基本 JSON 调用、SDK 接入、格式限制与网关安全。区分部分接口兼容与完整云服务替换。

2026-06-18

用 FunASR 命令行转写音频:文本/JSON/SRT 字幕

一行命令出文字/字幕/JSON,--spk 带说话人;还能 funasr-server 起 OpenAI 兼容 API。

2026-06-17 · 更新于 2026-09-09

长录音转写:VAD 分段、批处理与结果验收

核对完整音频加载、VAD 参数和 CPU 批处理行为,再检查长静音、切分边界与末尾覆盖。

2026-06-17

用 FunASR 实现实时流式语音识别(边说边出字)

600ms 级低延迟流式 ASR:分块+cache 边说边出字,含 2-pass(流式+离线)最佳实践。

2026-06-17

超越转写:用 SenseVoice 识别语言、情感与声学事件

一次非自回归前向同时输出转写+语种+情感+音频事件,Whisper 做不到的四合一。

2026-06-17

用 FunASR 做说话人分离:谁在何时说了什么

一次 generate 调用同时输出转写+说话人标签+时间戳,替代 pyannote+Whisper,无需 HF 授权。

2026-06-16

FunASR vs Whisper 实测对比:谁更快更准

184 中文文件 H100 实测:SenseVoice 169.6x、CER 7.81%,完整速度+准确率数据。

2026-06-16

Fun-ASR-Nano 使用指南:800M 端到端语音识别大模型

主力旗舰,中英日 + 7 大中文方言/26 种口音,热词/流式/说话人分离;31 语种请用 MLT-Nano。

2026-06-16

SenseVoice 部署指南:五语种识别、情感与音频事件

3 行代码跑通多语言识别,含语种/情感/事件检测、VAD、GPU/CPU。

更多:快速上手 · 模型

diff --git a/web-pages/product-site/legacy/en/blog/funasr-transcribe-long-audio.html b/web-pages/product-site/legacy/en/blog/funasr-transcribe-long-audio.html index 5e133bc7b..2826b1153 100644 --- a/web-pages/product-site/legacy/en/blog/funasr-transcribe-long-audio.html +++ b/web-pages/product-site/legacy/en/blog/funasr-transcribe-long-audio.html @@ -1,12 +1,12 @@ -Transcribe Long Audio with FunASR: Hours in a Single Call | FunASR - +Long-File Transcription: VAD Segmentation, Batching and Acceptance | FunASR +
-

Transcribe Long Audio with FunASR: Hours in a Single Call

- -

Transcribing a 1-hour podcast, lecture or meeting with Whisper is painful: it processes only 30 seconds at a time, so you have to chunk the audio, transcribe each piece, and stitch the results back together - while handling words cut off at chunk boundaries.

-

FunASR makes this trivial. With built-in VAD (voice activity detection), a single generate() call ingests audio of any length - it segments, batch-decodes and reassembles the full text for you, with zero chunking code.

-

Install

-
pip install -U funasr modelscope
-

Full, runnable code

-
from funasr import AutoModel
+

Long-File Transcription: VAD Segmentation, Batching and Acceptance

+ +

Podcasts, courses and meeting recordings require more than passing a file to a model: resource budgets, pauses, overlapping speech and missing tail speech all matter. This guide uses the offline SenseVoice + FSMN-VAD path in FunASR 1.4.15 to explain what happens inside a file-level call and what to check before rollout.

+ +

Start with one explicit offline configuration

+
+

Use an already prepared, separate FunASR 1.4.15 environment with PyTorch, working audio-decoding dependencies and access to model weights. Follow the installation and environment guide for platform preparation, then check the Python SDK guide. The version check below inspects installed package metadata; it is not proof of a complete installation or acoustic inference.

+

Place a local file you are authorized to process at meeting.wav. Validate the environment with a short recording before moving to longer business recordings; initial loading may download models. The output contains transcript text, which should stay out of public logs. The recipe pins the toolkit version, not every dependency or model revision; record and pin those separately for deployment.

+
+
from importlib.metadata import version
+from pathlib import Path
+
+installed = version("funasr")
+if installed != "1.4.15":
+    raise RuntimeError(f"Expected FunASR 1.4.15, found {installed}")
+
+audio = Path("meeting.wav")
+if not audio.is_file():
+    raise FileNotFoundError(audio)
+
+from funasr import AutoModel
 
 model = AutoModel(
-    model="iic/SenseVoiceSmall",   # or "paraformer-zh"
-    vad_model="fsmn-vad",          # built-in voice activity detection
+    model="iic/SenseVoiceSmall",
+    vad_model="fsmn-vad",
     vad_kwargs={"max_single_segment_time": 30000},
+    device="cpu",
 )
-
-# One call on a full 1-hour file - VAD segments and batches it internally.
-res = model.generate(
-    input="podcast_1hour.wav",
-    batch_size_s=300,   # dynamic batching of VAD segments (throughput)
+result = model.generate(
+    input=str(audio),
+    batch_size_s=300,
 )
-print(res[0]["text"])
-

Tested: 13 minutes transcribed in 4.3s

-
long clip: 791s = 13.2 min
-transcribed 791s in 4.3s -> RTFx=186
-output chars: 2104   # full transcript, head to tail
-

We fed a 13.2-minute (791s) recording into a single generate() call - done in 4.3s (186x realtime), with output covering the full file head to tail. A 1-hour file works exactly the same way, no code changes.

-

Why it handles any length

+if not result or not isinstance(result[0], dict): + raise RuntimeError("No result record; inspect audio and model logs") +text = result[0].get("text", "") +if not isinstance(text, str) or not text.strip(): + raise RuntimeError("Empty transcript; inspect audio, silence and VAD output") +print(text)
+

This explicit CPU path handles VAD regions individually; batch_size_s is not a CPU parallelism control or a speed recommendation. Empty output needs inspection: silence, VAD selection, input problems or inference errors are possible causes. It is not automatically success or a model defect. The example deliberately stops for inspection and supplies no fabricated transcript output.

+

The resource path inside a file call

+
+

This offline path loads the complete audio waveform into CPU memory, then organizes ASR inputs using VAD regions. It is not a bounded-memory stream that reads only the current segment from disk. Segmentation can change the work in one inference batch, but the full waveform, decoding buffers, segment metadata, outputs and concurrent requests still consume resources.

+

For a 16 kHz, mono, float32 waveform alone, one hour is about 230 MB. This is a data-size estimate, not measured peak process RAM or GPU memory, and excludes decoder copies and model state. It cannot establish support for every file or equal GPU memory for one-hour and one-minute inputs. Validate acceptable durations on the chosen hardware, model, audio format and concurrency.

+

See whole-file loading and segment sorting and the audio-decoding entry point. Source inspection explains the data path; it does not replace peak-memory measurement.

+
+

Two parameters with different boundaries

+
+ + + +
ParameterMeaningWhat it does not guarantee
max_single_segment_time=30000An FSMN-VAD endpoint threshold in milliseconds (ms), used in frame-level speech segmentation decisions.It is not a file-duration limit or a guarantee of exactly equal segments or cuts at word and sentence boundaries.
batch_size_s=300A VAD-wrapper batch budget in seconds (s), converted to milliseconds internally. Grouping considers the longest segment duration multiplied by the number of segments.It is not simply the sum of segment durations or a hard memory cap; a longer segment may still be processed alone.
+

CPU exception: the current wrapper disables that grouping budget for device="cpu" and handles segments individually. Changing batch_size_s in this example is therefore not evidence of CPU batch-throughput optimization. A prepared and validated GPU setup needs new measurements, not inherited memory or speed conclusions. See budget units and the CPU branch and grouping rule.

+
+

Acceptance before admitting long recordings

+
+
    +
  1. Verify inputs: check decodability, actual duration, channels and sample rate. A successful read or nonempty transcript is not completeness evidence.
  2. +
  3. Cover business difficulties: include short recordings, long silences, overlapping speakers, sustained speech and a clear spoken tail. Listen around cuts, key entities and the end. A timestamp gap may also be silence.
  4. +
  5. Record resources and latency: identify model/dependency versions, hardware, device, audio and concurrency. Separate initialization from file-call timing, state warmup conditions, measure peak RAM and GPU memory, and retain errors and empty results.
  6. +
  7. Increase load gradually: raise duration and concurrency to establish admission, timeout, queue and retry policies. One successful request does not validate all durations; a client timeout does not prove that backend inference was cancelled.
  8. +
+

See the meeting-transcript acceptance checklist and reproducible performance guide. The former's MOSS sentence_info audit script has a specific input contract, not an adapter for arbitrary SenseVoice results. Structural checks do not prove text accuracy or complete speech coverage.

+
+

Choose models and services separately

    -
  • VAD auto-segmentation: fsmn-vad splits the long audio into speech segments at pauses; max_single_segment_time caps the longest segment.
  • -
  • Dynamic batching: batch_size_s=300 packs segments by total duration for high-throughput decoding.
  • -
  • Bounded memory: because it processes segments, GPU memory is independent of total file length - 1 hour uses about as much as 1 minute.
  • +
  • This is an offline SenseVoice example with explicitly configured external FSMN-VAD. Check Paraformer configuration and outputs separately. SenseVoice rich-text tags are not verified emotion accuracy.
  • +
  • Third-party MOSS-Transcribe-Diarize has its own whole-recording transcription and native diarization path. It does not require this external VAD or CAM++ recipe. Anonymous speaker labels are not verified real-world identities.
  • +
  • For realtime microphone transcription, select an explicitly supported streaming model and service using the deployment matrix. Smaller offline batches do not create a streaming protocol. See model selection for capability differences.
-

Going further

- -

Typical use cases

-
    -
  • Podcasts / audiobooks: transcribe a whole episode at once.
  • -
  • Meetings / lectures: batch long recordings to text + archive/search.
  • -
  • Call recordings: full-call transcription for QA and mining.
  • -
-

FunASR is Tongyi Lab's open-source, industrial-grade speech recognition toolkit - long audio, streaming and multilingual all covered.

Star FunASR on GitHub ★
+

This article explains pinned source behavior, not new long-recording performance or accuracy measurements. The previous page's timing and full-coverage promotion lacked complete reproduction conditions and has been removed. Repository history preserves that text; it is not a current deployment promise.

Related posts

    @@ -118,4 +146,4 @@ output chars: 2104 # full transcript, head to tail
FunASR · Open-source speech recognition by Tongyi Lab · GitHub
- \ No newline at end of file + diff --git a/web-pages/product-site/legacy/en/blog/index.html b/web-pages/product-site/legacy/en/blog/index.html index b0b4492df..1a49f79a7 100644 --- a/web-pages/product-site/legacy/en/blog/index.html +++ b/web-pages/product-site/legacy/en/blog/index.html @@ -51,5 +51,5 @@ footer a{color:#94a3b8}
August 31, 2026 · FunClip v2.2.0

MOSS Multi-Speaker Transcription in FunClip: From One Prompt to Speaker Clips

Use the vLLM path validated on real two-speaker audio to turn the third-party MOSS model's segment timestamps and speaker labels into SRT and speaker-specific clip exports.

Read the deployment and clipping boundaries →
-

FunASR Blog

2026-06-23

Speech-to-Text with Word/Character-Level Timestamps in Python

FunASR Paraformer gives native character timestamps: every char has [start_ms, end_ms] in one call. Build word highlighting, click-to-seek, subtitle alignment. Real output + pairing code.

2026-06-23

Punctuation Restoration in Python — Add Punctuation to Text / ASR Output

FunASR ct-punc: open-source bilingual (zh+en) punctuation restoration. 3 lines of Python to add ,。? (and capitalize English), or attach to ASR in one line. Real output.

2026-06-22

Chinese (Mandarin) Speech Recognition in Python — Fast & Accurate with FunASR

Purpose-built for Chinese: default flagship Fun-ASR-Nano (CER 8.06%), with SenseVoice (7.81%) / Paraformer (10.18%, timestamps/hotwords) for CPU — all far better than Whisper (~20%).

2026-06-22

Self-Hosted Speech-to-Text — Free Alternative to Google / AWS / Azure Cloud Speech APIs

Open-source (MIT), local inference, no per-minute billing, audio stays on your network, strong Chinese, OpenAI-compatible (migrate via base_url). Runnable code + FunASR vs cloud comparison + cost analysis.

2026-06-22

Voice Activity Detection in Python — Detect Speech, Remove Silence, Split Audio

FunASR fsmn-vad returns millisecond speech spans in 3 lines. Measured: a 13s clip split into 2 segments in 0.12s, 18% silence removed. Trim silence, split long audio, preprocess Whisper to cut hallucinations.

2026-06-22

Japanese Speech Recognition in Python — SenseVoice: Transcription + Punctuation + Emotion in One Pass

Real side-by-side on one Japanese clip: SenseVoice writes 転売 correctly and adds punctuation; Whisper-small gives 天売 with none. Native ja support + auto language ID + emotion/events, 3 lines of Python.

2026-06-23

Migrating from Deepgram / AssemblyAI to Self-Hosted FunASR: Integration and Acceptance

Plan a self-hosted transcription migration: map request and response contracts, authentication, formats, model quality and operating costs. Includes a local FunASR JSON recipe, not a drop-in vendor SDK replacement.

2026-06-23

Which FunASR Model? Nano vs MLT-Nano vs SenseVoice vs Paraformer

A decision table with code: flagship Nano for zh/en/ja plus Chinese dialects/accents, separate MLT-Nano for 31 languages, and CPU choices SenseVoice or Paraformer.

2026-06-22

Lightweight Speech Recognition: Chinese ASR in ~250MB on CPU

One binary + a 254MB q8 model, no GPU/Python, 0.16s on CPU, 7.99% CER — smaller than whisper.cpp small and ~3x more accurate.

2026-06-21

Cantonese Speech Recognition in Python — SenseVoice Keeps Real Cantonese (Whisper Doesn't)

Real side-by-side on one Cantonese clip: SenseVoice keeps 呢/唔/嘅, Whisper rewrites to Mandarin. Native yue support + auto language ID, 3 lines of Python.

2026-06-21

FunASR vs faster-whisper: Chinese & Cantonese Compared

faster-whisper mislabels Cantonese as Mandarin + Japanese homophone errors; SenseVoice handles Cantonese natively, ~2.7x lower CER on Chinese.

2026-06-20

FunASR on llama.cpp — a whisper.cpp Alternative for Chinese ASR (CPU, no Python)

One self-contained binary, built-in VAD, any audio — download and transcribe Chinese; ~2.7x more accurate than whisper.cpp on CPU.

2026-06-18

Speech to Text in Python with FunASR

Transcribe audio in a few lines of Python — timestamps, speakers, batching. Local, free, no API key.

2026-06-18

Auto-Generate Subtitles (SRT & VTT) from Audio or Video with FunASR

One command for SRT, Python for VTT too, with speaker labels and real timestamps. Local, free, strong on Chinese.

2026-06-18

Self-Hosted OpenAI Whisper API Alternative: FunASR Integration and Security

Integrate local FunASR file transcription with explicit loopback binding, JSON requests, SDK checks, response-format limits and gateway security. Partial API support is not complete cloud-service parity.

2026-06-18

Transcribe Audio from the Command Line with FunASR

One command -> text/SRT/JSON, --spk for speakers; plus funasr-server for an OpenAI-compatible API.

2026-06-17

Transcribe Long Audio with FunASR: Hours in One Call

Whisper caps at 30s; FunASR ingests any length via built-in VAD - 13 min in 4.3s (186x).

2026-06-17

Real-Time Streaming Speech-to-Text with FunASR

~600ms low-latency streaming ASR with chunks + cache, plus the 2-pass (streaming + offline) best practice.

2026-06-17

Beyond Transcription: Language, Emotion & Audio Events with SenseVoice

Transcription + language + emotion + audio events in one pass — the 4-in-1 Whisper cannot do.

2026-06-17

Speaker Diarization with FunASR: Who Spoke When

Transcription + speaker labels + timestamps in one generate() call. A pyannote+Whisper alternative, no HF gated access.

2026-06-16

FunASR vs Whisper: Real Chinese ASR Benchmark

Measured on 184 Chinese files (H100): SenseVoice 169.6x, 7.81% CER — full speed & accuracy data.

2026-06-16

Fun-ASR-Nano Guide: 800M End-to-End ASR LLM

Flagship for zh/en/ja plus 7 Chinese dialect groups and 26 accents; choose MLT-Nano for 31 languages.

2026-06-16

SenseVoice Deployment Guide: Five-Language ASR, Emotion and Audio Events

Multilingual ASR in 3 lines, with language/emotion/event tags, VAD, GPU/CPU.

More: Quickstart · Models

+

FunASR Blog

2026-06-23

Speech-to-Text with Word/Character-Level Timestamps in Python

FunASR Paraformer gives native character timestamps: every char has [start_ms, end_ms] in one call. Build word highlighting, click-to-seek, subtitle alignment. Real output + pairing code.

2026-06-23

Punctuation Restoration in Python — Add Punctuation to Text / ASR Output

FunASR ct-punc: open-source bilingual (zh+en) punctuation restoration. 3 lines of Python to add ,。? (and capitalize English), or attach to ASR in one line. Real output.

2026-06-22

Chinese (Mandarin) Speech Recognition in Python — Fast & Accurate with FunASR

Purpose-built for Chinese: default flagship Fun-ASR-Nano (CER 8.06%), with SenseVoice (7.81%) / Paraformer (10.18%, timestamps/hotwords) for CPU — all far better than Whisper (~20%).

2026-06-22

Self-Hosted Speech-to-Text — Free Alternative to Google / AWS / Azure Cloud Speech APIs

Open-source (MIT), local inference, no per-minute billing, audio stays on your network, strong Chinese, OpenAI-compatible (migrate via base_url). Runnable code + FunASR vs cloud comparison + cost analysis.

2026-06-22

Voice Activity Detection in Python — Detect Speech, Remove Silence, Split Audio

FunASR fsmn-vad returns millisecond speech spans in 3 lines. Measured: a 13s clip split into 2 segments in 0.12s, 18% silence removed. Trim silence, split long audio, preprocess Whisper to cut hallucinations.

2026-06-22

Japanese Speech Recognition in Python — SenseVoice: Transcription + Punctuation + Emotion in One Pass

Real side-by-side on one Japanese clip: SenseVoice writes 転売 correctly and adds punctuation; Whisper-small gives 天売 with none. Native ja support + auto language ID + emotion/events, 3 lines of Python.

2026-06-23

Migrating from Deepgram / AssemblyAI to Self-Hosted FunASR: Integration and Acceptance

Plan a self-hosted transcription migration: map request and response contracts, authentication, formats, model quality and operating costs. Includes a local FunASR JSON recipe, not a drop-in vendor SDK replacement.

2026-06-23

Which FunASR Model? Nano vs MLT-Nano vs SenseVoice vs Paraformer

A decision table with code: flagship Nano for zh/en/ja plus Chinese dialects/accents, separate MLT-Nano for 31 languages, and CPU choices SenseVoice or Paraformer.

2026-06-22

Lightweight Speech Recognition: Chinese ASR in ~250MB on CPU

One binary + a 254MB q8 model, no GPU/Python, 0.16s on CPU, 7.99% CER — smaller than whisper.cpp small and ~3x more accurate.

2026-06-21

Cantonese Speech Recognition in Python — SenseVoice Keeps Real Cantonese (Whisper Doesn't)

Real side-by-side on one Cantonese clip: SenseVoice keeps 呢/唔/嘅, Whisper rewrites to Mandarin. Native yue support + auto language ID, 3 lines of Python.

2026-06-21

FunASR vs faster-whisper: Chinese & Cantonese Compared

faster-whisper mislabels Cantonese as Mandarin + Japanese homophone errors; SenseVoice handles Cantonese natively, ~2.7x lower CER on Chinese.

2026-06-20

FunASR on llama.cpp — a whisper.cpp Alternative for Chinese ASR (CPU, no Python)

One self-contained binary, built-in VAD, any audio — download and transcribe Chinese; ~2.7x more accurate than whisper.cpp on CPU.

2026-06-18

Speech to Text in Python with FunASR

Transcribe audio in a few lines of Python — timestamps, speakers, batching. Local, free, no API key.

2026-06-18

Auto-Generate Subtitles (SRT & VTT) from Audio or Video with FunASR

One command for SRT, Python for VTT too, with speaker labels and real timestamps. Local, free, strong on Chinese.

2026-06-18

Self-Hosted OpenAI Whisper API Alternative: FunASR Integration and Security

Integrate local FunASR file transcription with explicit loopback binding, JSON requests, SDK checks, response-format limits and gateway security. Partial API support is not complete cloud-service parity.

2026-06-18

Transcribe Audio from the Command Line with FunASR

One command -> text/SRT/JSON, --spk for speakers; plus funasr-server for an OpenAI-compatible API.

2026-06-17 · Updated 2026-09-09

Long-File Transcription: VAD Segmentation, Batching and Acceptance

Check full-file loading, VAD parameters and CPU batching, then audit long silences, segment boundaries and tail coverage.

2026-06-17

Real-Time Streaming Speech-to-Text with FunASR

~600ms low-latency streaming ASR with chunks + cache, plus the 2-pass (streaming + offline) best practice.

2026-06-17

Beyond Transcription: Language, Emotion & Audio Events with SenseVoice

Transcription + language + emotion + audio events in one pass — the 4-in-1 Whisper cannot do.

2026-06-17

Speaker Diarization with FunASR: Who Spoke When

Transcription + speaker labels + timestamps in one generate() call. A pyannote+Whisper alternative, no HF gated access.

2026-06-16

FunASR vs Whisper: Real Chinese ASR Benchmark

Measured on 184 Chinese files (H100): SenseVoice 169.6x, 7.81% CER — full speed & accuracy data.

2026-06-16

Fun-ASR-Nano Guide: 800M End-to-End ASR LLM

Flagship for zh/en/ja plus 7 Chinese dialect groups and 26 accents; choose MLT-Nano for 31 languages.

2026-06-16

SenseVoice Deployment Guide: Five-Language ASR, Emotion and Audio Events

Multilingual ASR in 3 lines, with language/emotion/event tags, VAD, GPU/CPU.

More: Quickstart · Models

diff --git a/web-pages/product-site/tests/browser/long-audio-blog.spec.ts b/web-pages/product-site/tests/browser/long-audio-blog.spec.ts new file mode 100644 index 000000000..93ea1dafc --- /dev/null +++ b/web-pages/product-site/tests/browser/long-audio-blog.spec.ts @@ -0,0 +1,52 @@ +import { expect, test } from '@playwright/test'; + +for (const prefix of ['', 'en/']) { + for (const width of [390, 1440]) { + test(`long-audio guide ${prefix || 'zh'} at ${width}px`, async ({ page }, testInfo) => { + await page.setViewportSize({ width, height: 900 }); + const route = `/${prefix}blog/funasr-transcribe-long-audio.html`; + await page.goto(`/${prefix}blog/`); + await page.locator(`a.post-card[href="${route}"]`).click(); + await expect(page).toHaveURL(new RegExp(route.replaceAll('.', '\\.'))); + for (const name of ['window', 'prerequisites', 'recipe', 'resources', 'batching', 'acceptance', 'evidence']) { + await expect(page.locator(`[data-long-audio-contract="${name}"]`)).toBeVisible(); + } + const layout = await page.evaluate(() => { + const article = document.querySelector('article')!; + const box = (selector: string) => article.querySelector(selector)!.getBoundingClientRect(); + return { + overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, + headingTop: box('h1').top, + navBottom: document.querySelector('nav')!.getBoundingClientRect().bottom, + warningBottom: box('[data-long-audio-contract="window"]').bottom, + recipeTop: box('[data-long-audio-contract="recipe"]').top, + }; + }); + expect(layout.overflow).toBeLessThanOrEqual(1); + expect(layout.headingTop).toBeGreaterThanOrEqual(layout.navBottom); + expect(layout.warningBottom).toBeLessThanOrEqual(layout.recipeTop); + await page.screenshot({ path: testInfo.outputPath('article-top.png') }); + const table = page.locator('[data-long-audio-contract="batching"] table'); + await table.scrollIntoViewIfNeeded(); + const scroll = await table.evaluate((node) => { + const wrapper = node.closest('.table-wrap')!; + const rect = wrapper.getBoundingClientRect(); + wrapper.scrollLeft = wrapper.scrollWidth; + return { left: rect.left, right: rect.right, width: wrapper.clientWidth, + content: wrapper.scrollWidth, scrolled: wrapper.scrollLeft }; + }); + expect(scroll.left).toBeGreaterThanOrEqual(0); + expect(scroll.right).toBeLessThanOrEqual(width); + if (width === 390) { + expect(scroll.content).toBeGreaterThan(scroll.width); + expect(scroll.scrolled).toBeGreaterThan(0); + expect(scroll.scrolled + scroll.width).toBeGreaterThanOrEqual(scroll.content - 1); + } + await page.screenshot({ path: testInfo.outputPath('batching-table.png') }); + await page.locator(`article a[href="/${prefix}docs/python-api.html"]`).click(); + await expect(page.locator('[data-source-link]')).toHaveAttribute( + 'href', new RegExp(`/docs/python_api${prefix ? '' : '_zh'}\\.md$`), + ); + }); + } +} diff --git a/web-pages/product-site/tests/test_long_audio_blog_contracts.py b/web-pages/product-site/tests/test_long_audio_blog_contracts.py new file mode 100644 index 000000000..e17f47661 --- /dev/null +++ b/web-pages/product-site/tests/test_long_audio_blog_contracts.py @@ -0,0 +1,303 @@ +"""Long-file documentation contracts, without acoustic models or network access.""" + +import ast +import copy +import importlib.metadata +import json +import re +import sys +from pathlib import Path +from urllib.parse import unquote, urlsplit + +import pytest +from bs4 import BeautifulSoup + +SITE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SITE)) +from build import build + +SLUG = "funasr-transcribe-long-audio.html" +WHISPER_REVISION = "86098128c0b4f24f0e2aa2994de830614b474227" +FALSE_CLAIMS = ( + r"Whisper\s+(?:caps?\s+at|限)\s*30\s*(?:s\b|seconds?\b|秒)", + r"(?:you\s+(?:have|need)\s+to|a\s+1-hour\s+file\s+means)\s+(?:manually\s+)?chunk(?:ing)?", + r"你(?:得|必须|需要)自己把音频切片", + r"(?:ingests?\s+audio\s+of|handles?)\s+any\s+length", + r"(?:吃下|处理)\s*(?:任意|无限)时长", + r"(?:GPU\s+memory\s+is\s+)?independent\s+of\s+total\s+file\s+length", + r"(?:显存占用\s*)?与文件总长无关", + r"1\s*hour\s+uses\s+about\s+as\s+much\s+as\s+1\s*minute", + r"1\s*小时和\s*1\s*分钟占用相近", +) +NEGATION = r"\b(?:not|no|never|cannot|doesn't|isn't)\b|不能|并非|不是|不保证|不代表|不意味着|没有" +HISTORICAL_NUMBER = r"\b791\b|\b4\.3\s*(?:s\b|秒)|\b186\s*[x倍]|\b2104\b" +MULTIPLICATION = r"multiply|multiplied|multiplication|times|product|×|\*|乘" +CPU_PER_SEGMENT = r"per.segment|one.*segment|individual.*segment|segments?\s+individually|逐段|单段|每段|按片段逐个" + + +@pytest.fixture(scope="module", params=("source", "built")) +def site_tree(request, tmp_path_factory): + if request.param == "source": + return SITE / "legacy" + output = tmp_path_factory.mktemp("long-audio-contracts") + build(output) + return output + + +def page(root, prefix): + return BeautifulSoup((root / prefix / "blog" / SLUG).read_text(), "html.parser") + + +def visible_text(node): + assert node is not None, "Missing visible long-audio explanation" + for parent in (node, *node.parents): + assert not parent.has_attr("hidden") + assert parent.get("aria-hidden") != "true" + assert not re.search(r"display\s*:\s*none|visibility\s*:\s*hidden", parent.get("style", "")) + text = " ".join(node.get_text(" ", strip=True).split()) + assert text, "A marker without readable content is not a contract" + return text + + +def contract(soup, name): + return soup.select_one(f'article [data-long-audio-contract="{name}"]') + + +def require_concepts(text, *patterns): + for pattern in patterns: + assert re.search(pattern, text, re.I), (pattern, text) + + +def reject_positive_false_claims(text): + text = " ".join(text.split()) + for pattern in FALSE_CLAIMS: + for match in re.finditer(pattern, text, re.I): + # A negation from a previous sentence/clause must not excuse a new claim. + before = re.split(r"[.!?。!?;;]|\bbut\b|\bhowever\b|但是|但", text[:match.start()], flags=re.I)[-1] + if re.search(NEGATION, before[-65:], re.I): + continue + pytest.fail(f"Unqualified long-audio claim: {match.group()}") + + +def promotional_texts(soup): + yield soup.title.get_text() if soup.title else "" + yield visible_text(soup.select_one("article h1")) + for meta in soup.select('meta[name="description"], meta[name="keywords"], meta[property="og:title"], meta[property="og:description"]'): + yield meta.get("content", "") + for script in soup.select('script[type="application/ld+json"]'): + records = json.loads(script.get_text()) + for record in records if isinstance(records, list) else [records]: + for key in ("headline", "description"): + yield record.get(key, "") + + +def recipe_tree(soup): + blocks = [pre for pre in soup.select("article pre") + if re.search(r"from\s+funasr\s+import\s+AutoModel", pre.get_text())] + assert len(blocks) == 1, "Retain one complete, unambiguous Python SDK recipe" + return ast.parse(blocks[0].get_text()) + + +@pytest.mark.parametrize("prefix", ("", "en/")) +def test_claims_are_bounded_in_article_seo_and_actual_index_card(site_tree, prefix): + soup = page(site_tree, prefix) + texts = [*promotional_texts(soup), visible_text(soup.select_one("article"))] + index = BeautifulSoup((site_tree / prefix / "blog/index.html").read_text(), "html.parser") + cards = index.select(f'a.post-card[href="/{prefix}blog/{SLUG}"]') + assert len(cards) == 1 + texts.append(visible_text(cards[0])) + for text in texts: + reject_positive_false_claims(text) + + +@pytest.mark.parametrize("prefix", ("", "en/")) +def test_whisper_window_is_distinguished_from_file_and_hosted_limits(site_tree, prefix): + text = visible_text(contract(page(site_tree, prefix), "window")) + require_concepts(text, r"Whisper", r"transcribe", r"30[\s-]*(?:second|s\b|秒)", + r"internal|internally|内部", r"window|窗口", r"file|文件", NEGATION, + r"hosted|cloud|托管|云", r"upload|上传") + + +@pytest.mark.parametrize("prefix", ("", "en/")) +def test_resources_distinguish_full_decode_batches_and_streaming(site_tree, prefix): + text = visible_text(contract(page(site_tree, prefix), "resources")) + require_concepts(text, r"CPU|RAM|host|主机|内存", r"full|whole|entire|完整|整段|整个", + r"decode|waveform|解码|波形") + require_concepts(text, r"GPU|VRAM|显存", r"batch|批", r"model|模型", NEGATION, + r"stream|流式", r"offline|file|离线|文件", + r"duration|length|时长|长度", r"limit|bound|上限|边界|限制|有界") + + +@pytest.mark.parametrize("prefix", ("", "en/")) +def test_batch_and_endpoint_units_match_the_specific_cpu_recipe(site_tree, prefix): + text = visible_text(contract(page(site_tree, prefix), "batching")) + require_concepts(text, r"max_single_segment_time", r"30000|30,000", + r"millisecond|\bms\b|毫秒", r"threshold|端点|阈值", NEGATION, + r"sentence|phoneme|linguistic|句|音素|语言") + require_concepts(text, r"batch_size_s", r"\b300\b", r"second|秒", + r"longest|max|最长", r"count|number|数量|段数", MULTIPLICATION) + require_concepts(text, r"CPU", CPU_PER_SEGMENT, + r"throughput|performance|吞吐|性能", NEGATION) + + +@pytest.mark.parametrize("prefix", ("", "en/")) +def test_prepared_sdk_environment_is_not_an_install_or_inference_guarantee(site_tree, prefix): + text = visible_text(contract(page(site_tree, prefix), "prerequisites")) + require_concepts(text, r"1\.4\.15", r"prepared|ready|准备|配置", + r"separate|isolated|独立|隔离", r"torch", r"audio|音频", + r"model|weight|模型|权重", r"inference|transcription|推理|转写", NEGATION) + + +@pytest.mark.parametrize("prefix", ("", "en/")) +def test_immutable_sources_and_same_language_acceptance_links(site_tree, prefix): + soup = page(site_tree, prefix) + hrefs = [link["href"] for link in soup.select("article a[href]") if visible_text(link)] + for path in ("README.md", "whisper/transcribe.py"): + assert any(urlsplit(h).netloc == "github.com" and urlsplit(h).path == + f"/openai/whisper/blob/{WHISPER_REVISION}/{path}" for h in hrefs) + for path in ("funasr/auto/auto_model.py", "funasr/utils/load_utils.py"): + assert any(re.fullmatch(r"/modelscope/FunASR/blob/[0-9a-f]{40}/" + re.escape(path), + urlsplit(h).path) and urlsplit(h).netloc == "github.com" for h in hrefs) + for route in (f"/{prefix}docs/installation.html", f"/{prefix}docs/python-api.html", + f"/{prefix}blog/meeting-transcript-acceptance.html"): + links = [h for h in hrefs if not urlsplit(h).netloc and urlsplit(h).path == route] + assert links, route + if site_tree != SITE / "legacy": + target = BeautifulSoup((site_tree / route.lstrip("/")).read_text(), "html.parser") + for href in links: + fragment = unquote(urlsplit(href).fragment) + if fragment: + assert target.find(id=fragment), (route, fragment) + for rel in ("canonical",): + assert soup.find("link", rel=rel)["href"] == f"https://www.funasr.com/{prefix}blog/{SLUG}" + for lang, route_prefix in (("zh", ""), ("en", "en/")): + rendered_lang = "zh-CN" if lang == "zh" and site_tree != SITE / "legacy" else lang + alternates = soup.find_all("link", rel="alternate", hreflang=rendered_lang) + assert len(alternates) == 1, (rendered_lang, alternates) + assert alternates[0]["href"] == f"https://www.funasr.com/{route_prefix}blog/{SLUG}" + + +@pytest.mark.parametrize("prefix", ("", "en/")) +def test_historical_numbers_are_not_promoted_as_new_capacity_or_coverage(site_tree, prefix): + soup = page(site_tree, prefix) + index = BeautifulSoup((site_tree / prefix / "blog/index.html").read_text(), "html.parser") + card = index.select_one(f'a.post-card[href="/{prefix}blog/{SLUG}"]') + for text in [*promotional_texts(soup), visible_text(card)]: + assert not re.search(HISTORICAL_NUMBER, text, re.I), text + article = copy.deepcopy(soup.select_one("article")) + history = article.select_one('[data-long-audio-contract="evidence"]') + if history and re.search(HISTORICAL_NUMBER, visible_text(history), re.I): + text = visible_text(history) + require_concepts(text, r"historical|archiv|历史", r"unverified|not.*verif|未.*核|未.*验证", + r"coverage|tail|覆盖|尾", NEGATION) + history.decompose() + assert not re.search(HISTORICAL_NUMBER, article.get_text(" ", strip=True), re.I) + + +@pytest.mark.parametrize("prefix", ("", "en/")) +def test_recipe_has_explicit_model_vad_device_and_matching_units(site_tree, prefix): + tree = recipe_tree(page(site_tree, prefix)) + constructors = [node for node in ast.walk(tree) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) and node.func.id == "AutoModel"] + assert len(constructors) == 1 + kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in constructors[0].keywords} + assert kwargs["model"] in ("iic/SenseVoiceSmall", "SenseVoiceSmall") + assert kwargs["vad_model"] == "fsmn-vad" + assert kwargs.get("device") == "cpu", "The demonstrated device must not depend on host GPU availability" + assert kwargs["vad_kwargs"]["max_single_segment_time"] == 30000 + calls = [node for node in ast.walk(tree) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) and node.func.attr == "generate"] + assert len(calls) == 1 + args = {kw.arg: kw.value for kw in calls[0].keywords} + assert ast.literal_eval(args["batch_size_s"]) == 300 + assert "input" in args + + +def test_bilingual_python_recipe_has_identical_ast(site_tree): + assert ast.dump(recipe_tree(page(site_tree, ""))) == ast.dump(recipe_tree(page(site_tree, "en/"))) + + +@pytest.mark.parametrize("prefix", ("", "en/")) +@pytest.mark.parametrize("result", ([], [{"text": ""}], [{"text": "controlled transcript"}])) +def test_actual_recipe_handles_empty_results_without_loading_models(site_tree, prefix, result, capsys, tmp_path, monkeypatch): + tree = recipe_tree(page(site_tree, prefix)) + imports = [node for node in ast.walk(tree) if isinstance(node, (ast.Import, ast.ImportFrom))] + model_imports = [node for node in imports if isinstance(node, ast.ImportFrom) and node.module == "funasr"] + assert len(model_imports) == 1 + assert [(n.name, n.asname) for n in model_imports[0].names] == [("AutoModel", None)] + for node in imports: + if node is model_imports[0]: + continue + if isinstance(node, ast.ImportFrom): + assert node.module in ("pathlib", "importlib.metadata") + else: + assert all(n.name in ("pathlib", "importlib.metadata") for n in node.names) + # Keep real Path/file checks and control flow; isolate only model and metadata boundaries. + tree.body.remove(model_imports[0]) + monkeypatch.chdir(tmp_path) + (tmp_path / "meeting.wav").write_bytes(b"") + metadata_calls = [] + real_version = importlib.metadata.version + + def controlled_version(name): + metadata_calls.append(name) + return "1.4.15" if name == "funasr" else real_version(name) + + monkeypatch.setattr(importlib.metadata, "version", controlled_version) + calls = [] + + class ControlledModel: + def __init__(self, **kwargs): + calls.append(("init", kwargs)) + + def generate(self, **kwargs): + calls.append(("generate", kwargs)) + return copy.deepcopy(result) + + code = compile(tree, SLUG, "exec") + namespace = {"AutoModel": ControlledModel, "__name__": "__main__"} + if not result or not result[0]["text"]: + with pytest.raises((ValueError, RuntimeError, SystemExit)) as rejected: + exec(code, namespace) + assert str(rejected.value), "Empty results need an explicit, readable failure" + else: + exec(code, namespace) + assert "funasr" in metadata_calls + assert [name for name, _ in calls] == ["init", "generate"] + audio = calls[-1][1]["input"] + assert isinstance(audio, str) and not urlsplit(audio).scheme and Path(audio).is_file() + output = capsys.readouterr().out + if result and result[0]["text"]: + assert result[0]["text"] in output, "Do not make empty handling pass by discarding successful output" + + +@pytest.mark.parametrize("text", ( + "Whisper caps at 30s; manually split the file.", + "FunASR ingests audio of any length.", + "GPU memory is independent of total file length.", + "显存占用与文件总长无关。", + "No benchmark was run. FunASR handles any length.", +)) +def test_false_claim_detector_rejects_positive_promises(text): + with pytest.raises(pytest.fail.Exception): + reject_positive_false_claims(text) + + +@pytest.mark.parametrize("text", ( + "Whisper transcribe internally processes 30-second windows, not a 30-second file limit.", + "It is not true that FunASR handles any length.", + "Do not assume GPU memory is independent of total file length.", + "不能保证显存占用与文件总长无关。", + "Full-file CPU decoding remains; smaller ASR batches do not turn this into streaming.", +)) +def test_false_claim_detector_accepts_explicit_corrections(text): + reject_positive_false_claims(text) + + +@pytest.mark.parametrize("pattern,text", ( + (MULTIPLICATION, "longest duration multiplied by the number of segments"), + (CPU_PER_SEGMENT, "CPU handles segments individually"), + (CPU_PER_SEGMENT, "CPU 按片段逐个处理"), +)) +def test_equivalent_batching_grammar_is_accepted(pattern, text): + require_concepts(text, pattern)