From dfbda65aad36c438dc4c39f3d53ab39590b59c23 Mon Sep 17 00:00:00 2001 From: zhifu gao Date: Thu, 10 Sep 2026 03:43:33 +0000 Subject: [PATCH] docs: make subtitle export examples reproducible and bounded Signed-off-by: zhifu gao --- .../product-site/content/legacy-manifest.json | 4 +- web-pages/product-site/data/blog.json | 10 +- ...te-subtitles-srt-vtt-from-audio-video.html | 179 +++++------------- ...te-subtitles-srt-vtt-from-audio-video.html | 179 +++++------------- .../browser/subtitle-output-article.spec.ts | 43 +++++ .../tests/test_subtitle_output_article.py | 80 ++++++++ 6 files changed, 224 insertions(+), 271 deletions(-) create mode 100644 web-pages/product-site/tests/browser/subtitle-output-article.spec.ts create mode 100644 web-pages/product-site/tests/test_subtitle_output_article.py diff --git a/web-pages/product-site/content/legacy-manifest.json b/web-pages/product-site/content/legacy-manifest.json index f2724fdd5..56e398158 100644 --- a/web-pages/product-site/content/legacy-manifest.json +++ b/web-pages/product-site/content/legacy-manifest.json @@ -30,7 +30,7 @@ "blog/funasr-vs-whisper-benchmark.html": "b7b49adf24d20570abb09b733ce03d4a50a4a0e98e746b4a9320f453e01cce84", "blog/funclip-v2-1-0-video-clipping-release.html": "88f6c44e5332d1746c4db0fc97d755ef82f12e46d8f1c1c0ad9351152ff9dfc1", "blog/funclip-v2-2-0-moss-speaker-clipping.html": "db4ee351aefc1cb6997ed36b624cbc7e2527d81665635bd6169550d782a2e145", - "blog/generate-subtitles-srt-vtt-from-audio-video.html": "f1133235673d441cc654310581f59a319f182314ab7c0b0824af87da9f4a0591", + "blog/generate-subtitles-srt-vtt-from-audio-video.html": "0f38a121c9f8f1c9f9fb3de5276ba422cb899094b2077085814e577c636ffb48", "blog/index.html": "bd04951ed11b340a6786c7b38a25beace6554f7269274c2d25d4228ebaaf2a0e", "blog/japanese-speech-recognition.html": "399f5ce84e68ac00854bdf70b52b1cde6795efca7c800fd6492035a37a7c1b68", "blog/lightweight-speech-recognition-cpu.html": "d6270066222ed5baef0df108e6a4155f4a87bb5482169f23f218794d371f3281", @@ -72,7 +72,7 @@ "en/blog/funasr-vs-whisper-benchmark.html": "367d4a8a1cc09ac932c80cdad065127925c5e5f942a806ace683c16dc1132769", "en/blog/funclip-v2-1-0-video-clipping-release.html": "229baf59adf2c3290541d9b3c8a6243992406ba84b712714e9d14599205cb1d4", "en/blog/funclip-v2-2-0-moss-speaker-clipping.html": "115f7dd0963754ed75c6022bb8ceb8ddf83b0391b1ba215af32358b2568c240a", - "en/blog/generate-subtitles-srt-vtt-from-audio-video.html": "1530d4b9e94820a0b60d801e8d1c19b7aeb031b2f2b76c092657392e47706c6d", + "en/blog/generate-subtitles-srt-vtt-from-audio-video.html": "937777559f43f5722994cf32da8f13196eae3a04d1488c1948d66291ee904d32", "en/blog/index.html": "fe18072fb67da6b9cc274fa26b8dabc5854c426a9a38210bb2317219e8123900", "en/blog/japanese-speech-recognition.html": "c476adcc2be1ed7c19e2345cc91b8ee6a0e04efd794b9b6476dd5d9b3a2c8b04", "en/blog/lightweight-speech-recognition-cpu.html": "0e02c0e0853b12613d32c250f593c05c2052f18231207b63ee01d6e6600a86f7", diff --git a/web-pages/product-site/data/blog.json b/web-pages/product-site/data/blog.json index 4ee2299eb..11fa5777c 100644 --- a/web-pages/product-site/data/blog.json +++ b/web-pages/product-site/data/blog.json @@ -285,14 +285,14 @@ { "slug": "generate-subtitles-srt-vtt-from-audio-video", "category": "applications", - "reviewed": false, + "reviewed": true, "zh": { - "title": "从录音生成字幕文件", - "summary": "" + "title": "字幕文件生成了,然后呢?", + "summary": "从一段普通话录音得到 SRT,转换为 VTT,再回听检查时间与文字。" }, "en": { - "title": "From a recording to subtitle files", - "summary": "" + "title": "You have a subtitle file. Is it ready?", + "summary": "Create SRT from a Mandarin recording, convert it to VTT, then check timing and text against the audio." } }, { diff --git a/web-pages/product-site/legacy/blog/generate-subtitles-srt-vtt-from-audio-video.html b/web-pages/product-site/legacy/blog/generate-subtitles-srt-vtt-from-audio-video.html index 0334b8d90..582a32490 100644 --- a/web-pages/product-site/legacy/blog/generate-subtitles-srt-vtt-from-audio-video.html +++ b/web-pages/product-site/legacy/blog/generate-subtitles-srt-vtt-from-audio-video.html @@ -1,134 +1,49 @@ - -用 FunASR 自动生成字幕:从音频/视频一键导出 SRT 和 VTT | FunASR - - - - - - - + + +字幕文件生成了,然后呢? | FunASR + + + + + + +
-

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

- -

要给视频或音频配字幕,不一定要上传到云端或买 API。FunASR 可以在本地、免费地把语音转成带时间戳的 SRTVTT 字幕,还能标注说话人。中文尤其强,同时支持 SenseVoiceSmall 五语种及 Fun-ASR-MLT-Nano 31 语种。下面的命令和代码都已实测。

- -

第一步(视频):先抽出音频

-

如果源文件是视频,用 ffmpeg 抽出 16kHz 单声道音频(FunASR 的标准输入):

-
ffmpeg -i video.mp4 -ar 16000 -ac 1 audio.wav
-

纯音频文件(wav/mp3/m4a 等)可跳过这步。

- -

最快:一行命令出 SRT

-
pip install -U funasr
-funasr audio.wav -f srt -o ./subs
-

./subs/audio.srt 生成标准 SRT 字幕,时间戳真实可用:

-
1
-00:00:00,000 --> 00:00:05,546
-欢迎大家来体验达摩院推出的语音识别模型
-

--spk 可以按说话人分句(适合访谈、会议):

-
funasr meeting.wav --spk -f srt -o ./subs
- -

用 Python:同时导出 SRT 和 VTT(带说话人)

-

需要更多控制(比如同时要网页用的 VTT、自定义说话人前缀)时,用 Python 直接从识别结果里的 sentence_info 拼字幕:

-
from funasr import AutoModel
-from funasr.utils.postprocess_utils import rich_transcription_postprocess
-
-model = AutoModel(model="iic/SenseVoiceSmall", vad_model="fsmn-vad", spk_model="cam++", device="cuda")
-res = model.generate(input="audio.wav")
-segments = res[0]["sentence_info"]   # 每段含 start/end(毫秒)、spk、sentence
-
-def ts(ms, sep):                     # 毫秒 -> HH:MM:SS,mmm(SRT 用 "," / VTT 用 ".")
-    h, mm, ss, mmm = ms//3600000, (ms%3600000)//60000, (ms%60000)//1000, ms%1000
-    return "%02d:%02d:%02d%s%03d" % (h, mm, ss, sep, mmm)
-
-with open("subs.srt", "w", encoding="utf-8") as f:
-    for i, s in enumerate(segments):
-        text = rich_transcription_postprocess(s["sentence"])
-        f.write("%d\n%s --> %s\nSpeaker %s: %s\n\n" % (i+1, ts(s["start"], ","), ts(s["end"], ","), s["spk"], text))
-
-with open("subs.vtt", "w", encoding="utf-8") as f:
-    f.write("WEBVTT\n\n")
-    for s in segments:
-        text = rich_transcription_postprocess(s["sentence"])
-        f.write("%s --> %s\n<v Speaker %s>%s\n\n" % (ts(s["start"], "."), ts(s["end"], "."), s["spk"], text))
-

实测输出(SRT):

-
1
-00:00:00,610 --> 00:00:05,530
-Speaker 0: 欢迎大家来体验达摩院推出的语音识别模型
-

注意一定要用 rich_transcription_postprocess 清掉 SenseVoice 的 <|zh|> 等标签,否则标签会混进字幕。

- -

最后(可选):把字幕烧进视频

-
ffmpeg -i video.mp4 -vf subtitles=subs.srt output.mp4
- -

为什么用 FunASR 做字幕

-
    -
  • 本地、免费、隐私:无需上传、无 API key、无时长费用。
  • -
  • :SenseVoice 非自回归,远快于 Whisper(实测对比),长视频也能很快出字幕。
  • -
  • 中文更准 + SenseVoiceSmall 五语种及 Fun-ASR-MLT-Nano 31 语种;自带 VAD 分句、说话人标注、真实时间戳。
  • -
-

FunASR 是通义实验室开源的工业级语音识别工具包。

在 GitHub 上 Star FunASR ★
- -

相关文章

- -
-
FunASR · 通义实验室开源语音识别 · GitHub
- \ No newline at end of file +

字幕文件生成了,然后呢?

+

2026-09-10 修订 · 应用实践 · 阅读约 4 分钟

+

给一段访谈配字幕,难点不是把文字写进 SRT,而是让观众在正确的时间读到正确的话。先从一段普通话短录音开始:生成 SRT,转换成网页播放器使用的 VTT,再回听核对。文件能打开,只是第一步。

+

先做一份可以回看的 SRT

+

先按安装与环境验证指南准备 CPU 环境,确认 PyTorch 与 torchaudio 版本配套且可以导入,再安装 funasr==1.4.15 并运行 python -m pip check。FFmpeg 是需要单独安装的系统程序,先用 ffmpeg -version 确认它可用。第一次运行会下载模型;本地推理不等于首次安装完全离线。

+

把有权使用的短录音放到 audio.wav。若只有视频,可以先抽取音轨;多音轨视频要先确认选中的是目标语言的音轨。-n 避免覆盖已有音频:

+
ffmpeg -n -i video.mp4 -vn -ar 16000 -ac 1 audio.wav
+

下面显式选择普通话 Paraformer 路径,而不是 CLI 默认的 SenseVoice。它组合 Paraformer、FSMN-VAD 与标点模型;不是把每个模型的语言和时间戳能力合在一起。请使用新的输出目录,CLI 会覆盖其中已有的同名字幕文件。

+
funasr audio.wav --model paraformer --device cpu --output-format srt --output-dir ./subs
+

./subs/audio.srt 是输出文件,--output-dir 后面是目录。SRT 模式会请求时间戳,不需要额外添加 --timestamps。默认 readable 模式会重组字幕;需要保留模型句边界时使用 --subtitle-segment-mode sentence,但两种模式都需要回听。

+

同一份字幕,换成 VTT

+

当前 FunASR CLI 的格式选项是 text/json/srt/tsv,没有 vtt。让 FFmpeg 转换已有 SRT,无需再跑一次识别:

+
ffmpeg -n -i ./subs/audio.srt ./subs/audio.vtt
+

下面是本次用官方约 5.55 秒中文短音频运行得到的文件内容。环境为 Linux CPU、FunASR 1.4.15、PyTorch/torchaudio 2.10.0;这是短样本功能验证,不是字幕准确率、速度或长视频质量测试。

+
本次输入的中文短音频实际波形,横轴为秒,纵轴为振幅
同一段输入音频的真实波形,不是模型置信度,也不表示字幕已经准确对齐。
+

SRT

+
1
+00:00:00,880 --> 00:00:05,195
+欢迎大家来体验达摩院推出的语音识别模型。
+

转换后的 VTT

+
WEBVTT
+
+00:00.880 --> 00:05.195
+欢迎大家来体验达摩院推出的语音识别模型。
+

逗号变为小数点、文件多出 WEBVTT 头,是格式转换,不是新的识别。文字、时间偏差、说话人错误或漏掉的尾句都不会因此自动修好。

+

导出后,检查这三件事

+
    +
  1. 回听开头、停顿和最后一句。 字幕是否提前出现、拖到下一句,尾部有没有漏字?不要只看程序退出码。
  2. +
  3. 核对名字、数字和每条字幕的阅读负担。 自动分组不保证所有字幕都满足固定字符数或时长;缺少可对齐时间戳时,原段可能被保留。
  4. +
  5. 在目标播放器或编辑器里回看。 确認导入了正确字幕和对应版本的视频,必要时人工校时,再导出交付文件。
  6. +
+

有 SRT,不等于有精确对齐。 当结果没有句级信息时,CLI 可能用整段时间戳范围,甚至音频总时长生成单条字幕。标点对齐失败也可能退回 VAD 片段边界。这些都不是逐字、音素或人工核验的边界;可从本次核对的 CLI 实现查看导出与回退逻辑。

+

这条最小路径没有启用说话人分离。--spk 也不会让 CLI 的 SRT 自动出现人物姓名或 Speaker 前缀;需要按说话人剪辑时,走独立的FunClip / MOSS 工作流,匿名标签不等于真实身份。

+

只需要文字,可以从Fun-ASR-Nano 原生 Transformers 入口开始。但它的文本输出不自带本教程需要的字幕时间戳或说话人分离,不能直接替换成“同样生成精准字幕”。

+

下一步,用FunClip 的字幕与选段流程回看你自己的短素材,从第一段经过人工核对的片段开始。

+
FunASR · 应用实践
+ diff --git a/web-pages/product-site/legacy/en/blog/generate-subtitles-srt-vtt-from-audio-video.html b/web-pages/product-site/legacy/en/blog/generate-subtitles-srt-vtt-from-audio-video.html index 22e1212a3..c352aaff6 100644 --- a/web-pages/product-site/legacy/en/blog/generate-subtitles-srt-vtt-from-audio-video.html +++ b/web-pages/product-site/legacy/en/blog/generate-subtitles-srt-vtt-from-audio-video.html @@ -1,134 +1,49 @@ - -Auto-Generate Subtitles (SRT & VTT) from Audio or Video with FunASR | FunASR - - - - - - - + + +You have a subtitle file. Is it ready? | FunASR + + + + + + +
-

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

- -

You don't need a cloud service or a paid API to subtitle a video. FunASR turns speech into timestamped SRT or VTT subtitles locally and for free, and can even label speakers. It's especially strong on Chinese and supports five SenseVoiceSmall languages and 31 Fun-ASR-MLT-Nano languages. Every command and snippet below is tested.

- -

Step 1 (video): extract the audio

-

If your source is a video, pull out 16 kHz mono audio (FunASR's standard input) with ffmpeg:

-
ffmpeg -i video.mp4 -ar 16000 -ac 1 audio.wav
-

For audio files (wav/mp3/m4a, ...) skip this step.

- -

Fastest: one command to SRT

-
pip install -U funasr
-funasr audio.wav -f srt -o ./subs
-

Writes a standard SRT file at ./subs/audio.srt with real, usable timestamps:

-
1
-00:00:00,000 --> 00:00:05,546
-欢迎大家来体验达摩院推出的语音识别模型   (the Chinese sample)
-

Add --spk to split cues by speaker (great for interviews and meetings):

-
funasr meeting.wav --spk -f srt -o ./subs
- -

Python: emit SRT and VTT together (with speakers)

-

When you need more control — e.g. a web-friendly VTT as well, or a custom speaker prefix — build subtitles directly from sentence_info in the result:

-
from funasr import AutoModel
-from funasr.utils.postprocess_utils import rich_transcription_postprocess
-
-model = AutoModel(model="iic/SenseVoiceSmall", vad_model="fsmn-vad", spk_model="cam++", device="cuda")
-res = model.generate(input="audio.wav")
-segments = res[0]["sentence_info"]   # each has start/end (ms), spk, sentence
-
-def ts(ms, sep):                     # ms -> HH:MM:SS,mmm  (SRT uses "," / VTT uses ".")
-    h, mm, ss, mmm = ms//3600000, (ms%3600000)//60000, (ms%60000)//1000, ms%1000
-    return "%02d:%02d:%02d%s%03d" % (h, mm, ss, sep, mmm)
-
-with open("subs.srt", "w", encoding="utf-8") as f:
-    for i, s in enumerate(segments):
-        text = rich_transcription_postprocess(s["sentence"])
-        f.write("%d\n%s --> %s\nSpeaker %s: %s\n\n" % (i+1, ts(s["start"], ","), ts(s["end"], ","), s["spk"], text))
-
-with open("subs.vtt", "w", encoding="utf-8") as f:
-    f.write("WEBVTT\n\n")
-    for s in segments:
-        text = rich_transcription_postprocess(s["sentence"])
-        f.write("%s --> %s\n<v Speaker %s>%s\n\n" % (ts(s["start"], "."), ts(s["end"], "."), s["spk"], text))
-

Tested output (SRT):

-
1
-00:00:00,610 --> 00:00:05,530
-Speaker 0: 欢迎大家来体验达摩院推出的语音识别模型
-

Always run rich_transcription_postprocess to strip SenseVoice tags like <|zh|>, otherwise they leak into your subtitles.

- -

Last (optional): burn subtitles into the video

-
ffmpeg -i video.mp4 -vf subtitles=subs.srt output.mp4
- -

Why use FunASR for subtitles

-
    -
  • Local, free, private — no uploads, no API key, no per-minute fees.
  • -
  • Fast: SenseVoice is non-autoregressive, far faster than Whisper (benchmark) — long videos subtitle quickly.
  • -
  • Stronger on Chinese + five SenseVoiceSmall languages and 31 Fun-ASR-MLT-Nano languages; built-in VAD segmentation, speaker labels, and real timestamps.
  • -
-

FunASR is Tongyi Lab's open-source, industrial-grade speech recognition toolkit.

Star FunASR on GitHub ★
- -

Related posts

- -
-
FunASR · Open-source speech recognition by Tongyi Lab · GitHub
- \ No newline at end of file +

You have a subtitle file. Is it ready?

+

Revised 2026-09-10 · Application guide · 4 min read

+

For an interview, the hard part is not writing text into an SRT file. It is showing the right words at the right time. Start with a short Mandarin recording: create SRT, convert it to WebVTT, then listen back. A file that opens is only the first step.

+

Make one SRT you can review

+

Follow the installation and environment checks for a CPU environment with matching, importable PyTorch and torchaudio. Then install funasr==1.4.15 and run python -m pip check. Install FFmpeg separately and check ffmpeg -version. The first run downloads models: local inference does not mean the first installation is fully offline.

+

Place a short recording you have permission to use at audio.wav. For video, extract the audio first. For multiple audio tracks, check that the selected track has the intended language. -n prevents overwriting an existing audio file:

+
ffmpeg -n -i video.mp4 -vn -ar 16000 -ac 1 audio.wav
+

Select the Mandarin Paraformer path explicitly. The CLI otherwise defaults to SenseVoice. This path combines Paraformer, FSMN-VAD and punctuation; it does not combine the language and timing capabilities of every model. Use a fresh output directory: the CLI overwrites subtitle files with the same name.

+
funasr audio.wav --model paraformer --device cpu --output-format srt --output-dir ./subs
+

The output is ./subs/audio.srt; --output-dir takes a directory. SRT mode requests timestamps without an extra --timestamps flag. The default readable mode regroups cues. Use --subtitle-segment-mode sentence to retain model sentence boundaries. Both modes still need review.

+

Convert the same subtitles to VTT

+

The current FunASR CLI accepts text/json/srt/tsv, not vtt. Convert the existing SRT with FFmpeg instead of running recognition again:

+
ffmpeg -n -i ./subs/audio.srt ./subs/audio.vtt
+

These files came from this run of the official short Chinese audio sample, about 5.55 seconds long, on Linux CPU with FunASR 1.4.15 and PyTorch/torchaudio 2.10.0. This is a short-sample functional check, not a subtitle-accuracy, speed or long-video benchmark.

+
Actual waveform of the Chinese input sample, with time in seconds and signal amplitude
The same input recording's actual waveform, not model confidence or proof of correct subtitle alignment.
+

SRT

+
1
+00:00:00,880 --> 00:00:05,195
+欢迎大家来体验达摩院推出的语音识别模型。
+

Converted VTT

+
WEBVTT
+
+00:00.880 --> 00:05.195
+欢迎大家来体验达摩院推出的语音识别模型。
+

The decimal separator changes and a WEBVTT header appears. That is a format conversion, not new recognition. It does not fix text, timing offsets, speaker errors or a missing final sentence.

+

Check three things before delivery

+
    +
  1. Listen to the beginning, pauses and final sentence. Do cues appear too early, linger into the next sentence or miss the end? An exit code cannot answer that.
  2. +
  3. Review names, numbers and reading load. Automatic grouping does not guarantee a fixed character or duration limit for every cue. Without alignable timestamps, an original segment may remain intact.
  4. +
  5. Play the result in the target player or editor. Check that the subtitle file matches this version of the video. Adjust timing where needed before exporting the deliverable.
  6. +
+

SRT output is not proof of precise alignment. Without sentence information, the CLI may emit one cue over the overall timestamp range or even the full audio duration. Failed punctuation alignment may fall back to VAD segment boundaries. These are not word, phoneme or human-verified boundaries. See the CLI implementation checked for this guide for export and fallback behavior.

+

This minimal path does not enable diarization. Adding --spk does not automatically put names or Speaker prefixes into CLI SRT output. For speaker-based clipping, use the separate FunClip / MOSS workflow; anonymous labels are not real identities.

+

For text-only transcription, start with the native Fun-ASR-Nano Transformers guide. Its text output does not provide the subtitle timestamps or diarization needed here. Do not substitute it and promise the same aligned subtitles.

+

Next, review your own short material with the FunClip subtitle and selection workflow, starting with one manually checked clip.

+ + diff --git a/web-pages/product-site/tests/browser/subtitle-output-article.spec.ts b/web-pages/product-site/tests/browser/subtitle-output-article.spec.ts new file mode 100644 index 000000000..5c22feb19 --- /dev/null +++ b/web-pages/product-site/tests/browser/subtitle-output-article.spec.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; + +for (const prefix of ['', 'en/']) { + for (const width of [320, 390, 1440]) { + test(`Subtitle output ${prefix || 'zh'} at ${width}px`, async ({ page }, testInfo) => { + const errors: string[] = []; + page.on('pageerror', error => errors.push(String(error))); + await page.setViewportSize({ width, height: 1000 }); + await page.goto(`/${prefix}blog/applications/`); + await page.locator(`a[data-blog-story][href="/${prefix}blog/generate-subtitles-srt-vtt-from-audio-video.html"]`).click(); + const article = page.locator('article'); + await expect(article.locator('h1')).toBeVisible(); + await expect(article.locator('[data-editorial="opening"]')).toBeVisible(); + await page.screenshot({ path: testInfo.outputPath('opening.png') }); + await article.locator('figure').scrollIntoViewIfNeeded(); + expect(await article.locator('figure img').evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth >= 1400)).toBeTruthy(); + for (const name of ['transcribe', 'convert', 'srt-output', 'vtt-output']) { + const example = article.locator(`pre[data-example="${name}"]`); + await example.scrollIntoViewIfNeeded(); + expect(await example.evaluate(node => node.scrollWidth - node.clientWidth)).toBeLessThanOrEqual(1); + expect(await example.evaluate(node => { + const text = [...node.childNodes].find(child => child.nodeType === Node.TEXT_NODE && child.textContent?.trim()); + const button = node.querySelector('button'); + if (!text || !button) return false; + const range = document.createRange(); + const start = text.textContent!.search(/\S/); + range.setStart(text, start); + range.setEnd(text, start + 1); + return range.getBoundingClientRect().top >= button.getBoundingClientRect().bottom + 2; + }), `${name} text must not overlap its copy button`).toBeTruthy(); + } + await expect(article.locator('[data-example="transcribe"]')).toContainText('--model paraformer'); + await expect(article.locator('[data-example="vtt-output"]')).toContainText('WEBVTT'); + await page.screenshot({ path: testInfo.outputPath('output.png') }); + await expect(article.locator('[data-editorial="native-boundary"] a')).toHaveAttribute('href', `/${prefix}docs/native-transformers.html`); + expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1); + const peer = prefix ? '' : 'en/'; + await page.locator(`.header-actions a[href="/${peer}blog/generate-subtitles-srt-vtt-from-audio-video.html"]`).click(); + await expect(page).toHaveURL(new RegExp(`/${peer}blog/generate-subtitles-srt-vtt-from-audio-video\\.html$`)); + expect(errors).toEqual([]); + }); + } +} diff --git a/web-pages/product-site/tests/test_subtitle_output_article.py b/web-pages/product-site/tests/test_subtitle_output_article.py new file mode 100644 index 000000000..7cb36dde7 --- /dev/null +++ b/web-pages/product-site/tests/test_subtitle_output_article.py @@ -0,0 +1,80 @@ +"""Subtitle readers get an explicit model and a format conversion, not invented timing.""" + +import json +from pathlib import Path +import shlex +import sys + +from bs4 import BeautifulSoup +import pytest + +SITE = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SITE)) +from build import build + +SLUG = "generate-subtitles-srt-vtt-from-audio-video" + + +@pytest.fixture(scope="module", params=["source", "built"]) +def root(request, tmp_path_factory): + if request.param == "source": + return SITE / "legacy" + output = tmp_path_factory.mktemp("subtitle-story") + build(output) + return output + + +@pytest.mark.parametrize("prefix", ["", "en/"]) +def test_subtitle_story_has_a_bounded_reader_path(root, prefix): + soup = BeautifulSoup((root / prefix / "blog" / f"{SLUG}.html").read_text(), "html.parser") + article = soup.select_one("article") + assert article.select_one('[data-editorial="opening"]') + assert article.select_one('[data-editorial="timing-boundary"]') + assert article.select_one('[data-editorial="native-boundary"] a[href$="native-transformers.html"]') + assert article.select_one('a[data-installation-guide]') + assert "PyTorch" in article.select_one('[data-editorial="environment"]').get_text() + assert "torchaudio" in article.select_one('[data-editorial="environment"]').get_text() + assert len(article.select("h1")) == 1 + assert len(article.select("h2")) <= 4 + assert len(article.select('[data-editorial="next-step"] a')) == 1 + assert not any(claim in article.get_text() for claim in [ + "下面的命令和代码都已实测", "远快于 Whisper", "中文更准", "real, usable timestamps", + ]) + assert not article.select("pre code.language-python") + metadata = json.loads(soup.select_one('script[type="application/ld+json"]').get_text()) + assert metadata["datePublished"] == "2026-06-18" + assert metadata["dateModified"] == "2026-09-10" + assert metadata["headline"] == article.h1.get_text() + assert soup.select_one('link[rel="canonical"]')["href"] == f"https://www.funasr.com/{prefix}blog/{SLUG}.html" + image = article.select_one("figure img") + assert image and image.get("alt") and (root / image["src"].lstrip("/")).is_file() + + +@pytest.mark.parametrize("prefix", ["", "en/"]) +def test_examples_select_paraformer_and_convert_vtt_with_ffmpeg(prefix): + soup = BeautifulSoup((SITE / "legacy" / prefix / "blog" / f"{SLUG}.html").read_text(), "html.parser") + command = soup.select_one('pre[data-example="transcribe"]') + assert command is not None + assert shlex.split(command.get_text()) == [ + "funasr", "audio.wav", "--model", "paraformer", "--device", "cpu", + "--output-format", "srt", "--output-dir", "./subs", + ] + convert = soup.select_one('pre[data-example="convert"]') + assert convert is not None + assert shlex.split(convert.get_text()) == [ + "ffmpeg", "-n", "-i", "./subs/audio.srt", "./subs/audio.vtt", + ] + assert soup.select_one('pre[data-example="srt-output"]') + assert soup.select_one('pre[data-example="vtt-output"]') + assert "--spk" not in command.get_text() + + +def test_subtitle_story_does_not_expand_homepage_selection(): + data = json.loads((SITE / "data/blog.json").read_text()) + row = next(entry for entry in data["articles"] if entry["slug"] == SLUG) + assert row["reviewed"] and row["category"] == "applications" + assert data["lead"] == "funclip-v2-2-0-moss-speaker-clipping" + assert data["selected"] == [ + "meeting-transcript-acceptance", "fun-asr-nano-transformers", + "self-hosted-openai-whisper-api-alternative", "funasr-transcribe-long-audio", + ]