pyvideotrans / videotrans /task /speech2text.py
jameson512
Upload files via LFS
1f235c3
Raw
History Blame Contribute Delete
14.5 kB
import json
import os
import re
import shutil
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import List
from videotrans.configure import contants
from videotrans.configure.config import ROOT_DIR, tr, settings, logger, HOME_DIR
from videotrans.configure import config
from videotrans.recognition import run
from videotrans.task._base import BaseTask
from videotrans.task.taskcfg import TaskCfgSTT
"""
仅语音识别
"""
@dataclass
class SpeechToText(BaseTask):
cfg: TaskCfgSTT = field(default_factory=TaskCfgSTT, repr=False)
# 识别后输出的字幕格式,srt txt 等
out_format: str = field(init=True, default='srt')
# 在这个子类中,should_recogn 总是 True。
should_recogn: bool = True
# 是否需要将生成的字幕复制到原始视频所在目录下,并重命名为视频同名,以方便视频自动加载软字幕
copysrt_rawvideo: bool = field(default=False, init=True)
# 存放原始语言字幕
source_srt_list: List = field(default_factory=list)
# 插入说话人到字幕开头
spk_insert: bool = False
def __post_init__(self):
super().__post_init__()
# -1=不启用说话人,0=启用并且不限制说话人数量,>0+1是最大说话人数量
self.max_speakers = self.cfg.nums_diariz if self.cfg.enable_diariz else -1
if self.max_speakers > 0:
self.max_speakers += 1
# 存放目标文件夹
if not self.cfg.target_dir:
self.cfg.target_dir = HOME_DIR + f"/recogn"
# 转录后的目标字幕文件,先统一转为srt,然后再使用ffmpeg转为其他格式字幕
self.cfg.target_sub = self.cfg.target_dir + '/' + self.cfg.noextname + '.srt'
# 临时文件夹
self.cfg.cache_folder = config.TEMP_DIR + f'/{self.uuid}'
# 处理为 16k 的wav单通道音频,供模型识别用
self.cfg.shibie_audio = self.cfg.cache_folder + f'/{self.cfg.noextname}-{time.time()}.wav'
self.signal(text=tr("Speech Recognition to Word Processing"))
# 预先处理
def prepare(self):
if self._exit(): return
Path(self.cfg.target_dir).mkdir(parents=True, exist_ok=True)
Path(self.cfg.cache_folder).mkdir(parents=True, exist_ok=True)
from videotrans.util.help_ffmpeg import conver_to_16k
conver_to_16k(self.cfg.name, self.cfg.shibie_audio)
def recogn(self):
while 1:
if self._exit(): return
# 尚未生成
if Path(self.cfg.shibie_audio).exists():
break
time.sleep(0.5)
from videotrans.util.help_down import down_file_from_ms
from videotrans.configure.excepts import SpeechToTextError
# 需要降噪
if self.cfg.remove_noise:
logger.debug('开始降噪')
from videotrans.process.prepare_audio import remove_noise
title = tr('Starting to process speech noise reduction, which may take a long time, please be patient')
down_file_from_ms(f'{ROOT_DIR}/models/onnx', urls=[
'https://modelscope.cn/models/himyworld/videotrans/resolve/master/onnx/dpdfnet4.onnx'],
callback=self._process_callback)
_noise_wav = f"{config.TEMP_DIR}/{self.cfg.noextname}-{os.path.getsize(self.cfg.name)}-removed_noise.wav"
kw = {
"input_file": self.cfg.shibie_audio,
"output_file": _noise_wav,
"is_cuda": self.cfg.is_cuda
}
try:
_rs = self._new_process(callback=remove_noise, title=title, is_cuda=self.cfg.is_cuda, kwargs=kw)
if _rs:
self.cfg.shibie_audio = _noise_wav
self.signal(text='remove noise end')
except Exception as e:
logger.exception(f'降噪失败,跳过 {e}', exc_info=True)
if self._exit(): return
raw_subtitles = run(
recogn_type=self.cfg.recogn_type,
uuid=self.uuid,
model_name=self.cfg.model_name,
audio_file=self.cfg.shibie_audio,
detect_language=self.cfg.detect_language,
cache_folder=self.cfg.cache_folder,
is_cuda=self.cfg.is_cuda,
subtitle_type=0,
max_speakers=self.max_speakers,
llm_post=self.cfg.rephrase == 1
)
if not raw_subtitles or len(raw_subtitles) < 1:
raise SpeechToTextError(self.cfg.basename + tr('recogn result is empty'))
self.source_srt_list = raw_subtitles
self._save_srt_target(self.source_srt_list, self.cfg.target_sub)
if self._exit() or self.cfg.detect_language == 'auto': return
# 中英恢复标点符号
if self.cfg.fix_punc==1 and self.cfg.detect_language[:2] in ['zh', 'en']:
from videotrans.process.prepare_audio import fix_punc
down_file_from_ms(f'{ROOT_DIR}/models/puntc', [
"https://www.modelscope.cn/models/himyworld/videotrans/resolve/master/puntc/model.onnx",
"https://www.modelscope.cn/models/himyworld/videotrans/resolve/master/puntc/config.yaml",
"https://www.modelscope.cn/models/himyworld/videotrans/resolve/master/puntc/tokens.json",
], callback=self._process_callback)
text_dict = {f'{it["line"]}': re.sub(r'[,.?!,。?!]', ' ', it["text"]) for it in self.source_srt_list}
# 序列化后传递文件路径
text_dict_file=f'{self.cfg.cache_folder}/text_dict_file_{time.time()}.json'
Path(text_dict_file).write_text(json.dumps(text_dict),encoding="utf-8")
kw = {"text_dict_file": text_dict_file, "is_cuda": self.cfg.is_cuda}
try:
_rs = self._new_process(callback=fix_punc, title=tr("Restoring punct"), kwargs=kw)
if _rs:
text_dict_obj=json.loads(Path(text_dict_file).read_text(encoding='utf-8'))
for it in self.source_srt_list:
it['text'] = text_dict_obj.get(f'{it["line"]}', it['text'])
if self.cfg.detect_language[:2] == 'en':
it['text'] = it['text'].replace(',', ',').replace('。', '. ').replace('?', '?').replace(
'!', '!')
self._save_srt_target(self.source_srt_list, self.cfg.target_sub)
else:
logger.error('标点恢复出错')
except Exception as e:
logger.exception(f'恢复标点出错,跳过{e}', exc_info=True)
# 本身已有说话人识别的,就不再重新断句
self.signal(text=Path(self.cfg.target_sub).read_text(encoding='utf-8'), type='replace_subtitle')
if Path(self.cfg.cache_folder + "/speaker.json").exists(): return
if self.cfg.rephrase == 1:
# LLM重新断句
try:
from videotrans.translator._openaicompat import OpenAICampat
ob = OpenAICampat(
ainame='chatgpt' if settings.get('llm_ai_type', 'chatgpt') != 'deepseek' else 'deepseek',
uuid=self.uuid)
self.signal(text=tr("Re-segmenting..."))
srt_list = ob.llm_segment(self.source_srt_list)
if srt_list and len(srt_list) > len(self.source_srt_list) / 2:
self.source_srt_list = srt_list
self._save_srt_target(self.source_srt_list, self.cfg.target_sub)
else:
logger.error(f'重新断句失败,已恢复原样,原始字幕行:{len(self.source_srt_list)}, 重新断句后字幕行:{len(srt_list)}\n断句结果:\n{srt_list=}')
except Exception as e:
self.signal(text=tr("Re-segmenting Error"))
logger.exception(f"重新断句失败已恢复原样 {e}", exc_info=True)
def diariz(self):
if self._exit() or not self.cfg.enable_diariz or Path(self.cfg.cache_folder + "/speaker.json").exists():
return
from videotrans.util.help_down import down_file_from_ms, check_and_down_ms
speaker_type = settings.get('speaker_type', 'built')
hf_token = settings.get('hf_token')
if speaker_type == 'built' and self.cfg.detect_language[:2] not in ['zh', 'en']:
logger.error(f'当前选择 built 说话人分离模型,但不支持当前语言:{self.cfg.detect_language}')
return
if speaker_type in ['pyannote', 'reverb'] and not hf_token:
logger.error(f'当前选择 pyannote 说话人分离模型,但未设置 huggingface.co 的token: {self.cfg.detect_language}')
return
hf_endpoit = "https://huggingface.co"
if speaker_type in ['pyannote', 'reverb']:
try:
import requests
requests.head('https://huggingface.co', timeout=5)
except Exception:
logger.exception(f'当前选择 {speaker_type} 说话人分离模型,但无法连接到 https://huggingface.co,可能会失败', exc_info=True)
hf_endpoit = "https://hf-mirror.com"
self.precent += 3
title = tr(f'Begin separating the speakers') + f':{speaker_type}'
subtitles_file=f'{self.cfg.cache_folder}/diariz-{time.time()}.json'
Path(subtitles_file).write_text(json.dumps([[it['start_time'], it['end_time']] for it in self.source_srt_list]),encoding='utf-8')
kw = {
"input_file": self.cfg.shibie_audio,
"subtitles_file": subtitles_file,
"speak_file":self.cfg.cache_folder + "/speaker.json",
"num_speakers": self.max_speakers,
"is_cuda": self.cfg.is_cuda
}
if speaker_type == 'built':
down_file_from_ms(f'{ROOT_DIR}/models/onnx', [
"https://www.modelscope.cn/models/himyworld/videotrans/resolve/master/onnx/seg_model.onnx",
"https://www.modelscope.cn/models/himyworld/videotrans/resolve/master/onnx/nemo_en_titanet_small.onnx",
"https://www.modelscope.cn/models/himyworld/videotrans/resolve/master/onnx/3dspeaker_speech_eres2net_large_sv_zh-cn_3dspeaker_16k.onnx"
], callback=self._process_callback)
from videotrans.process.prepare_audio import built_speakers as _run_speakers
del kw['is_cuda']
kw['num_speakers'] = -1 if self.max_speakers < 1 else self.max_speakers
kw['language'] = self.cfg.detect_language
elif speaker_type == 'ali_CAM':
check_and_down_ms(model_id='iic/speech_campplus_speaker-diarization_common',
callback=self._process_callback)
from videotrans.process.prepare_audio import cam_speakers as _run_speakers
elif speaker_type == 'pyannote':
from videotrans.process.prepare_audio import pyannote_speakers as _run_speakers
elif speaker_type == 'reverb':
from videotrans.process.prepare_audio import reverb_speakers as _run_speakers
else:
logger.error(f'当前所选说话人分离模型不支持:{speaker_type=}')
return
try:
if speaker_type in ['pyannote', 'reverb']:
self.signal(text='Downloading speakers models')
from huggingface_hub import snapshot_download
snapshot_download(
repo_id="pyannote/speaker-diarization-3.1" if speaker_type == 'pyannote' else "Revai/reverb-diarization-v1",
token=hf_token,
endpoint=hf_endpoit
)
_rs = self._new_process(callback=_run_speakers, title=title,
is_cuda=self.cfg.is_cuda and speaker_type != 'built', kwargs=kw)
logger.debug('分离说话人成功完成' if _rs else '分离失败说话人失败')
self.signal(text=tr('separating speakers end'))
except Exception as e:
logger.exception(f'说话人分离失败,跳过 {e}', exc_info=True)
self.signal(text=tr('separating speakers end'))
def task_done(self):
if self._exit(): return
from videotrans.util.help_srt import simple_wrap
if self.cfg.detect_language and self.cfg.detect_language != 'auto':
# 处理换行
maxlen = int(
settings.get('cjk_len', 15) if self.cfg.detect_language[:2] in contants.CJK_LANG else
settings.get('other_len', 60))
for i, it in enumerate(self.source_srt_list):
it['text'] = simple_wrap(it['text'], maxlen, self.cfg.detect_language)
# 移除标点符号
if self.cfg.fix_punc==2:
from videotrans.util.help_srt import delete_punc
for i, it in enumerate(self.source_srt_list):
it['text'] = delete_punc(it['text'])
if self.cfg.enable_diariz and self.spk_insert and Path(
self.cfg.cache_folder + "/speaker.json").exists():
speakers = json.loads(Path(self.cfg.cache_folder + "/speaker.json").read_text(encoding='utf-8'))
if speakers:
speakers_len = len(speakers)
for i, it in enumerate(self.source_srt_list):
if i < speakers_len and speakers[i]:
it['text'] = f'[{speakers[i]}]{it["text"]}'
self._save_srt_target(self.source_srt_list, self.cfg.target_sub)
if self.out_format == 'txt':
self.cfg.target_sub = self.cfg.target_sub[:-3] + 'txt'
Path(self.cfg.target_sub).write_text("\r\n".join([it["text"] for it in self.source_srt_list]),
encoding='utf-8')
elif self.out_format != 'srt':
from videotrans.util.help_ffmpeg import runffmpeg
runffmpeg(['-y', '-i', self.cfg.target_sub, self.cfg.target_sub[:-3] + self.out_format])
Path(self.cfg.target_sub).unlink(missing_ok=True)
self.cfg.target_sub = self.cfg.target_sub[:-3] + self.out_format
if self.copysrt_rawvideo:
p = Path(self.cfg.name)
try:
shutil.copy2(self.cfg.target_sub, f'{p.parent.as_posix()}/{p.stem}.{self.out_format}')
except shutil.SameFileError:
pass
self.set_end(True)