Skip to content

Commit b8351fd

Browse files
committed
feat: add agent-friendly CLI (funasr audio.wav --output-format json)
- New funasr CLI with argparse: positional audio, --model, --language, --output-format (text/json/srt/tsv), --spk, --timestamps - Old Hydra CLI preserved as funasr-hydra - Updated README.md and README_zh.md with CLI examples
1 parent 1065d2e commit b8351fd

4 files changed

Lines changed: 209 additions & 1 deletion

File tree

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,31 @@ model = AutoModel(model="emotion2vec_plus_large", device="cuda")
197197
result = model.generate(input="audio.wav", granularity="utterance")
198198
```
199199

200+
201+
### CLI (Agent-Friendly)
202+
203+
```bash
204+
# Transcribe audio (simplest)
205+
funasr audio.wav
206+
207+
# JSON output (for AI agents)
208+
funasr audio.wav --output-format json
209+
210+
# SRT subtitles
211+
funasr audio.wav --output-format srt --output-dir ./subs
212+
213+
# Speaker diarization + timestamps
214+
funasr audio.wav --spk --timestamps -f json
215+
216+
# Choose model and language
217+
funasr audio.wav --model paraformer --language zh
218+
219+
# Batch transcribe
220+
funasr *.wav --output-format srt --output-dir ./output
221+
```
222+
223+
Available models: `sensevoice` (default), `paraformer`, `paraformer-en`, `fun-asr-nano`
224+
200225
---
201226

202227
## Deploy

README_zh.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,31 @@ model = AutoModel(model="emotion2vec_plus_large", device="cuda")
195195
result = model.generate(input="audio.wav", granularity="utterance")
196196
```
197197

198+
### 命令行工具(Agent 友好)
199+
200+
```bash
201+
# 转写音频(最简用法)
202+
funasr audio.wav
203+
204+
# JSON 输出(适合 AI Agent 调用)
205+
funasr audio.wav --output-format json
206+
207+
# 生成 SRT 字幕
208+
funasr audio.wav --output-format srt --output-dir ./subs
209+
210+
# 说话人分离 + 时间戳
211+
funasr audio.wav --spk --timestamps -f json
212+
213+
# 指定模型和语言
214+
funasr audio.wav --model paraformer --language zh
215+
216+
# 批量转写
217+
funasr *.wav --output-format srt --output-dir ./output
218+
```
219+
220+
可用模型:`sensevoice`(默认)、`paraformer``paraformer-en``fun-asr-nano`
221+
222+
198223
---
199224

200225
## 部署

