1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# -*- coding: utf-8 -*-
"""whisper_asr.py — faster-whisper 命令行转写工具.

用法: whisper <媒体文件> [-m 模型] [-l 语言] [-o 输出] [--plain]
"""
from __future__ import annotations

import argparse
import os
import subprocess
import sys
import tempfile
import wave
from pathlib import Path

os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com")

import numpy as np # noqa: E402
from faster_whisper import WhisperModel # noqa: E402


def extract_audio(media: Path) -> np.ndarray:
"""用 ffmpeg 将任意媒体解码为 16kHz 单声道 float32 数组."""
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
tmp_path = tmp.name
try:
cmd = [
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
"-i", str(media), "-vn", "-ac", "1", "-ar", "16000", tmp_path,
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
sys.exit(f"ffmpeg 解码失败:\n{result.stderr.strip()}")
with wave.open(tmp_path, "rb") as w:
frames = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)
return frames.astype(np.float32) / 32768.0
finally:
os.unlink(tmp_path)


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="whisper",
description="通过 faster-whisper 将音视频文件转写为文案",
)
parser.add_argument("media", type=Path, help="待转写的音频/视频文件路径")
parser.add_argument("-m", "--model", default="small",
help="模型名称: tiny/base/small/medium/large-v3 (默认: small)")
parser.add_argument("-l", "--language", default=None,
help="语言代码, 如 zh/en (默认: 自动检测)")
parser.add_argument("-o", "--out", type=Path, default=None,
help="输出文件路径 (默认: 与输入同名的 .txt)")
parser.add_argument("--plain", action="store_true",
help="只输出纯文案, 不带时间轴")
return parser.parse_args()


def main() -> None:
args = parse_args()
if not args.media.is_file():
sys.exit(f"文件不存在: {args.media}")
out_path = args.out or args.media.with_suffix(".txt")

print(f"加载模型 {args.model} ...", file=sys.stderr)
model = WhisperModel(args.model, device="cpu", compute_type="int8")

print(f"转写 {args.media.name} ...", file=sys.stderr)
audio = extract_audio(args.media)
segments, info = model.transcribe(
audio,
language=args.language,
beam_size=5,
vad_filter=True,
)

lines = []
for seg in segments:
text = seg.text.strip()
line = text if args.plain else f"[{seg.start:.1f}s - {seg.end:.1f}s] {text}"
print(line)
lines.append(line)

out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"\n语言: {info.language} ({info.language_probability:.0%}),"
f"已保存到: {out_path}", file=sys.stderr)


if __name__ == "__main__":
main()