diff --git a/.gitignore b/.gitignore index b9dba3c8..31675d3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,31 @@ +# Python __pycache__/ desktop/ +build/ +dist/ +*venv*/ +python_embeded/ +build/ +dist/ +*venv*/ +python_embeded/ *.pyc -*.bin -log.txt -config.txt + +# Misc +project/ +.vscode/ +*.log .env gui_settings.yaml -# VSCode -.vscode +*.dll +*.exe useful_tools/GalTransl_DumpInjector/config.ini useful_tools/GalTransl_DumpInjector/VNTextPatch/names.xml -*.log -venv*/ -project/ -python_embeded/ + +# Models +models/ *.gguf -faster-whisper*/ -build/ -dist/ -translate/ -whisper-faster/whisper-faster.exe -*.dll -*.exe -# Python venv -.venv -# Faster Whisper XXL -_xxl_data -project/config.yaml + # Tests -Tests -separate/ +tests +separate \ No newline at end of file diff --git a/app.py b/app.py index 8ec0199c..c7bf53b1 100644 --- a/app.py +++ b/app.py @@ -40,6 +40,13 @@ def open_path(path_value: str): from prompt2srt import make_srt, make_lrc, merge_lrc_files from srt2prompt import make_prompt, merge_srt_files +import asrlabs_bridge +from compat import ( + migrate_config_txt, + migrate_old_whisper_models, + migrate_output_format, + map_whisper_to_asr, +) ONLINE_TRANSLATOR_MAPPING = { 'Kimi': 'https://api.moonshot.cn', @@ -902,7 +909,6 @@ def save_config(self, silent: bool = False): """保存 GUI 配置到 gui_settings.yaml 及相关文件""" if not silent: self._emit_status(_("status_reading_config")) - whisper_file = self.whisper_file.currentText() translator = self.translator_group.currentText() language = self.input_lang.currentText() gpt_token = self.gpt_token.text() @@ -925,8 +931,18 @@ def save_config(self, silent: bool = False): target_translation_lang = self.target_lang.currentData() if hasattr(self, 'target_lang') else 'zh-cn' current_lang = get_language() + # ASRLabs 配置 + asr_engine = self.asr_engine_combo.currentData() or '' if hasattr(self, 'asr_engine_combo') else '' + asr_model = self.asr_model_combo.currentData() or '' if hasattr(self, 'asr_model_combo') else '' + asr_device = self.asr_device_combo.currentText() if hasattr(self, 'asr_device_combo') else 'auto' + asr_compute_type = self.asr_compute_type_combo.currentText() if hasattr(self, 'asr_compute_type_combo') else 'float16' + asr_extra = self.asr_extra_edit.toPlainText() if hasattr(self, 'asr_extra_edit') else '' + align_engine = self.align_engine_combo.currentData() or 'none' if hasattr(self, 'align_engine_combo') else 'none' + align_model = self.align_model_combo.currentData() or '' if hasattr(self, 'align_model_combo') else '' + align_device = self.align_device_combo.currentText() if hasattr(self, 'align_device_combo') else 'auto' + align_extra = self.align_extra_edit.toPlainText() if hasattr(self, 'align_extra_edit') else '' + gui_settings = { - 'whisper_file': whisper_file, 'translator': translator, 'language': language, 'gpt_address': gpt_address, @@ -946,16 +962,22 @@ def save_config(self, silent: bool = False): 'log_level_filter': self.log_filter_combo.currentText(), 'verbose_mode': self.verbose_checkbox.isChecked(), 'ui_language': current_lang, - 'target_translation_lang': target_translation_lang, + # ASRLabs 配置 + 'asr_engine': asr_engine, + 'asr_model': asr_model, + 'asr_device': asr_device, + 'asr_compute_type': asr_compute_type, + 'asr_extra': asr_extra, + 'align_engine': align_engine, + 'align_model': align_model, + 'align_device': align_device, + 'align_extra': align_extra, } with open('gui_settings.yaml', 'w', encoding='utf-8') as f: yaml.dump(gui_settings, f, allow_unicode=True, sort_keys=False, default_flow_style=False) _save_api_key(gpt_token) - with open('whisper/param.txt', 'w', encoding='utf-8') as f: - f.write(self.param_whisper.toPlainText()) - with open('llama/param.txt', 'w', encoding='utf-8') as f: f.write(self.param_llama.toPlainText()) @@ -1113,19 +1135,7 @@ def collect_font_candidates(self): return unique def refresh_speech_model_lists(self): - if hasattr(self, 'whisper_file'): - current_whisper = self.whisper_file.currentText() - whisper_lst = [ - i for i in os.listdir('whisper') - if i.startswith('ggml') and i.endswith('bin') and 'silero' not in i - ] + [ - i for i in os.listdir('whisper-faster') if i.startswith('faster-whisper') - ] + ['不进行听写'] - self.whisper_file.clear() - self.whisper_file.addItems(whisper_lst) - if current_whisper in whisper_lst: - self.whisper_file.setCurrentText(current_whisper) - + """兼容旧调用:刷新 UVR 模型列表""" if hasattr(self, 'uvr_file'): current_uvr = self.uvr_file.currentText() uvr_lst = [i for i in os.listdir('separate') if i.endswith('onnx')] @@ -1134,6 +1144,115 @@ def refresh_speech_model_lists(self): if current_uvr in uvr_lst: self.uvr_file.setCurrentText(current_uvr) + def refresh_asr_engine_lists(self): + """刷新 ASRLabs 引擎列表、模型列表和对齐器列表 + + 主要逻辑: + 1. 调用 asrlabs list --json 获取引擎元数据 + 2. 填充听写引擎下拉框(保留当前选择) + 3. 填充听写模型下拉框(扫描 models/transcribe/) + 4. 触发 on_asr_engine_changed 更新对齐器下拉框 + 5. 填充对齐模型下拉框(扫描 models/align/) + """ + # 获取引擎元数据 + try: + transcribers, aligners = asrlabs_bridge.fetch_engine_metadata(force_refresh=True) + except Exception as e: + self._emit_status(_("status_asrlabs_metadata_error", error=e)) + return + + # 填充听写引擎下拉框 + current_engine = self.asr_engine_combo.currentData() or '' + self.asr_engine_combo.blockSignals(True) + self.asr_engine_combo.clear() + self.asr_engine_combo.addItem('不进行听写', userData='') + for t in transcribers: + display = f"{t['name']} ({t['display_name']})" + self.asr_engine_combo.addItem(display, userData=t['name']) + # 恢复选择 + if current_engine: + idx = self.asr_engine_combo.findData(current_engine) + if idx >= 0: + self.asr_engine_combo.setCurrentIndex(idx) + self.asr_engine_combo.blockSignals(False) + + # 填充听写模型下拉框(currentData 存完整路径) + current_t_model = self.asr_model_combo.currentData() or '' + t_models = asrlabs_bridge.list_transcribe_models() + self.asr_model_combo.clear() + for m in t_models: + full_path = os.path.join('models', 'transcribe', m) + self.asr_model_combo.addItem(m, userData=full_path) + if current_t_model: + idx = self.asr_model_combo.findData(current_t_model) + if idx >= 0: + self.asr_model_combo.setCurrentIndex(idx) + + # 填充对齐模型下拉框 + current_a_model = self.align_model_combo.currentData() or '' + a_models = asrlabs_bridge.list_align_models() + self.align_model_combo.clear() + for m in a_models: + full_path = os.path.join('models', 'align', m) + self.align_model_combo.addItem(m, userData=full_path) + if current_a_model: + idx = self.align_model_combo.findData(current_a_model) + if idx >= 0: + self.align_model_combo.setCurrentIndex(idx) + + # 触发对齐引擎下拉框更新 + self.on_asr_engine_changed() + + def on_asr_engine_changed(self): + """听写引擎变更时动态更新对齐引擎下拉框 + + 规则: + - 引擎有内置时间戳(supports_timestamps=True)→ 添加"不进行对齐"选项 + - 引擎无内置时间戳 → 不添加"不进行对齐",必须选择对齐器 + - 推荐对齐器排在第一位 + """ + engine_name = self.asr_engine_combo.currentData() or '' + if not engine_name: + # "不进行听写"时不需更新对齐器 + return + + meta = asrlabs_bridge.get_transcriber_meta(engine_name) + if not meta: + return + + _transcribers, aligners = asrlabs_bridge.fetch_engine_metadata() + + current_aligner = self.align_engine_combo.currentData() or '' + self.align_engine_combo.blockSignals(True) + self.align_engine_combo.clear() + + # 引擎有内置时间戳时允许跳过对齐 + if meta.get('supports_timestamps', False): + self.align_engine_combo.addItem(_('settings_align_no_align'), userData='none') + + # 推荐对齐器排在第一位 + recommended = meta.get('recommended_aligner') + added_names = set() + if recommended: + for a in aligners: + if a['name'] == recommended: + self.align_engine_combo.addItem(f"{a['name']} ({a['display_name']})", userData=a['name']) + added_names.add(a['name']) + break + + # 其余对齐器 + for a in aligners: + if a['name'] not in added_names: + self.align_engine_combo.addItem(f"{a['name']} ({a['display_name']})", userData=a['name']) + added_names.add(a['name']) + + # 恢复选择 + if current_aligner: + idx = self.align_engine_combo.findData(current_aligner) + if idx >= 0: + self.align_engine_combo.setCurrentIndex(idx) + self.align_engine_combo.blockSignals(False) + def refresh_language_model_lists(self): if hasattr(self, 'sakura_file'): current_model = self.sakura_file.currentText() @@ -1162,39 +1281,6 @@ def cancel_task(self): self._emit_status(_("status_cancel_done")) - def _migrate_config_txt(self): - """从旧 config.txt 迁移到 gui_settings.yaml + .env,返回 gui_settings 字典""" - with open('config.txt', 'r', encoding='utf-8') as f: - lines = f.readlines() - - gpt_token = lines[3].strip() if len(lines) > 3 else '' - _save_api_key(gpt_token) - - gui_settings = { - 'whisper_file': lines[0].strip(), - 'translator': lines[1].strip(), - 'language': lines[2].strip(), - 'gpt_address': lines[4].strip(), - 'gpt_model': lines[5].strip(), - 'sakura_file': lines[6].strip(), - 'sakura_mode': lines[7].strip(), - 'proxy_address': lines[8].strip(), - 'uvr_file': lines[9].strip(), - 'output_format': lines[10].strip(), - 'subtitle_font': lines[11].strip() if len(lines) > 11 else "", - 'output_dir': lines[12].strip() if len(lines) > 12 else self.default_output_dir(), - 'use_input_dir': (lines[13].strip().lower() == 'true') if len(lines) > 13 else False, - 'max_concurrent': int(lines[14].strip()) if len(lines) > 14 else 1, - 'enable_segment': (lines[15].strip().lower() == 'true') if len(lines) > 15 else False, - 'segment_duration': int(lines[16].strip()) if len(lines) > 16 else 10, - 'change_prompt_mode': lines[17].strip() if len(lines) > 17 else '不修改', - } - - with open('gui_settings.yaml', 'w', encoding='utf-8') as f: - yaml.dump(gui_settings, f, allow_unicode=True, sort_keys=False, default_flow_style=False) - - return gui_settings - def load_config(self): """加载 GUI 配置(优先 gui_settings.yaml,兼容旧 config.txt 自动迁移)""" self._suppress_auto_save = True @@ -1204,11 +1290,9 @@ def load_config(self): with open('gui_settings.yaml', 'r', encoding='utf-8') as f: gui_settings = yaml.safe_load(f) or {} elif os.path.exists('config.txt'): - gui_settings = self._migrate_config_txt() + gui_settings = migrate_config_txt() if gui_settings: - if self.whisper_file and gui_settings.get('whisper_file'): - self.whisper_file.setCurrentText(gui_settings['whisper_file']) self.translator_group.setCurrentText(gui_settings.get('translator', '')) self.input_lang.setCurrentText(gui_settings.get('language', '')) self.gpt_address.setText(gui_settings.get('gpt_address', '')) @@ -1219,10 +1303,7 @@ def load_config(self): self.proxy_address.setText(gui_settings.get('proxy_address', '')) if self.uvr_file: self.uvr_file.setCurrentText(gui_settings.get('uvr_file', '')) - _fmt_loaded = gui_settings.get('output_format', '双语SRT') - # 迁移旧值:中文SRT/LRC → 目标SRT/LRC - _fmt_migrate = {'中文SRT': '目标SRT', '中文LRC': '目标LRC'} - _fmt_loaded = _fmt_migrate.get(_fmt_loaded, _fmt_loaded) + _fmt_loaded = migrate_output_format(gui_settings.get('output_format', '双语SRT')) _fmt_idx = self.output_format.findData(_fmt_loaded) if _fmt_idx >= 0: self.output_format.setCurrentIndex(_fmt_idx) @@ -1253,6 +1334,59 @@ def load_config(self): if _tl_idx >= 0: self.target_lang.setCurrentIndex(_tl_idx) + # ── ASRLabs 配置加载 + 旧配置迁移 ── + # 旧配置迁移:检测 whisper_file → 迁移模型文件 → 清理旧目录(须在刷新引擎列表之前) + old_whisper = gui_settings.get('whisper_file', '') + if old_whisper and old_whisper != '不进行听写' and not gui_settings.get('asr_engine'): + migrated = migrate_old_whisper_models(old_whisper, log_fn=self._emit_status) + gui_settings['asr_engine'] = migrated['asr_engine'] + gui_settings['asr_model'] = migrated['asr_model'] + gui_settings['whisper_file'] = '' # 清理旧字段 + + # 刷新引擎列表(从 asrlabs 获取引擎元数据,扫描已迁移到 models/transcribe/ 的模型) + try: + self.refresh_asr_engine_lists() + except Exception: + pass # asrlabs 未安装时静默跳过 + + # 应用 ASRLabs 配置 + if hasattr(self, 'asr_engine_combo'): + asr_engine = gui_settings.get('asr_engine', '') + if asr_engine: + idx = self.asr_engine_combo.findData(asr_engine) + if idx >= 0: + self.asr_engine_combo.setCurrentIndex(idx) + # 引擎变更后刷新对齐器下拉框 + self.on_asr_engine_changed() + + if hasattr(self, 'asr_model_combo'): + asr_model = gui_settings.get('asr_model', '') + if asr_model: + idx = self.asr_model_combo.findData(asr_model) + if idx >= 0: + self.asr_model_combo.setCurrentIndex(idx) + if hasattr(self, 'asr_device_combo'): + self.asr_device_combo.setCurrentText(gui_settings.get('asr_device', 'auto')) + if hasattr(self, 'asr_compute_type_combo'): + self.asr_compute_type_combo.setCurrentText(gui_settings.get('asr_compute_type', 'float16')) + if hasattr(self, 'asr_extra_edit'): + self.asr_extra_edit.setPlainText(gui_settings.get('asr_extra', '')) + if hasattr(self, 'align_engine_combo'): + align_engine = gui_settings.get('align_engine', 'none') + idx = self.align_engine_combo.findData(align_engine) + if idx >= 0: + self.align_engine_combo.setCurrentIndex(idx) + if hasattr(self, 'align_model_combo'): + align_model = gui_settings.get('align_model', '') + if align_model: + idx = self.align_model_combo.findData(align_model) + if idx >= 0: + self.align_model_combo.setCurrentIndex(idx) + if hasattr(self, 'align_device_combo'): + self.align_device_combo.setCurrentText(gui_settings.get('align_device', 'auto')) + if hasattr(self, 'align_extra_edit'): + self.align_extra_edit.setPlainText(gui_settings.get('align_extra', '')) + # API Key 始终从 .env 加载 api_key = _load_api_key() if api_key: @@ -1263,14 +1397,6 @@ def load_config(self): self.update_output_dir_controls() - if os.path.exists('whisper/param.txt'): - with open('whisper/param.txt', 'r', encoding='utf-8') as f: - self.param_whisper.setPlainText(f.read()) - - if os.path.exists('whisper-faster/param.txt'): - with open('whisper-faster/param.txt', 'r', encoding='utf-8') as f: - self.param_whisper_faster.setPlainText(f.read()) - if os.path.exists('llama/param.txt'): with open('llama/param.txt', 'r', encoding='utf-8') as f: self.param_llama.setPlainText(f.read()) @@ -1744,48 +1870,89 @@ def initDictTab(self): def initSettingsTab(self): self.settings_tab = Widget("Settings", self) self.settings_layout = self.settings_tab.vBoxLayout - - # Whisper Section - self.settings_whisper_label = BodyLabel(_("settings_whisper_label")) - self.settings_layout.addWidget(self.settings_whisper_label) - self.whisper_file = QComboBox() - whisper_lst = [i for i in os.listdir('whisper') if i.startswith('ggml') and i.endswith('bin') and not 'silero' in i] + [i for i in os.listdir('whisper-faster') if i.startswith('faster-whisper')] + ['不进行听写'] - self.whisper_file.addItems(whisper_lst) - self.settings_layout.addWidget(self.whisper_file) + # ── ASRLabs 听写引擎 ── + self.settings_asr_engine_label = BodyLabel(_("settings_asr_engine_label")) + self.settings_layout.addWidget(self.settings_asr_engine_label) + self.asr_engine_combo = QComboBox() + # 首项为"不进行听写",后续从 asrlabs 动态填充 + self.asr_engine_combo.addItem('不进行听写', userData='') + self.settings_layout.addWidget(self.asr_engine_combo) + + self.settings_asr_model_label = BodyLabel(_("settings_asr_model_label")) + self.settings_layout.addWidget(self.settings_asr_model_label) + self.asr_model_combo = QComboBox() + self.settings_layout.addWidget(self.asr_model_combo) + + # 设备 + 计算精度 + asr_hw_layout = QHBoxLayout() + self.settings_asr_device_label = BodyLabel(_("settings_asr_device_label")) + asr_hw_layout.addWidget(self.settings_asr_device_label) + self.asr_device_combo = QComboBox() + self.asr_device_combo.addItems(['auto', 'cuda', 'cpu', 'vulkan']) + asr_hw_layout.addWidget(self.asr_device_combo) + asr_hw_layout.addSpacing(20) + self.settings_asr_compute_type_label = BodyLabel(_("settings_asr_compute_type_label")) + asr_hw_layout.addWidget(self.settings_asr_compute_type_label) + self.asr_compute_type_combo = QComboBox() + self.asr_compute_type_combo.addItems(['float16', 'int8', 'float32']) + asr_hw_layout.addWidget(self.asr_compute_type_combo) + asr_hw_layout.addStretch() + self.settings_layout.addLayout(asr_hw_layout) + + # 额外参数 + self.settings_asr_extra_label = BodyLabel(_("settings_asr_extra_label")) + self.settings_layout.addWidget(self.settings_asr_extra_label) + self.asr_extra_edit = QTextEdit() + self.asr_extra_edit.setPlaceholderText(_("settings_asr_extra_placeholder")) + self.asr_extra_edit.setMaximumHeight(60) + self.settings_layout.addWidget(self.asr_extra_edit) + + # ─ ASRLabs 对齐引擎 ─ + self.settings_align_engine_label = BodyLabel(_("settings_align_engine_label")) + self.settings_layout.addWidget(self.settings_align_engine_label) + self.align_engine_combo = QComboBox() + self.settings_layout.addWidget(self.align_engine_combo) + + self.settings_align_model_label = BodyLabel(_("settings_align_model_label")) + self.settings_layout.addWidget(self.settings_align_model_label) + self.align_model_combo = QComboBox() + self.settings_layout.addWidget(self.align_model_combo) + + self.settings_align_device_label = BodyLabel(_("settings_align_device_label")) + self.settings_layout.addWidget(self.settings_align_device_label) + self.align_device_combo = QComboBox() + self.align_device_combo.addItems(['auto', 'cuda', 'cpu', 'vulkan']) + self.settings_layout.addWidget(self.align_device_combo) + + self.settings_align_extra_label = BodyLabel(_("settings_align_extra_label")) + self.settings_layout.addWidget(self.settings_align_extra_label) + self.align_extra_edit = QTextEdit() + self.align_extra_edit.setPlaceholderText(_("settings_align_extra_placeholder")) + self.align_extra_edit.setMaximumHeight(60) + self.settings_layout.addWidget(self.align_extra_edit) + + # ─ 听写语言 ─ self.settings_lang_label = BodyLabel(_("settings_lang_label")) self.settings_layout.addWidget(self.settings_lang_label) self.input_lang = QComboBox() self.input_lang.addItems(['ja','en','ko','ru','fr','zh']) self.settings_layout.addWidget(self.input_lang) - self.settings_whisper_param_label = BodyLabel(_("settings_whisper_param_label")) - self.settings_layout.addWidget(self.settings_whisper_param_label) - self.param_whisper = QTextEdit() - self.param_whisper.setPlaceholderText(_("settings_whisper_param_placeholder")) - self.settings_layout.addWidget(self.param_whisper) - - self.settings_faster_param_label = BodyLabel(_("settings_faster_param_label")) - self.settings_layout.addWidget(self.settings_faster_param_label) - self.param_whisper_faster = QTextEdit() - self.param_whisper_faster.setPlaceholderText(_("settings_faster_param_placeholder")) - self.settings_layout.addWidget(self.param_whisper_faster) - + # ─ 按钮 ─ button_layout = QHBoxLayout() - - self.open_whisper_dir = QPushButton(_("settings_open_whisper_btn")) - self.open_whisper_dir.clicked.connect(lambda: open_path(os.path.join(os.getcwd(),'whisper'))) - self.open_faster_dir = QPushButton(_("settings_open_faster_btn")) - self.open_faster_dir.clicked.connect(lambda: open_path(os.path.join(os.getcwd(),'whisper-faster'))) - button_layout.addWidget(self.open_whisper_dir) - button_layout.addWidget(self.open_faster_dir) - - self.refresh_speech_models_button = QPushButton(_("settings_refresh_speech_btn")) - self.refresh_speech_models_button.clicked.connect(self.refresh_speech_model_lists) - button_layout.addWidget(self.refresh_speech_models_button) + self.open_transcribe_dir_btn = QPushButton(_("settings_open_transcribe_dir_btn")) + self.open_transcribe_dir_btn.clicked.connect(lambda: open_path(os.path.join(os.getcwd(),'models','transcribe'))) + button_layout.addWidget(self.open_transcribe_dir_btn) + self.open_align_dir_btn = QPushButton(_("settings_open_align_dir_btn")) + self.open_align_dir_btn.clicked.connect(lambda: open_path(os.path.join(os.getcwd(),'models','align'))) + button_layout.addWidget(self.open_align_dir_btn) + self.refresh_asr_btn = QPushButton(_("settings_refresh_asr_btn")) + self.refresh_asr_btn.clicked.connect(self.refresh_asr_engine_lists) + button_layout.addWidget(self.refresh_asr_btn) self.settings_layout.addLayout(button_layout) - # UVR models move into speech settings for consistency + # UVR models self.settings_uvr_label = BodyLabel(_("settings_uvr_label")) self.settings_layout.addWidget(self.settings_uvr_label) self.uvr_file = QComboBox() @@ -1798,6 +1965,9 @@ def initSettingsTab(self): self.addSubInterface(self.settings_tab, FluentIcon.SETTING, _("tab_settings"), NavigationItemPosition.TOP) + # 引擎选择变更时动态更新对齐引擎下拉框 + self.asr_engine_combo.currentIndexChanged.connect(self.on_asr_engine_changed) + # Sync transcription language between IO tab and Settings tab def sync_transcription_to_settings(idx): self.input_lang.blockSignals(True) @@ -2648,39 +2818,63 @@ def audiosynth(self): self.finished.emit() - def _process_single_audio(self, wav_file, whisper_file, language, param_whisper, param_whisper_faster, json_path, start_named_proc, stop_named_proc): - """处理单个音频文件的听写""" + def _process_single_audio(self, wav_file, language, json_path, start_named_proc, stop_named_proc): + """处理单个音频文件的听写 + + 改造后通过 ASRLabs bridge 调用 asrlabs transcribe + align, + 再将结果转为 GalTransl JSON 格式。 + """ base_path = wav_file[:-4] # 去掉 .wav - if whisper_file.startswith('ggml'): - self.msg_queue.put("detail", param_whisper) - whisper_proc, _unused = start_named_proc( - 'whisper', - [param.replace('$whisper_file',whisper_file).replace('$input_file',base_path).replace('$language',language) for param in param_whisper.split()] - ) - elif whisper_file.startswith('faster-whisper'): - self.msg_queue.put("detail", param_whisper_faster) - whisper_proc, _unused = start_named_proc( - 'whisper_faster', - [param.replace('$whisper_file',whisper_file[15:]).replace('$input_file',base_path).replace('$language',language).replace('$output_dir',os.path.dirname(wav_file)) for param in param_whisper_faster.split()] - ) - else: + # 从 master 获取 ASRLabs 配置 + asr_engine = self.master.asr_engine_combo.currentData() or '' + if not asr_engine: return - whisper_proc.wait() - if whisper_file.startswith('ggml'): - stop_named_proc('whisper') - else: - stop_named_proc('whisper_faster') - # 转换该片段的SRT到JSON,完成后清理中间文件 - intermediate_srt = base_path + '.srt' - make_prompt(intermediate_srt, json_path) - # 清理 whisper 产出的中间 .16k.srt 文件 - if intermediate_srt.endswith('.16k.srt') and os.path.exists(intermediate_srt): - try: - os.remove(intermediate_srt) - except Exception: - pass + asr_model = self.master.asr_model_combo.currentData() or '' + asr_device = self.master.asr_device_combo.currentText() + asr_compute_type = self.master.asr_compute_type_combo.currentText() + asr_extra = self.master.asr_extra_edit.toPlainText() + align_engine = self.master.align_engine_combo.currentData() or 'none' + align_model = self.master.align_model_combo.currentData() or '' + align_device = self.master.align_device_combo.currentText() + align_extra = self.master.align_extra_edit.toPlainText() + + output_dir = os.path.abspath(os.path.dirname(json_path)) + output_name = os.path.basename(json_path).replace('.json', '') + + self._emit_status(_("status_asrlabs_transcribing", engine=asr_engine, audio=os.path.basename(wav_file))) + + try: + galtransl_json = asrlabs_bridge.run_transcribe_and_align( + audio_path=wav_file, + engine=asr_engine, + model_path=asr_model, + language=language, + device=asr_device, + compute_type=asr_compute_type, + aligner=align_engine, + align_model_path=align_model, + align_device=align_device, + transcribe_extra=asr_extra, + align_extra=align_extra, + split_logic='punct', + max_chars=40, + output_dir=output_dir, + output_name=output_name, + msg_queue=self.msg_queue, + stop_event=self._stop_event, + ) + + # 将 galtransl JSON 复制/重命名为期望的 json_path + if galtransl_json != json_path: + shutil.copy(galtransl_json, json_path) + + self._emit_status(_("status_asrlabs_done", output=os.path.basename(json_path))) + + except Exception as e: + self._emit_status(_("status_asrlabs_error", error=e)) + raise def _get_audio_duration(self, audio_file): """获取音频文件时长(秒)""" @@ -2816,7 +3010,7 @@ def run(self): self.save_config() input_files = self.master.input_files_list.toPlainText() - whisper_file = self.master.whisper_file.currentText() + asr_engine = self.master.asr_engine_combo.currentData() or '' translator = self.master.translator_group.currentText() language = self.master.input_lang.currentText() sakura_file = self.master.sakura_file.currentText() @@ -2825,8 +3019,6 @@ def run(self): before_dict = self.master.before_dict.toPlainText() gpt_dict = self.master.gpt_dict.toPlainText() after_dict = self.master.after_dict.toPlainText() - param_whisper = self.master.param_whisper.toPlainText() - param_whisper_faster = self.master.param_whisper_faster.toPlainText() param_llama = self.master.param_llama.toPlainText() output_format = self.master.output_format.currentData() output_dir = self.master.output_dir_edit.text().strip() or self.master.default_output_dir() @@ -2834,12 +3026,6 @@ def run(self): enable_segment = self.master.enable_segment_checkbox.isChecked() segment_duration_minutes = self.master.segment_duration_spin.value() if enable_segment else 0 - with open('whisper/param.txt', 'w', encoding='utf-8') as f: - f.write(param_whisper) - - with open('whisper-faster/param.txt', 'w', encoding='utf-8') as f: - f.write(param_whisper_faster) - with open('llama/param.txt', 'w', encoding='utf-8') as f: f.write(param_llama) @@ -3034,7 +3220,7 @@ def stop_named_proc(proc_name): ) else: # 音视频输入:提取音频 → 听写(如果已有srt则跳过) - if whisper_file == '不进行听写': + if not asr_engine: self._emit_status(_("status_no_transcribe_skip")) continue @@ -3121,27 +3307,12 @@ def stop_named_proc(proc_name): segment_base = segment_file[:-4] # 去掉 .wav segment_name = os.path.basename(segment_base) - if whisper_file.startswith('ggml'): - whisper_proc, _unused = start_named_proc( - 'whisper', - [param.replace('$whisper_file',whisper_file).replace('$input_file',segment_base).replace('$language',language) for param in param_whisper.split()] - ) - elif whisper_file.startswith('faster-whisper'): - whisper_proc, _unused = start_named_proc( - 'whisper_faster', - [param.replace('$whisper_file',whisper_file[15:]).replace('$input_file',segment_base).replace('$language',language).replace('$output_dir',segment_dir) for param in param_whisper_faster.split()] - ) - else: - break - whisper_proc.wait() - if whisper_file.startswith('ggml'): - stop_named_proc('whisper') - else: - stop_named_proc('whisper_faster') - - # 转换该片段的SRT到JSON + # ASRLabs 听写+对齐 segment_json = os.path.join(transcribed_dir, segment_name + '.json') - make_prompt(segment_base + '.srt', segment_json) + self._process_single_audio( + segment_file, language, + segment_json, start_named_proc, stop_named_proc, + ) # 立即提交该分段进行翻译 if need_translate: @@ -3173,7 +3344,7 @@ def stop_named_proc(proc_name): else: # 正常流程(未启用分段) self._emit_status(_("status_asr_in_progress")) - self._process_single_audio(wav_file, whisper_file, language, param_whisper, param_whisper_faster, json_path, start_named_proc, stop_named_proc) + self._process_single_audio(wav_file, language, json_path, start_named_proc, stop_named_proc) # 生成原文 SRT/LRC 输出 if output_format == '原文SRT' or output_format == '双语SRT': diff --git a/asrlabs_bridge.py b/asrlabs_bridge.py new file mode 100644 index 00000000..38ab7bba --- /dev/null +++ b/asrlabs_bridge.py @@ -0,0 +1,383 @@ +"""ASRLabs CLI 适配层 + +封装 asrlabs transcribe / align / list 命令的子进程调用, +提供引擎元数据同步、听写+对齐流程编排、JSON 格式转换等功能。 +""" + +import json +import os +import subprocess +import sys +from pathlib import Path + +# PyInstaller 打包后使用独立 exe,源码运行时使用 python -m asrlabs +_FROZEN = hasattr(sys, '_MEIPASS') +_ASRLABS_CMD = ['asrlabs/asrlabs'] if _FROZEN else [sys.executable, '-m', 'asrlabs'] + +# 引擎元数据缓存(避免每次调用都启动子进程) +_transcriber_cache: list[dict] | None = None +_aligner_cache: list[dict] | None = None + + +def _run_asrlabs_json(args: list[str], timeout: int = 30) -> str: + """运行 asrlabs 命令并返回 stdout 字符串 + + Args: + args: asrlabs 子命令及参数列表 + timeout: 超时秒数 + + Returns: + stdout 输出文本 + """ + creationflags = 0x08000000 if os.name == 'nt' else 0 + proc = subprocess.run( + [*_ASRLABS_CMD, *args], + capture_output=True, text=True, + timeout=timeout, creationflags=creationflags, + ) + if proc.returncode != 0: + raise RuntimeError(f"asrlabs 命令失败 (exit={proc.returncode}): {proc.stderr}") + return proc.stdout + + +def fetch_engine_metadata(force_refresh: bool = False) -> tuple[list[dict], list[dict]]: + """从 asrlabs list --json 获取引擎元数据 + + 首次调用后缓存结果,后续调用直接返回缓存。 + + Args: + force_refresh: True 时强制重新获取 + + Returns: + (transcribers, aligners) 元组 + transcribers: [{"name", "display_name", "supports_timestamps", + "recommended_aligner", "supported_devices", "supports_initial_prompt"}, ...] + aligners: [{"name", "display_name"}, ...] + """ + global _transcriber_cache, _aligner_cache + + if _transcriber_cache is None or force_refresh: + raw = _run_asrlabs_json(['list', 'transcribers', '--json']) + _transcriber_cache = json.loads(raw) + + if _aligner_cache is None or force_refresh: + raw = _run_asrlabs_json(['list', 'aligners', '--json']) + _aligner_cache = json.loads(raw) + + return _transcriber_cache, _aligner_cache + + +def get_transcriber_meta(engine_name: str) -> dict | None: + """获取指定听写引擎的元数据 + + Args: + engine_name: 引擎名,如 "faster-whisper" + + Returns: + 元数据字典,找不到返回 None + """ + transcribers, _ = fetch_engine_metadata() + for t in transcribers: + if t['name'] == engine_name: + return t + return None + + +def list_transcribe_models() -> list[str]: + """扫描 models/transcribe/ 目录,返回可用模型文件夹列表 + + Returns: + 模型文件夹名列表,如 ["faster-whisper-large-v3", "Qwen3-ASR-1.7B"] + """ + models_dir = Path('models/transcribe') + if not models_dir.exists(): + return [] + return sorted([ + d.name for d in models_dir.iterdir() + if d.is_dir() and not d.name.startswith('.') + ]) + + +def list_align_models() -> list[str]: + """扫描 models/align/ 目录,返回可用模型文件夹列表 + + Returns: + 模型文件夹名列表 + """ + models_dir = Path('models/align') + if not models_dir.exists(): + return [] + return sorted([ + d.name for d in models_dir.iterdir() + if d.is_dir() and not d.name.startswith('.') + ]) + + +def transcribe( + audio_path: str, + engine: str, + model_path: str, + language: str, + device: str, + compute_type: str, + output_dir: str, + output_name: str, + extra_args: str, + msg_queue, + stop_event, +) -> str: + """调用 asrlabs transcribe,返回输出的 JSON 文件路径 + + Args: + audio_path: 音频文件路径 + engine: 引擎名(如 "faster-whisper") + model_path: 模型路径(本地路径或 HF ID,空串使用引擎默认) + language: 语言代码(ISO 639-1,如 "ja") + device: 设备类型("auto"/"cuda"/"cpu"/"vulkan") + compute_type: 计算精度("float16"/"int8"/"float32") + output_dir: 输出目录 + output_name: 输出文件名 stem(不含扩展名) + extra_args: 额外命令行参数(空格分隔的字符串) + msg_queue: UIMessageQueue 实例,用于日志转发 + stop_event: threading.Event,用于取消检测 + + Returns: + 输出的 JSON 文件路径 + """ + cmd = [*_ASRLABS_CMD, 'transcribe', audio_path, + '-m', engine, + '-l', language, + '--device', device, + '--compute-type', compute_type, + '-d', output_dir, + '-o', output_name] + + if model_path: + cmd.extend(['--model-path', model_path]) + + if extra_args.strip(): + cmd.extend(extra_args.split()) + + return _run_with_log(cmd, msg_queue, stop_event, output_dir, output_name) + + +def align( + audio_path: str, + transcribe_json_path: str, + aligner: str, + model_path: str, + device: str, + output_dir: str, + output_name: str, + extra_args: str, + msg_queue, + stop_event, +) -> str: + """调用 asrlabs align,返回对齐后的 JSON 文件路径 + + Args: + audio_path: 音频文件路径 + transcribe_json_path: 听写产出的 JSON 路径 + aligner: 对齐器名称(如 "qwen3_align") + model_path: 对齐模型路径(空串使用默认) + device: 设备类型 + output_dir: 输出目录 + output_name: 输出文件名 stem(与听写相同则覆盖) + extra_args: 额外命令行参数 + msg_queue: UIMessageQueue 实例 + stop_event: threading.Event + + Returns: + 对齐后的 JSON 文件路径 + """ + cmd = [*_ASRLABS_CMD, 'align', audio_path, transcribe_json_path, + '--aligner', aligner, + '--device', device, + '-d', output_dir, + '-o', output_name] + + if model_path: + cmd.extend(['--model-path', model_path]) + + if extra_args.strip(): + cmd.extend(extra_args.split()) + + return _run_with_log(cmd, msg_queue, stop_event, output_dir, output_name) + + +def _run_with_log(cmd, msg_queue, stop_event, output_dir, output_name) -> str: + """运行 asrlabs 子进程,转发日志到 msg_queue,返回 JSON 路径 + + 主要逻辑: + 1. subprocess.Popen 启动子进程 + 2. 逐行读取 stdout/stderr,转发到 UIMessageQueue + 3. 检测 stop_event,提前终止子进程 + 4. 等待子进程结束,校验返回码 + 5. 返回 /.json 路径 + """ + # 确保输出目录存在 + os.makedirs(output_dir, exist_ok=True) + + creationflags = 0x08000000 if os.name == 'nt' else 0 + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, creationflags=creationflags, bufsize=1, + ) + + # 逐行读取输出并转发到消息队列 + for line in iter(proc.stdout.readline, ''): + if stop_event.is_set(): + proc.terminate() + try: + proc.wait(timeout=5) + except Exception: + proc.kill() + break + + cleaned = line.rstrip('\n\r') + if cleaned.strip(): + msg_queue.put("detail", f"[ASRLabs] {cleaned}") + + proc.stdout.close() + retcode = proc.wait() + + if retcode != 0: + raise RuntimeError(f"asrlabs 子进程失败 (exit={retcode})") + + json_path = os.path.join(output_dir, output_name + '.json') + if not os.path.exists(json_path): + raise FileNotFoundError(f"asrlabs 未生成预期输出文件: {json_path}") + + return json_path + + +def convert_to_galtransl_json(asrlabs_json_path: str, output_path: str): + """将 ASRLabs JSON 转为 GalTransl JSON 格式 + + ASRLabs 格式: + {"text": "...", "segments": [{"text": "...", "start": 0.5, "end": 2.3, ...}], ...} + + GalTransl 格式: + [{"start": 0.5, "end": 2.3, "message": "..."}, ...] + + Args: + asrlabs_json_path: ASRLabs 产出的 JSON 文件路径 + output_path: 转换后的 GalTransl JSON 文件路径 + """ + with open(asrlabs_json_path, encoding='utf-8') as f: + data = json.load(f) + + galtransl_data = [] + for seg in data.get('segments', []): + text = seg.get('text', '').strip() + if not text: + continue + galtransl_data.append({ + 'start': seg.get('start', 0.0), + 'end': seg.get('end', 0.0), + 'message': text, + }) + + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(galtransl_data, f, ensure_ascii=False, indent=4) + + +def convert_json( + json_path: str, + logic: str, + max_chars: int, + msg_queue, + stop_event, +) -> str: + """调用 asrlabs convert -f json 对 JSON 进行分句后处理(原地覆盖) + + 主要逻辑: + 1. 调用 asrlabs convert -f json -l --max-chars + 2. asrlabs 读取 JSON → apply_split_logic → 保存为 JSON(原地覆盖) + 3. 返回处理后 JSON 路径(与输入相同) + + Args: + json_path: 待后处理的 ASRLabs JSON 文件路径 + logic: 分句逻辑 "punct" 或 "pysbd" + max_chars: 单条最大字符数 + msg_queue: UIMessageQueue 实例 + stop_event: threading.Event + + Returns: + 处理后的 JSON 文件路径(与输入相同,原地覆盖) + """ + output_dir = os.path.dirname(json_path) + output_name = os.path.basename(json_path).replace('.json', '') + + cmd = [*_ASRLABS_CMD, 'convert', json_path, + '-f', 'json', + '-l', logic, + '--max-chars', str(max_chars), + '-d', output_dir, + '-o', output_name] + + return _run_with_log(cmd, msg_queue, stop_event, output_dir, output_name) + + +def run_transcribe_and_align( + audio_path: str, + engine: str, + model_path: str, + language: str, + device: str, + compute_type: str, + aligner: str, + align_model_path: str, + align_device: str, + transcribe_extra: str, + align_extra: str, + split_logic: str, + max_chars: int, + output_dir: str, + output_name: str, + msg_queue, + stop_event, +) -> str: + """完整的听写+对齐+后处理流程,返回 GalTransl JSON 路径 + + 主要逻辑: + 1. 调用 asrlabs transcribe → 产出 ASRLabs JSON + 2. 如果 aligner 不为 "none" → 调用 asrlabs align(覆盖同一 JSON) + 3. 调用 asrlabs convert -f json → 分句后处理(原地覆盖,仅切分 segments 不改格式) + 4. 将后处理后的 JSON 转为 GalTransl 格式 + + Args: + aligner: 对齐器名称,"none" 表示跳过对齐 + split_logic: 分句逻辑 "punct" 或 "pysbd" + max_chars: 单条字幕最大字符数(0=不限制) + 其余参数同 transcribe() / align() + + Returns: + GalTransl 格式 JSON 文件路径 + """ + # 步骤 1:听写 + asrlabs_json = transcribe( + audio_path, engine, model_path, language, device, compute_type, + output_dir, output_name, transcribe_extra, msg_queue, stop_event, + ) + + # 步骤 2:对齐(可选) + if aligner and aligner != 'none': + asrlabs_json = align( + audio_path, asrlabs_json, aligner, align_model_path, + align_device, output_dir, output_name, + align_extra, msg_queue, stop_event, + ) + + # 步骤 3:分句后处理(asrlabs convert -f json,原地覆盖) + if max_chars > 0 or split_logic: + asrlabs_json = convert_json( + asrlabs_json, split_logic, max_chars, msg_queue, stop_event, + ) + + # 步骤 4:转为 GalTransl 格式 + galtransl_json = os.path.join(output_dir, output_name + '.galtransl.json') + convert_to_galtransl_json(asrlabs_json, galtransl_json) + + return galtransl_json diff --git a/compat.py b/compat.py new file mode 100644 index 00000000..2b859ecb --- /dev/null +++ b/compat.py @@ -0,0 +1,159 @@ +import os +import shutil +from pathlib import Path + + +def migrate_config_txt(config_path: str = 'config.txt') -> dict: + """从旧 config.txt 迁移到 gui_settings.yaml 格式 + + config.txt 行结构(按行号): + 0: whisper_file 1: translator 2: language + 3: gpt_token 4: gpt_address 5: gpt_model + 6: sakura_file 7: sakura_mode 8: proxy_address + 9: uvr_file 10: output_format 11: subtitle_font + 12: output_dir 13: use_input_dir 14: max_concurrent + 15: enable_segment 16: segment_duration 17: change_prompt_mode + + Returns: + gui_settings 字典(与 gui_settings.yaml 结构一致) + """ + with open(config_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + + # API Key 写入 .env(与旧版行为一致) + gpt_token = lines[3].strip() if len(lines) > 3 else '' + _save_api_key(gpt_token) + + return { + 'whisper_file': lines[0].strip(), + 'translator': lines[1].strip(), + 'language': lines[2].strip(), + 'gpt_address': lines[4].strip(), + 'gpt_model': lines[5].strip(), + 'sakura_file': lines[6].strip(), + 'sakura_mode': lines[7].strip(), + 'proxy_address': lines[8].strip(), + 'uvr_file': lines[9].strip(), + 'output_format': lines[10].strip(), + 'subtitle_font': lines[11].strip() if len(lines) > 11 else "", + 'output_dir': lines[12].strip() if len(lines) > 12 else '', + 'use_input_dir': (lines[13].strip().lower() == 'true') if len(lines) > 13 else False, + 'max_concurrent': int(lines[14].strip()) if len(lines) > 14 else 1, + 'enable_segment': (lines[15].strip().lower() == 'true') if len(lines) > 15 else False, + 'segment_duration': int(lines[16].strip()) if len(lines) > 16 else 10, + 'change_prompt_mode': lines[17].strip() if len(lines) > 17 else '不修改', + } + + +def migrate_old_whisper_models( + old_whisper_file: str, + log_fn=None, +) -> dict: + """将旧版 whisper/whisper-faster 模型迁移到 models/transcribe/,并清理旧目录 + + 迁移规则: + - whisper-faster/faster-whisper-* 目录 → models/transcribe/(CTranslate2 格式,兼容) + - whisper/ggml-*.bin → 跳过并警告(whisper.cpp ggml 格式,与 ASRLabs stable-ts 后端不兼容) + - whisper/ggml-silero-*.bin → 跳过(VAD 模型,ASRLabs 内置 Silero VAD) + - 迁移完成后删除 whisper/ 和 whisper-faster/ 目录 + + Args: + old_whisper_file: 旧配置中的 whisper_file 值(如 "faster-whisper-large-v3") + log_fn: 日志回调函数,签名为 log_fn(str);为 None 时静默 + + Returns: + {'asr_engine': str, 'asr_model': str} + """ + def _log(msg): + if log_fn: + log_fn(msg) + + result = {'asr_engine': '', 'asr_model': ''} + transcribe_dir = Path('models/transcribe') + transcribe_dir.mkdir(parents=True, exist_ok=True) + + # 1. 迁移 whisper-faster/ 下的 faster-whisper-* 模型目录 + faster_dir = Path('whisper-faster') + if faster_dir.exists(): + for item in faster_dir.iterdir(): + if item.is_dir() and item.name.startswith('faster-whisper'): + target = transcribe_dir / item.name + if not target.exists(): + try: + shutil.move(str(item), str(target)) + _log(f"[INFO] 模型迁移: {item.name} -> models/transcribe/") + except Exception as e: + _log(f"[WARN] 模型迁移失败: {item.name}: {e}") + + # 2. 检测 whisper/ 下的 ggml 模型(不兼容,跳过) + whisper_dir = Path('whisper') + if whisper_dir.exists(): + ggml_models = [m for m in whisper_dir.glob('ggml-*.bin') if 'silero' not in m.name] + if ggml_models: + names = ", ".join(m.name for m in ggml_models) + _log(f"[WARN] ggml 格式模型({names})与 ASRLabs 不兼容,已跳过。" + f"请在 models/transcribe/ 中放置 stable-ts 或 HuggingFace 格式的 Whisper 模型。") + + # 3. 根据旧配置映射到新引擎 + mapped = map_whisper_to_asr(old_whisper_file) + result['asr_engine'] = mapped['asr_engine'] + result['asr_model'] = mapped['asr_model'] + # 如果模型目录已迁移到 models/transcribe/,修正路径 + if result['asr_model'] and result['asr_engine'] == 'faster-whisper': + model_name = os.path.basename(result['asr_model']) + migrated_path = str(transcribe_dir / model_name) + if os.path.exists(migrated_path): + result['asr_model'] = migrated_path + + # 4. 删除旧目录 + for old_dir in [Path('whisper'), Path('whisper-faster')]: + if old_dir.exists(): + try: + shutil.rmtree(old_dir) + _log(f"[INFO] 已清理旧目录: {old_dir}/") + except Exception as e: + _log(f"[WARN] 清理旧目录失败 {old_dir}/: {e}") + + return result + + +def migrate_output_format(old_fmt: str) -> str: + """旧格式名 → 新格式名映射 + + '中文SRT' → '目标SRT' + '中文LRC' → '目标LRC' + 其余值原样返回 + """ + _FORMAT_MIGRATION = {'中文SRT': '目标SRT', '中文LRC': '目标LRC'} + return _FORMAT_MIGRATION.get(old_fmt, old_fmt) + + +def map_whisper_to_asr(old_whisper_file: str) -> dict: + """将旧 whisper_file 字符串映射为 ASRLabs 引擎配置 + + Args: + old_whisper_file: 旧配置值,如 "faster-whisper-large-v3" 或 "ggml-medium.bin" + + Returns: + {'asr_engine': str, 'asr_model': str} + - faster-whisper-* → asr_engine='faster-whisper', asr_model=旧完整路径 + - ggml-* → asr_engine='whisper', asr_model=''(格式不兼容,留空) + - 其他/空 → 均为空串 + """ + if old_whisper_file.startswith('faster-whisper'): + return { + 'asr_engine': 'faster-whisper', + 'asr_model': os.path.join('whisper-faster', old_whisper_file), + } + elif old_whisper_file.startswith('ggml'): + return { + 'asr_engine': 'whisper', + 'asr_model': '', # ggml 不兼容,留空让用户自行配置 + } + return {'asr_engine': '', 'asr_model': ''} + + +def _save_api_key(api_key: str) -> None: + """将 API Key 写入 .env 文件(与 app.py 中的逻辑一致)""" + with open('.env', 'w', encoding='utf-8') as f: + f.write(f'VOICETRANSL_API_KEY={api_key}\n') diff --git a/i18n.py b/i18n.py index 43c7497a..7573a3b1 100644 --- a/i18n.py +++ b/i18n.py @@ -284,6 +284,30 @@ "status_first_run_init": "[INFO] 首次运行,正在初始化项目配置文件...", "status_worker_not_exited": "[WARN] 翻译工作线程 {name} 未能在停止信号后退出", "status_translate_proc_ended": "[INFO] [进程{idx}] 翻译进程已结束 (exit={retcode})", + + # === ASRLabs Integration === + "settings_asr_engine_label": "🗣️ 听写引擎", + "settings_asr_model_label": "📦 听写模型路径(本地目录或 HuggingFace ID)", + "settings_asr_device_label": "🖥️ 设备", + "settings_asr_compute_type_label": "⚙️ 计算精度", + "settings_asr_extra_label": "🔧 额外参数(空格分隔,透传给 asrlabs transcribe)", + "settings_asr_extra_placeholder": "如:--beam_size 5,留空使用默认", + "settings_align_engine_label": "⏱️ 对齐引擎", + "settings_align_no_align": "不进行对齐(使用内置时间戳)", + "settings_align_model_label": "📦 对齐模型路径", + "settings_align_device_label": "🖥️ 对齐设备", + "settings_align_extra_label": "🔧 对齐额外参数", + "settings_align_extra_placeholder": "如:--beam_size 3,留空使用默认", + "settings_open_transcribe_dir_btn": "📁 打开听写模型目录", + "settings_open_align_dir_btn": "📁 打开对齐模型目录", + "settings_refresh_asr_btn": "🔄 刷新引擎与模型列表", + "settings_asr_no_timestamps_hint": "⚠️ 当前引擎无内置时间戳,必须选择对齐器", + "status_asrlabs_transcribing": "[INFO] [ASRLabs] 正在听写:{engine} -> {audio}", + "status_asrlabs_aligning": "[INFO] [ASRLabs] 正在对齐:{aligner} -> {audio}", + "status_asrlabs_done": "[INFO] [ASRLabs] 听写+对齐完成:{output}", + "status_asrlabs_error": "[ERROR] [ASRLabs] 处理失败: {error}", + "status_asrlabs_no_engine": "[ERROR] 未选择听写引擎,请在语音模型设置中选择。", + "status_asrlabs_metadata_error": "[ERROR] 获取 ASRLabs 引擎列表失败: {error}", }, "en": { # === Window & Tray === @@ -568,6 +592,30 @@ "status_first_run_init": "[INFO] First run, initializing project config file...", "status_worker_not_exited": "[WARN] Translation worker thread {name} did not exit after stop signal", "status_translate_proc_ended": "[INFO] [Worker {idx}] Translation process ended (exit={retcode})", + + # === ASRLabs Integration === + "settings_asr_engine_label": "🗣️ Transcription Engine", + "settings_asr_model_label": "📦 Model Path (local directory or HuggingFace ID)", + "settings_asr_device_label": "🖥️ Device", + "settings_asr_compute_type_label": "⚙️ Compute Type", + "settings_asr_extra_label": "🔧 Extra Parameters (space-separated, passed to asrlabs transcribe)", + "settings_asr_extra_placeholder": "e.g. --beam_size 5, leave empty for default", + "settings_align_engine_label": "⏱️ Alignment Engine", + "settings_align_no_align": "No alignment (use built-in timestamps)", + "settings_align_model_label": "📦 Alignment Model Path", + "settings_align_device_label": "🖥️ Alignment Device", + "settings_align_extra_label": "🔧 Alignment Extra Parameters", + "settings_align_extra_placeholder": "e.g. --beam_size 3, leave empty for default", + "settings_open_transcribe_dir_btn": "📁 Open Transcribe Model Directory", + "settings_open_align_dir_btn": "📁 Open Align Model Directory", + "settings_refresh_asr_btn": "🔄 Refresh Engine & Model List", + "settings_asr_no_timestamps_hint": "⚠️ Current engine has no built-in timestamps, an aligner is required", + "status_asrlabs_transcribing": "[INFO] [ASRLabs] Transcribing: {engine} -> {audio}", + "status_asrlabs_aligning": "[INFO] [ASRLabs] Aligning: {aligner} -> {audio}", + "status_asrlabs_done": "[INFO] [ASRLabs] Transcription+alignment complete: {output}", + "status_asrlabs_error": "[ERROR] [ASRLabs] Processing failed: {error}", + "status_asrlabs_no_engine": "[ERROR] No transcription engine selected, please configure in Speech Model settings.", + "status_asrlabs_metadata_error": "[ERROR] Failed to fetch ASRLabs engine list: {error}", }, "ja": { # === Window & Tray === @@ -851,6 +899,30 @@ "status_first_run_init": "[INFO] 初回実行、プロジェクト設定ファイルを初期化中...", "status_worker_not_exited": "[WARN] 翻訳ワーカースレッド {name} が停止信号後に終了しませんでした", "status_translate_proc_ended": "[INFO] [ワーカー{idx}] 翻訳プロセスが終了しました (exit={retcode})", + + # === ASRLabs Integration === + "settings_asr_engine_label": "🗣️ 文字起こしエンジン", + "settings_asr_model_label": "📦 モデルパス(ローカルディレクトリまたはHuggingFace ID)", + "settings_asr_device_label": "🖥️ デバイス", + "settings_asr_compute_type_label": "⚙️ 計算精度", + "settings_asr_extra_label": "🔧 追加パラメータ(スペース区切り、asrlabs transcribeに透過)", + "settings_asr_extra_placeholder": "例: --beam_size 5、空欄でデフォルト", + "settings_align_engine_label": "⏱️ アライメントエンジン", + "settings_align_no_align": "アライメントなし(内蔵タイムスタンプを使用)", + "settings_align_model_label": "📦 アライメントモデルパス", + "settings_align_device_label": "🖥️ アライメントデバイス", + "settings_align_extra_label": "🔧 アライメント追加パラメータ", + "settings_align_extra_placeholder": "例: --beam_size 3、空欄でデフォルト", + "settings_open_transcribe_dir_btn": "📁 文字起こしモデルディレクトリを開く", + "settings_open_align_dir_btn": "📁 アライメントモデルディレクトリを開く", + "settings_refresh_asr_btn": "🔄 エンジンとモデルリストを更新", + "settings_asr_no_timestamps_hint": "⚠️ 現在のエンジンには内蔵タイムスタンプがありません。アライナーを選択する必要があります。", + "status_asrlabs_transcribing": "[INFO] [ASRLabs] 文字起こし中: {engine} -> {audio}", + "status_asrlabs_aligning": "[INFO] [ASRLabs] アライメント中: {aligner} -> {audio}", + "status_asrlabs_done": "[INFO] [ASRLabs] 文字起こし+アライメント完了: {output}", + "status_asrlabs_error": "[ERROR] [ASRLabs] 処理失敗: {error}", + "status_asrlabs_no_engine": "[ERROR] 文字起こしエンジンが選択されていません。音声モデル設定で構成してください。", + "status_asrlabs_metadata_error": "[ERROR] ASRLabsエンジンリストの取得に失敗: {error}", }, } diff --git a/models/.gitkeep b/models/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/models/.gitkeep @@ -0,0 +1 @@ + diff --git a/requirements-macos.txt b/requirements-macos.txt index 86c58191..87c9879e 100644 --- a/requirements-macos.txt +++ b/requirements-macos.txt @@ -1,3 +1,7 @@ +# ASRLabs +asrlabs[ctc] @ git+https://github.com/MurthiNext/ASRLabs.git + +# VoiceTransl httpx<0.28 opencc packaging @@ -20,14 +24,6 @@ librosa onnx soundfile onnxruntime -opencc -openai -packaging -PyYAML -Requests -tenacity -tiktoken -colorlog openpyxl orjson beautifulsoup4 diff --git a/requirements.txt b/requirements.txt index 05b32052..4622c9e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ +# ASRLabs +asrlabs[ctc] @ git+https://github.com/MurthiNext/ASRLabs.git + +# VoiceTransl httpx<0.28 opencc packaging @@ -19,15 +23,6 @@ torch librosa onnx soundfile -onnxruntime-directml -opencc -openai -packaging -PyYAML -Requests -tenacity -tiktoken -colorlog openpyxl orjson beautifulsoup4 diff --git a/separate/DONT_README.md b/separate/DONT_README.md deleted file mode 100644 index 3021bb91..00000000 --- a/separate/DONT_README.md +++ /dev/null @@ -1 +0,0 @@ -这个文件夹需要存在。 \ No newline at end of file diff --git a/whisper-faster/README.md b/whisper-faster/README.md deleted file mode 100644 index 8fbc1ad9..00000000 --- a/whisper-faster/README.md +++ /dev/null @@ -1,8 +0,0 @@ -## Faster-Whisper - -* 请下载[Faster-Whisper-XXL](https://github.com/Purfview/whisper-standalone-win/releases/tag/Faster-Whisper-XXL)放到这个文件夹,并将`faster-whisper-xxl.exe`更名为`whisper-faster.exe`。 - -``` -whisper-faster.exe -_xxl_data/ -``` diff --git a/whisper-faster/param.txt b/whisper-faster/param.txt deleted file mode 100644 index 9dc4fc06..00000000 --- a/whisper-faster/param.txt +++ /dev/null @@ -1 +0,0 @@ -Whisper-Faster/whisper-faster.exe --beep_off --verbose True --model $whisper_file --model_dir Whisper-Faster --task transcribe --language $language --output_format srt --output_dir $output_dir $input_file.wav --compute_type float16 \ No newline at end of file diff --git a/whisper/README.md b/whisper/README.md deleted file mode 100644 index fb021e05..00000000 --- a/whisper/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# whispercpp 使用说明 - -* 请下载[WhisperCpp](https://github.com/ggml-org/whisper.cpp/releases)放到这个文件夹,确保包含`whisper-cli`可执行文件。 -* 请下载[Seiro-VAD](https://huggingface.co/ggml-org/whisper-vad/tree/main)放到这个文件夹,确保包含`ggml-silero-v5.1.2.bin`文件。 \ No newline at end of file diff --git a/whisper/param.txt b/whisper/param.txt deleted file mode 100644 index e373e3d2..00000000 --- a/whisper/param.txt +++ /dev/null @@ -1 +0,0 @@ -whisper/whisper-cli -m whisper/$whisper_file -osrt -l $language $input_file.wav -of $input_file --vad --vad-model whisper/ggml-silero-v5.1.2.bin \ No newline at end of file