funasr/cli.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""FunASR CLI - Agent-friendly speech recognition from the command line."""
2+
3+
import argparse
4+
import json
5+
import os
6+
import re
7+
import sys
8+
import time
9+
10+
MODEL_CONFIGS = {
11+
"sensevoice": {"model": "iic/SenseVoiceSmall", "vad_model": "fsmn-vad", "vad_kwargs": {"max_single_segment_time": 30000}},
12+
"paraformer": {"model": "paraformer-zh", "vad_model": "fsmn-vad", "punc_model": "ct-punc"},
13+
"paraformer-en": {"model": "paraformer-en", "vad_model": "fsmn-vad"},
14+
"fun-asr-nano": {"model": "FunAudioLLM/Fun-ASR-Nano"},
15+
}
16+
17+
18+
def clean_text(text):
19+
return re.sub(r"<\|[^|]*\|>", "", text).strip()
20+
21+
22+
def _srt_time(ms):
23+
s = ms / 1000.0
24+
h, m, sec = int(s // 3600), int((s % 3600) // 60), int(s % 60)
25+
return f"{h:02d}:{m:02d}:{sec:02d},{int((s % 1) * 1000):03d}"
26+
27+
28+
def format_srt(segments):
29+
lines = []
30+
for i, seg in enumerate(segments, 1):
31+
lines += [str(i), f"{_srt_time(seg.get('start',0))} --> {_srt_time(seg.get('end',0))}", seg.get('text',''), ""]
32+
return "\n".join(lines)
33+
34+
35+
def format_tsv(segments):
36+
lines = ["start\tend\ttext"]
37+
for seg in segments:
38+
lines.append(f"{seg.get('start',0)/1000:.3f}\t{seg.get('end',0)/1000:.3f}\t{seg.get('text','')}")
39+
return "\n".join(lines)
40+
41+
42+
def _format_output(text, segments, timestamps, fmt, audio_path, model_name, language, elapsed):
43+
if fmt == "text":
44+
return text
45+
elif fmt == "json":
46+
obj = {"text": text}
47+
if segments:
48+
obj["segments"] = segments
49+
if timestamps:
50+
obj["timestamps"] = timestamps
51+
obj.update({"file": os.path.basename(audio_path), "model": model_name, "language": language or "auto", "duration_s": round(elapsed, 3)})
52+
return json.dumps(obj, ensure_ascii=False, indent=2)
53+
elif fmt == "srt":
54+
return format_srt(segments) if segments else f"1\n00:00:00,000 --> 99:59:59,999\n{text}\n"
55+
elif fmt == "tsv":
56+
return format_tsv(segments) if segments else f"start\tend\ttext\n0.000\t0.000\t{text}"
57+
58+
59+
def _get_version():
60+
try:
61+
from funasr import __version__
62+
return __version__
63+
except Exception:
64+
return "unknown"
65+
66+
67+
def main():
68+
p = argparse.ArgumentParser(
69+
prog="funasr",
70+
description="FunASR - speech recognition CLI. 50+ languages, speaker diarization.",
71+
epilog="Examples:\n"
72+
" funasr audio.wav\n"
73+
" funasr audio.wav --model sensevoice -f json\n"
74+
" funasr audio.wav -f srt -o ./subs\n"
75+
" funasr audio.wav --spk --timestamps\n",
76+
formatter_class=argparse.RawDescriptionHelpFormatter,
77+
)
78+
p.add_argument("audio", nargs="+", help="Audio file(s) to transcribe")
79+
p.add_argument("--model", "-m", default="sensevoice", choices=list(MODEL_CONFIGS), help="Model (default: sensevoice)")
80+
p.add_argument("--language", "-l", default=None, help="Language: zh, en, ja, ko, yue, auto")
81+
p.add_argument("--device", default=None, help="Device: cuda:0, cpu (default: auto)")
82+
p.add_argument("--output-format", "-f", default="text", choices=["text", "json", "srt", "tsv"], help="Output format (default: text)")
83+
p.add_argument("--output-dir", "-o", default=None, help="Write output files to directory")
84+
p.add_argument("--timestamps", action="store_true", help="Include word-level timestamps")
85+
p.add_argument("--spk", action="store_true", help="Enable speaker diarization")
86+
p.add_argument("--hotwords", default=None, help="Comma-separated hotwords")
87+
p.add_argument("--verbose", "-v", action="store_true", help="Show loading/timing info on stderr")
88+
p.add_argument("--version", action="version", version=f"%(prog)s {_get_version()}")
89+
args = p.parse_args()
90+
91+
if args.verbose:
92+
print(f"Loading model: {args.model} ...", file=sys.stderr)
93+
94+
import torch
95+
from funasr import AutoModel
96+
97+
device = args.device or ("cuda:0" if torch.cuda.is_available() else "cpu")
98+
config = MODEL_CONFIGS[args.model].copy()
99+
if args.spk and "spk_model" not in config:
100+
config["spk_model"] = "cam++"
101+
if "punc_model" not in config and args.model not in ("fun-asr-nano", "sensevoice"):
102+
config["punc_model"] = "ct-punc"
103+
104+
t_load = time.time()
105+
model = AutoModel(device=device, disable_update=True, **config)
106+
if args.verbose:
107+
print(f"Model loaded in {time.time() - t_load:.1f}s", file=sys.stderr)
108+
109+
if args.output_dir:
110+
os.makedirs(args.output_dir, exist_ok=True)
111+
112+
for audio_path in args.audio:
113+
if not os.path.isfile(audio_path):
114+
print(f"Error: file not found: {audio_path}", file=sys.stderr)
115+
sys.exit(1)
116+
117+
if args.verbose:
118+
print(f"Transcribing: {audio_path}", file=sys.stderr)
119+
120+
t0 = time.time()
121+
gen_kw = {"input": audio_path, "batch_size": 1}
122+
if args.language:
123+
gen_kw["language"] = args.language
124+
if args.hotwords:
125+
gen_kw["hotwords"] = args.hotwords.split(",")
126+
127+
result = model.generate(**gen_kw)
128+
elapsed = time.time() - t0
129+
130+
text = clean_text(result[0].get("text", ""))
131+
segments = []
132+
if "sentence_info" in result[0]:
133+
for seg in result[0]["sentence_info"]:
134+
s = {"start": seg.get("start", 0), "end": seg.get("end", 0), "text": clean_text(seg.get("text", ""))}
135+
if args.spk and "spk" in seg:
136+
s["speaker"] = seg["spk"]
137+
segments.append(s)
138+
139+
timestamps = result[0].get("timestamps") if args.timestamps else None
140+
output = _format_output(text, segments, timestamps, args.output_format, audio_path, args.model, args.language, elapsed)
141+
142+
if args.output_dir:
143+
ext = {"text": "txt", "json": "json", "srt": "srt", "tsv": "tsv"}[args.output_format]
144+
out_path = os.path.join(args.output_dir, os.path.splitext(os.path.basename(audio_path))[0] + "." + ext)
145+
with open(out_path, "w", encoding="utf-8") as f:
146+
f.write(output)
147+
if args.verbose:
148+
print(f"Written: {out_path}", file=sys.stderr)
149+
else:
150+
print(output)
151+
152+
if args.verbose:
153+
print(f"Done in {elapsed:.2f}s", file=sys.stderr)
154+
155+
156+
if __name__ == "__main__":
157+
main()

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@
152152
],
153153
entry_points={
154154
"console_scripts": [
155-
"funasr = funasr.bin.inference:main_hydra",
155+
"funasr = funasr.cli:main",
156+
"funasr-hydra = funasr.bin.inference:main_hydra",
156157
"funasr-server = funasr.bin.server:main",
157158
"funasr-train = funasr.bin.train:main_hydra",
158159
"funasr-train-ds = funasr.bin.train_ds:main_hydra",

0 commit comments

Comments
 (0)