Files
2026-07-13 13:25:10 +08:00

140 lines
4.6 KiB
Python

"""
FunASR Subtitle Generator
Generate SRT/VTT subtitles from audio/video files.
Usage:
python generate_subtitle.py input.mp4
python generate_subtitle.py input.wav --format vtt
python generate_subtitle.py meeting.mp3 --spk # with speaker labels
"""
import argparse
import sys
import os
import re
def clean_text(text):
return re.sub(r'<\|[^|]*\|>', '', text or "").strip()
def format_time_srt(ms):
h = ms // 3600000
m = (ms % 3600000) // 60000
s = (ms % 60000) // 1000
ms_rem = ms % 1000
return f"{h:02d}:{m:02d}:{s:02d},{ms_rem:03d}"
def format_time_vtt(ms):
h = ms // 3600000
m = (ms % 3600000) // 60000
s = (ms % 60000) // 1000
ms_rem = ms % 1000
return f"{h:02d}:{m:02d}:{s:02d}.{ms_rem:03d}"
def timestamp_bounds_ms(result):
bounds = []
for key in ("timestamp", "timestamps"):
for ts in result.get(key, []) or []:
if isinstance(ts, dict):
start = ts.get("start_time", ts.get("start"))
end = ts.get("end_time", ts.get("end"))
if start is None or end is None:
continue
start_ms = int(float(start) * 1000)
end_ms = int(float(end) * 1000)
elif isinstance(ts, (list, tuple)) and len(ts) >= 2:
start_ms = int(ts[0])
end_ms = int(ts[1])
else:
continue
if end_ms > start_ms:
bounds.append((start_ms, end_ms))
if not bounds:
return None
return min(start for start, _ in bounds), max(end for _, end in bounds)
def main():
parser = argparse.ArgumentParser(description="Generate subtitles from audio/video using FunASR")
parser.add_argument("input", help="Audio/video file path")
parser.add_argument("-o", "--output", help="Output file (default: input.srt)")
parser.add_argument("--format", choices=["srt", "vtt"], default="srt")
parser.add_argument("--model", default="iic/SenseVoiceSmall")
parser.add_argument("--device", default="cuda")
parser.add_argument("--spk", action="store_true", help="Include speaker labels")
parser.add_argument("--lang", default="auto")
args = parser.parse_args()
if not os.path.exists(args.input):
print(f"Error: {args.input} not found")
sys.exit(1)
output_path = args.output or f"{os.path.splitext(args.input)[0]}.{args.format}"
print(f"Input: {args.input}")
print(f"Output: {output_path}")
from funasr import AutoModel
kwargs = {"model": args.model, "vad_model": "fsmn-vad", "punc_model": "ct-punc",
"vad_kwargs": {"max_single_segment_time": 30000},
"device": args.device, "disable_update": True}
if args.spk:
kwargs["spk_model"] = "cam++"
if "Fun-ASR-Nano" in args.model or "Qwen" in args.model:
kwargs["trust_remote_code"] = True
kwargs["hub"] = "hf"
print("Loading model...")
model = AutoModel(**kwargs)
print("Transcribing...")
generate_kwargs = {
"input": args.input,
"batch_size": 1,
"sentence_timestamp": True,
"output_timestamp": True,
"return_time_stamps": True,
}
if args.lang != "auto":
generate_kwargs["language"] = args.lang
result = model.generate(**generate_kwargs)
segments = []
result_item = result[0]
for seg in result_item.get("sentence_info", []) or []:
text = clean_text(seg.get("sentence") or seg.get("text", ""))
start = int(seg.get("start", 0) or 0)
end = int(seg.get("end", 0) or 0)
if text and end > start:
segments.append({"start": start, "end": end, "text": text, "spk": seg.get("spk")})
if not segments:
text = clean_text(result_item.get("text", ""))
if text:
start, end = timestamp_bounds_ms(result_item) or (0, 0)
segments.append({"start": start, "end": end, "text": text, "spk": None})
if not segments:
print("No speech detected.")
sys.exit(0)
fmt = format_time_srt if args.format == "srt" else format_time_vtt
with open(output_path, "w", encoding="utf-8") as f:
if args.format == "vtt":
f.write("WEBVTT\n\n")
for i, seg in enumerate(segments, 1):
text = f"[Speaker {seg['spk']}] {seg['text']}" if args.spk and seg['spk'] is not None else seg['text']
if args.format == "srt":
f.write(f"{i}\n{fmt(seg['start'])} --> {fmt(seg['end'])}\n{text}\n\n")
else:
f.write(f"{fmt(seg['start'])} --> {fmt(seg['end'])}\n{text}\n\n")
print(f"Done! {len(segments)} subtitles → {output_path}")
if __name__ == "__main__":
main()