ToothpasteLake commited on
Commit
a7d6ddd
·
verified ·
1 Parent(s): 8cea383

add gradio demo: upload audio -> transcript + RTF

Browse files
Files changed (3) hide show
  1. README.md +54 -19
  2. app.py +162 -0
  3. requirements.txt +5 -0
README.md CHANGED
@@ -1,27 +1,62 @@
1
  ---
2
- title: Offlisten
3
- emoji: 🧠
4
- colorFrom: yellow
5
- colorTo: indigo
6
- sdk: static
 
7
  pinned: false
8
  license: mit
9
- short_description: '# Offlisten 本地离线会议转写(MV3 扩展 + sherpa-onnx/SenseVoice 引擎)'
10
  ---
11
 
12
- # Nerfies
13
 
14
- This is the repository that contains source code for the [Nerfies website](https://nerfies.github.io).
15
 
16
- If you find Nerfies useful for your work please cite:
17
- ```
18
- @article{park2021nerfies
19
- author = {Park, Keunhong and Sinha, Utkarsh and Barron, Jonathan T. and Bouaziz, Sofien and Goldman, Dan B and Seitz, Steven M. and Martin-Brualla, Ricardo},
20
- title = {Nerfies: Deformable Neural Radiance Fields},
21
- journal = {ICCV},
22
- year = {2021},
23
- }
24
- ```
25
 
26
- # Website License
27
- <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Offlisten ASR
3
+ emoji: 🎙️
4
+ colorFrom: indigo
5
+ colorTo: gray
6
+ sdk: gradio
7
+ app_file: app.py
8
  pinned: false
9
  license: mit
10
+ short_description: Offline ASR demo for zh/en/ja/ko/yue, reports RTF
11
  ---
12
 
13
+ # Offlisten ASR — 在线演示
14
 
15
+ 把一段音频转成带标点的文字,并报出在 2 vCPU 上的 RTF。
16
 
17
+ **这不是产品。** 产品 Offlisten 是本地桌面端:WASAPI 抓系统声音、转写结果写本地文件、全程不联网。
18
+ 这个 Space 跑的是同一个 SenseVoice int8 模型,放在 Linux CPU 上,只用来让人先听到效果。
 
 
 
 
 
 
 
19
 
20
+ ## 会看到什么
21
+
22
+ | 项 | 值 |
23
+ |---|---|
24
+ | 模型 | SenseVoice int8,229 MiB,中 / 英 / 日 / 韩 / 粤 |
25
+ | 标点 | 模型自带,不需要单独标点模型 |
26
+ | 硬件 | 免费 CPU Basic,2 vCPU / 16 GB |
27
+ | 输出 | 转写文本 + 音频时长 + 耗时 + RTF |
28
+
29
+ ## 实测(本机 Windows,2 线程,不是 Space 硬件)
30
+
31
+ 用 `test_wavs/` 里五个语种各一条,走的就是本 Space 的 `transcribe()`:
32
+
33
+ | 音频 | 时长 | 耗时 | RTF | 输出 |
34
+ |---|---|---|---|---|
35
+ | zh.wav | 5.6 s | 0.45 s | 0.080 | 开饭时间早上9点至下午5点。 |
36
+ | en.wav | 7.2 s | 0.81 s | 0.114 | The tribal chieftain called for the boy and presented him with 50 pieces of code. |
37
+ | ja.wav | 7.2 s | 0.61 s | 0.085 | うちの中学は弁当制で持っていきない場合は50円の学校販売のパンを買う。 |
38
+ | ko.wav | 4.6 s | 0.39 s | 0.084 | 조금만 생각을 하면서 살면 훨씬 편할 거야. |
39
+ | yue.wav | 5.1 s | 0.45 s | 0.088 | 呢几个字都表达唔到我想讲嘅意思。 |
40
+
41
+ 两个如实标注:
42
+
43
+ - 官方示例对 `zh.wav` 输出「**开放**时间」,int8 量化 + 2 线程下这里是「**开饭**时间」。
44
+ 重采样到 44.1 kHz 再喂回去,结果相同——说明不是重采样管线的问题,是模型在这段短音频上的真实取舍。
45
+ - 上表是本机 CPU 的成绩;Space 的免费档是共享 2 vCPU,实测 RTF 可能更高,以页面里显示的数字为准。
46
+
47
+ ## 它不是产品的原因
48
+
49
+ - 这里**必须上传音频**,产品是**不上传**的;
50
+ - 这里只能处理整段文件,产品是实时抓取、边开边写;
51
+ - 免费档闲置会休眠,第一名访客要等容器唤醒并加载 229 MiB 模型。
52
+
53
+ ## 模型从哪来
54
+
55
+ 默认从 `OFFLISTEN_MODEL_REPO`(默认 `ToothpasteLake/offlisten-models`)用 `hf_hub_download` 拉取,
56
+ 只取 `model.int8.onnx` 与 `tokens.txt`。要换成本地目录,设 `OFFLISTEN_MODEL_DIR` 指向含这两个文件的目录。
57
+
58
+ 许可证与来源见主仓库 `docs/distribution.md`;模型的原始许可见 SenseVoice 上游(Apache-2.0)。
59
+
60
+ ## 主仓库
61
+
62
+ <https://github.com/peaceaddpaasion/Offlisten>
app.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Offlisten 在线演示:把一段音频转成带标点的文字,并报出 RTF。
2
+
3
+ 这不是产品。产品是本地运行的桌面端(WASAPI 采集、不联网);这里跑的是同一个
4
+ SenseVoice int8 模型在 Linux CPU 上的离线转写,用来证明模型与引擎的可用性。
5
+
6
+ 设计约束:
7
+ - 免费 CPU Basic 档是 2 vCPU / 16 GB / 50 GB 非持久磁盘,容器会休眠后重建,
8
+ 所以模型不写进镜像,改为运行时从 Hub 拉取并缓存在容器内。
9
+ - 只接受 16 kHz 单声道交给模型;其它采样率在本地重采样,不依赖 ffmpeg。
10
+ - 所有失败都给出能照做的下一步,不返回 500。
11
+ """
12
+
13
+ import os
14
+ import time
15
+
16
+ import gradio as gr
17
+ import numpy as np
18
+ import sherpa_onnx
19
+
20
+ MODEL_REPO = os.environ.get("OFFLISTEN_MODEL_REPO", "ToothpasteLake/offlisten-models")
21
+ MODEL_SUBDIR = "sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17"
22
+ TARGET_SR = 16000
23
+
24
+ _state = {"recognizer": None, "error": None, "lang": None}
25
+
26
+
27
+ def _find_model_files():
28
+ """返回 (model_path, tokens_path)。优先用镜像内已有的目录,其次从 Hub 拉。"""
29
+ local_dir = os.environ.get("OFFLISTEN_MODEL_DIR")
30
+ if local_dir:
31
+ m = os.path.join(local_dir, "model.int8.onnx")
32
+ t = os.path.join(local_dir, "tokens.txt")
33
+ if os.path.isfile(m) and os.path.isfile(t):
34
+ return m, t
35
+ raise RuntimeError("OFFLISTEN_MODEL_DIR 指向 %s,但里面没有 model.int8.onnx / tokens.txt" % local_dir)
36
+
37
+ from huggingface_hub import hf_hub_download
38
+
39
+ m = hf_hub_download(MODEL_REPO, "%s/model.int8.onnx" % MODEL_SUBDIR)
40
+ t = hf_hub_download(MODEL_REPO, "%s/tokens.txt" % MODEL_SUBDIR)
41
+ return m, t
42
+
43
+
44
+ def get_recognizer(language):
45
+ """按语言惰性建识别器;同语言复用,避免每次请求都加载 229 MB。"""
46
+ if _state["recognizer"] is not None and _state["lang"] == language:
47
+ return _state["recognizer"]
48
+ if _state["error"]:
49
+ raise RuntimeError(_state["error"])
50
+ try:
51
+ model, tokens = _find_model_files()
52
+ _state["recognizer"] = sherpa_onnx.OfflineRecognizer.from_sense_voice(
53
+ model=model,
54
+ tokens=tokens,
55
+ language=language,
56
+ use_itn=True,
57
+ num_threads=int(os.environ.get("OFFLISTEN_THREADS", "2")),
58
+ )
59
+ _state["lang"] = language
60
+ return _state["recognizer"]
61
+ except Exception as e:
62
+ _state["error"] = "%s: %s" % (type(e).__name__, e)
63
+ raise
64
+
65
+
66
+ def _load_audio(path):
67
+ """读成 float32 单声道 + 16 kHz。不依赖 ffmpeg。"""
68
+ import soundfile as sf
69
+
70
+ samples, sr = sf.read(path, dtype="float32", always_2d=True)
71
+ mono = samples.mean(axis=1)
72
+ if sr != TARGET_SR and len(mono) > 1:
73
+ n = int(round(len(mono) * TARGET_SR / float(sr)))
74
+ mono = np.interp(
75
+ np.linspace(0.0, len(mono) - 1, n), np.arange(len(mono)), mono
76
+ ).astype(np.float32)
77
+ return mono, TARGET_SR
78
+
79
+
80
+ def transcribe(audio_path, language):
81
+ if not audio_path:
82
+ return "", "先上传一段音频,或直接用麦克风录一句。"
83
+
84
+ try:
85
+ samples, sr = _load_audio(audio_path)
86
+ except Exception as e:
87
+ return "", (
88
+ "读不了这个文件(%s)。请换成 wav / flac / ogg / mp3;"
89
+ "如果是从会议软件导出的 m4a,先用任意工具转成 wav。" % e
90
+ )
91
+
92
+ duration = len(samples) / float(sr)
93
+ if duration < 0.2:
94
+ return "", "音频只有 %.2f 秒,太短了。" % duration
95
+
96
+ try:
97
+ recognizer = get_recognizer(language)
98
+ except Exception as e:
99
+ return "", (
100
+ "模型没加载起来:%s\n\n"
101
+ "常见原因:模型仓库还没推上去(`%s`),或该仓库是私有的而 Space 没有读权限。"
102
+ % (e, MODEL_REPO)
103
+ )
104
+
105
+ started = time.perf_counter()
106
+ stream = recognizer.create_stream()
107
+ stream.accept_waveform(sr, samples)
108
+ recognizer.decode_stream(stream)
109
+ elapsed = time.perf_counter() - started
110
+
111
+ text = (stream.result.text or "").strip()
112
+ if not text:
113
+ return "", "这段音频里没识别出语音。确认它不是纯音乐或空白录音。"
114
+
115
+ rtf = elapsed / duration if duration > 0 else float("nan")
116
+ note = "音频 %.1f 秒,转写耗时 %.2f 秒,**RTF %.3f**(CPU Basic,2 vCPU,%s 线程)" % (
117
+ duration,
118
+ elapsed,
119
+ rtf,
120
+ os.environ.get("OFFLISTEN_THREADS", "2"),
121
+ )
122
+ return text, note
123
+
124
+
125
+ INTRO = """\
126
+ ## 这是模型能力演示,不是产品
127
+
128
+ 产品 **Offlisten** 在本地跑:抓系统声音、写本地文件、全程不联网。
129
+ 这里把同一份 SenseVoice int8 模型放在服务器 CPU 上,只有一个目的——让你先听到效果,再决定要不要装。
130
+
131
+ - 模型:SenseVoice int8(中 / 英 / 日 / 韩 / 粤),229 MiB,自带标点与逆文本正则
132
+ - 语言选 `auto` 由模型自判;已知语种时指定会更稳
133
+ - 上传的音频只���于本次转写,不留存
134
+ """
135
+
136
+
137
+ def build():
138
+ with gr.Blocks(title="Offlisten ASR demo") as demo:
139
+ gr.Markdown(INTRO)
140
+ with gr.Row():
141
+ with gr.Column():
142
+ audio = gr.Audio(
143
+ sources=["upload", "microphone"],
144
+ type="filepath",
145
+ label="音频(wav / flac / ogg / mp3,任意采样率)",
146
+ )
147
+ language = gr.Dropdown(
148
+ ["auto", "zh", "en", "ja", "ko", "yue"],
149
+ value="auto",
150
+ label="语言",
151
+ )
152
+ run = gr.Button("转写", variant="primary")
153
+ with gr.Column():
154
+ text = gr.Textbox(label="转写结果", lines=10, show_copy_button=True)
155
+ note = gr.Markdown()
156
+ run.click(transcribe, inputs=[audio, language], outputs=[text, note])
157
+ audio.change(transcribe, inputs=[audio, language], outputs=[text, note])
158
+ return demo
159
+
160
+
161
+ if __name__ == "__main__":
162
+ build().launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=5.0
2
+ sherpa-onnx>=2.0.0,<3
3
+ soundfile>=0.12
4
+ huggingface_hub>=0.30
5
+ numpy>=1.24