Skip to content

Commit 6900c96

Browse files
committed
feat(v1.6.6): 添加画布拖动、日志显示和MOCR镜像下载
1 parent b99c111 commit 6900c96

16 files changed

Lines changed: 778 additions & 965 deletions

README.md

Lines changed: 575 additions & 507 deletions
Large diffs are not rendered by default.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.6.5
1+
1.6.6

desktop_qt_ui/app_logic.py

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -499,9 +499,28 @@ def start_backend_task(self):
499499
self.logger.warning("一个任务已经在运行中。")
500500
return
501501

502+
# 检查文件列表是否为空
502503
files_to_process = self._resolve_input_files()
503504
if not files_to_process:
504505
self.logger.warning("没有找到有效的图片文件,任务中止")
506+
from PyQt6.QtWidgets import QMessageBox
507+
QMessageBox.warning(
508+
None,
509+
"文件列表为空",
510+
"请先添加要翻译的图片文件!\n\n可以通过以下方式添加:\n• 点击「添加文件」按钮\n• 点击「添加文件夹」按钮\n• 直接拖拽文件到文件列表"
511+
)
512+
return
513+
514+
# 检查输出目录是否合法
515+
output_path = self.config_service.get_config().app.last_output_path
516+
if not output_path or not os.path.isdir(output_path):
517+
self.logger.warning(f"输出目录不合法: {output_path}")
518+
from PyQt6.QtWidgets import QMessageBox
519+
QMessageBox.warning(
520+
None,
521+
"输出目录不合法",
522+
"请先设置有效的输出目录!\n\n可以通过以下方式设置:\n• 点击「浏览...」按钮选择输出目录\n• 直接在输出目录输入框中输入路径"
523+
)
505524
return
506525

507526
self.saved_files_count = 0
@@ -644,20 +663,40 @@ def stop_task(self) -> bool:
644663
self.state_manager.set_translating(False)
645664
self.state_manager.set_status_message("正在停止翻译...")
646665

666+
# 1. 先通知 worker 停止
647667
if self.worker:
648668
self.worker.stop()
669+
670+
# 2. 请求线程退出
649671
self.thread.quit()
650672

651673
# 保存线程引用,避免在等待过程中被清空
652674
thread_ref = self.thread
675+
worker_ref = self.worker
653676

654677
# 在后台等待线程停止,不阻塞UI
655678
from PyQt6.QtCore import QTimer
679+
timeout_counter = [0] # 使用列表以便在闭包中修改
680+
656681
def wait_for_thread():
657682
try:
658683
if thread_ref and not thread_ref.wait(100): # 等待100ms
659-
# 如果还没停止,继续等待
660-
QTimer.singleShot(100, wait_for_thread)
684+
timeout_counter[0] += 100
685+
686+
# 如果超过5秒还没停止,强制终止
687+
if timeout_counter[0] >= 5000:
688+
self.logger.warning("线程5秒内未停止,强制终止...")
689+
try:
690+
thread_ref.terminate()
691+
thread_ref.wait(1000) # 等待1秒
692+
self.logger.info("翻译线程已被强制终止。")
693+
self.state_manager.set_status_message("任务已强制停止")
694+
except Exception as e:
695+
self.logger.error(f"强制终止线程失败: {e}")
696+
self.state_manager.set_status_message("停止失败")
697+
else:
698+
# 继续等待
699+
QTimer.singleShot(100, wait_for_thread)
661700
else:
662701
# 线程已停止
663702
self.logger.info("翻译线程已成功停止。")
@@ -839,9 +878,32 @@ async def _do_processing(self):
839878
'input_folders': input_folders
840879
}
841880

881+
# 确定翻译流程模式
882+
workflow_mode = "正常翻译流程"
883+
workflow_tip = ""
884+
cli_config = self.config_dict.get('cli', {})
885+
if cli_config.get('generate_and_export', False):
886+
workflow_mode = "导出翻译"
887+
workflow_tip = "💡 提示:导出翻译后,可在 manga_translator_work/translations/ 目录查看 图片名_translated.txt 文件"
888+
elif cli_config.get('template', False):
889+
workflow_mode = "导出原文"
890+
workflow_tip = "💡 提示:导出原文后,可在 manga_translator_work/originals/ 目录手动翻译 图片名_original.txt 文件,然后使用「导入翻译并渲染」模式"
891+
elif cli_config.get('load_text', False):
892+
workflow_mode = "导入翻译并渲染"
893+
workflow_tip = "💡 提示:将从 manga_translator_work/originals/ 或 translations/ 目录读取 TXT 文件并渲染(优先使用 _original.txt)"
894+
842895
if is_hq or (len(self.files) > 1 and batch_size > 1):
843896
self.log_received.emit(f"--- [12] THREAD: Starting batch processing ({'HQ mode' if is_hq else 'Batch mode'})...")
844897

898+
# 输出批量处理信息
899+
total_images = len(self.files)
900+
total_batches = (total_images + batch_size - 1) // batch_size if batch_size > 0 else 1
901+
self.log_received.emit(f"📊 批量处理模式:共 {total_images} 张图片,分 {total_batches} 个批次处理")
902+
self.log_received.emit(f"🔧 翻译流程:{workflow_mode}")
903+
self.log_received.emit(f"📁 输出目录:{self.output_folder}")
904+
if workflow_tip:
905+
self.log_received.emit(workflow_tip)
906+
845907
images_with_configs = []
846908
for file_path in self.files:
847909
if not self._is_running: raise asyncio.CancelledError("Task stopped by user.")
@@ -850,39 +912,62 @@ async def _do_processing(self):
850912
image.name = file_path
851913
images_with_configs.append((image, config))
852914

915+
self.log_received.emit(f"🚀 开始翻译...")
853916
contexts = await translator.translate_batch(images_with_configs, save_info=save_info)
854917

855918
# The backend now handles saving for batch jobs. We just need to collect the paths/status.
919+
success_count = 0
856920
for ctx in contexts:
857921
if not self._is_running: raise asyncio.CancelledError("Task stopped by user.")
858922
if ctx:
859923
results.append({'success': True, 'original_path': ctx.image_name, 'image_data': None})
924+
success_count += 1
860925
else:
861926
results.append({'success': False, 'original_path': 'Unknown', 'error': 'Batch translation returned no context'})
862927

863-
else:
928+
self.log_received.emit(f"✅ 批量翻译完成:成功 {success_count}/{total_images} 张")
929+
self.log_received.emit(f"💾 文件已保存到:{self.output_folder}")
930+
931+
else:
864932
self.log_received.emit("--- [12] THREAD: Starting sequential processing...")
865933
total_files = len(self.files)
934+
935+
# 输出顺序处理信息
936+
self.log_received.emit(f"📊 顺序处理模式:共 {total_files} 张图片")
937+
self.log_received.emit(f"🔧 翻译流程:{workflow_mode}")
938+
self.log_received.emit(f"📁 输出目录:{self.output_folder}")
939+
if workflow_tip:
940+
self.log_received.emit(workflow_tip)
941+
942+
success_count = 0
866943
for i, file_path in enumerate(self.files):
867944
if not self._is_running:
868945
raise asyncio.CancelledError("Task stopped by user.")
869946

947+
current_num = i + 1
870948
self.progress.emit(i, total_files, f"Processing: {os.path.basename(file_path)}")
871-
949+
self.log_received.emit(f"🔄 [{current_num}/{total_files}] 正在处理:{os.path.basename(file_path)}")
950+
872951
try:
873952
image = Image.open(file_path)
874953
image.name = file_path
875-
954+
876955
ctx = await translator.translate(image, config, image_name=image.name)
877-
956+
878957
if ctx and ctx.result:
879958
self.file_processed.emit({'success': True, 'original_path': file_path, 'image_data': ctx.result})
959+
success_count += 1
960+
self.log_received.emit(f"✅ [{current_num}/{total_files}] 完成:{os.path.basename(file_path)}")
880961
else:
881962
self.file_processed.emit({'success': False, 'original_path': file_path, 'error': 'Translation returned no result or image'})
963+
self.log_received.emit(f"❌ [{current_num}/{total_files}] 失败:{os.path.basename(file_path)}")
882964

883965
except Exception as e:
884-
self.log_received.emit(f"Error processing file {os.path.basename(file_path)}: {e}")
966+
self.log_received.emit(f"❌ [{current_num}/{total_files}] 错误:{os.path.basename(file_path)} - {e}")
885967
self.file_processed.emit({'success': False, 'original_path': file_path, 'error': str(e)})
968+
969+
self.log_received.emit(f"✅ 顺序翻译完成:成功 {success_count}/{total_files} 张")
970+
self.log_received.emit(f"💾 文件已保存到:{self.output_folder}")
886971

887972
self.finished.emit(results)
888973

desktop_qt_ui/editor/graphics_view.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,17 @@ def mousePressEvent(self, event):
921921
dummy_event = event.clone()
922922
dummy_event.setButton(Qt.MouseButton.LeftButton)
923923
super().mousePressEvent(dummy_event)
924-
else:
924+
elif event.button() == Qt.MouseButton.LeftButton:
925+
# 检查是否点击在空白区域
926+
item_at_pos = self.itemAt(event.pos())
927+
928+
# 如果点击在空白区域(没有 item 或只有图片),启用拖动模式
929+
if item_at_pos is None or item_at_pos == self._image_item:
930+
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
931+
dummy_event = event.clone()
932+
super().mousePressEvent(dummy_event)
933+
return
934+
925935
# 先记录当前选择
926936
old_selection = self.model.get_selection().copy()
927937

@@ -934,6 +944,8 @@ def mousePressEvent(self, event):
934944
# 只有真正点击空白时,event 才不会被 accept
935945
if not event.isAccepted():
936946
self.model.set_selection([])
947+
else:
948+
super().mousePressEvent(event)
937949

938950
def mouseMoveEvent(self, event):
939951
"""Handle mouse move for drawing."""
@@ -1007,7 +1019,7 @@ def mouseReleaseEvent(self, event):
10071019
if self._is_drawing and event.button() == Qt.MouseButton.LeftButton:
10081020
self._finish_drawing()
10091021

1010-
if event.button() == Qt.MouseButton.MiddleButton:
1022+
if event.button() == Qt.MouseButton.MiddleButton or event.button() == Qt.MouseButton.LeftButton:
10111023
self.setDragMode(QGraphicsView.DragMode.NoDrag)
10121024
super().mouseReleaseEvent(event)
10131025

desktop_qt_ui/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import logging
44
import warnings
55

6+
# 设置 Hugging Face 镜像站(国内用户加速下载)
7+
os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
8+
69
# 修复PyInstaller打包后onnxruntime的DLL加载问题
710
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
811
# 运行在PyInstaller打包环境中

desktop_qt_ui/main_window.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def _connect_signals(self):
8484
self.app_logic.file_removed.connect(self.main_view.file_list.remove_file)
8585
self.app_logic.output_path_updated.connect(self.main_view.update_output_path_display)
8686
self.app_logic.task_completed.connect(self.on_task_completed, type=Qt.ConnectionType.QueuedConnection)
87+
self.app_logic.log_message.connect(self.main_view.append_log)
8788

8889
# --- View to Logic Connections ---
8990
self.main_view.setting_changed.connect(self.app_logic.update_single_config)

examples/config-example.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
{
22
"app": {
3-
"last_open_dir": "",
4-
"last_output_path": ""
3+
"last_open_dir": "D:/xiazai/图片助手(ImageAssistant)_批量图片下载器/午夜心旋律",
4+
"last_output_path": "D:/xiazai/图片助手(ImageAssistant)_批量图片下载器/output"
55
},
66
"filter_text": null,
77
"kernel_size": 3,
88
"mask_dilation_offset": 70,
99
"translator": {
10-
"translator": "gemini",
10+
"translator": "gemini_hq",
1111
"target_lang": "CHS",
1212
"no_text_lang_skip": false,
1313
"gpt_config": "../examples/gpt_config-example.yaml",
1414
"high_quality_prompt_path": "dict/prompt_example.json"
1515
},
1616
"ocr": {
1717
"use_mocr_merge": false,
18-
"ocr": "paddleocr",
18+
"ocr": "mocr",
1919
"use_hybrid_ocr": false,
2020
"secondary_ocr": "48px",
2121
"min_text_length": 0,

manga_translator/ocr/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
def get_ocr(key: Ocr, *args, **kwargs) -> CommonOCR:
2323
if key not in OCRS:
2424
raise ValueError(f'Could not find OCR for: "{key}". Choose from the following: %s' % ','.join(OCRS))
25-
# Always create a new instance to avoid caching issues in editor mode
26-
ocr = OCRS[key]
27-
return ocr(*args, **kwargs)
25+
# Use cache to avoid reloading models in the same translation session
26+
if key not in ocr_cache:
27+
ocr = OCRS[key]
28+
ocr_cache[key] = ocr(*args, **kwargs)
29+
return ocr_cache[key]
2830

2931
async def prepare(ocr_key: Ocr, device: str = 'cpu'):
3032
ocr = get_ocr(ocr_key)

manga_translator/ocr/model_paddleocr.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def _decode_ctc(self, pred: np.ndarray):
256256
return text, confidence
257257

258258
def _estimate_colors(self, region: np.ndarray, textline: Quadrilateral):
259-
"""Estimate foreground/background colors using Otsu thresholding"""
259+
"""Estimate foreground/background colors using improved Otsu thresholding"""
260260
try:
261261
if len(region.shape) == 3:
262262
gray = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY)
@@ -281,17 +281,43 @@ def _estimate_colors(self, region: np.ndarray, textline: Quadrilateral):
281281

282282
if np.any(fg_mask):
283283
fg_pixels = region_rgb[fg_mask]
284-
textline.fg_r = int(np.mean(fg_pixels[:, 0]))
285-
textline.fg_g = int(np.mean(fg_pixels[:, 1]))
286-
textline.fg_b = int(np.mean(fg_pixels[:, 2]))
284+
285+
# 改进:使用中位数代替平均值,减少抗锯齿像素的影响
286+
fg_r = int(np.median(fg_pixels[:, 0]))
287+
fg_g = int(np.median(fg_pixels[:, 1]))
288+
fg_b = int(np.median(fg_pixels[:, 2]))
289+
290+
# 颜色量化:如果接近黑色(RGB < 40),强制设为纯黑
291+
if fg_r < 40 and fg_g < 40 and fg_b < 40:
292+
textline.fg_r = textline.fg_g = textline.fg_b = 0
293+
# 如果接近白色(RGB > 215),强制设为纯白
294+
elif fg_r > 215 and fg_g > 215 and fg_b > 215:
295+
textline.fg_r = textline.fg_g = textline.fg_b = 255
296+
else:
297+
textline.fg_r = fg_r
298+
textline.fg_g = fg_g
299+
textline.fg_b = fg_b
287300
else:
288301
textline.fg_r = textline.fg_g = textline.fg_b = 0
289302

290303
if np.any(bg_mask):
291304
bg_pixels = region_rgb[bg_mask]
292-
textline.bg_r = int(np.mean(bg_pixels[:, 0]))
293-
textline.bg_g = int(np.mean(bg_pixels[:, 1]))
294-
textline.bg_b = int(np.mean(bg_pixels[:, 2]))
305+
306+
# 改进:使用中位数代替平均值
307+
bg_r = int(np.median(bg_pixels[:, 0]))
308+
bg_g = int(np.median(bg_pixels[:, 1]))
309+
bg_b = int(np.median(bg_pixels[:, 2]))
310+
311+
# 颜色量化:如果接近白色(RGB > 215),强制设为纯白
312+
if bg_r > 215 and bg_g > 215 and bg_b > 215:
313+
textline.bg_r = textline.bg_g = textline.bg_b = 255
314+
# 如果接近黑色(RGB < 40),强制设为纯黑
315+
elif bg_r < 40 and bg_g < 40 and bg_b < 40:
316+
textline.bg_r = textline.bg_g = textline.bg_b = 0
317+
else:
318+
textline.bg_r = bg_r
319+
textline.bg_g = bg_g
320+
textline.bg_b = bg_b
295321
else:
296322
textline.bg_r = textline.bg_g = textline.bg_b = 255
297323
else:

manga_translator/translators/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@
6767
def get_translator(key: Translator, *args, **kwargs) -> CommonTranslator:
6868
if key not in TRANSLATORS:
6969
raise ValueError(f'Could not find translator for: "{key}". Choose from the following: %s' % ','.join(TRANSLATORS))
70-
# Always create a new instance to avoid caching issues in editor mode
71-
translator = TRANSLATORS[key]
72-
return translator(*args, **kwargs)
70+
# Use cache to avoid reloading models in the same translation session
71+
if key not in translator_cache:
72+
translator = TRANSLATORS[key]
73+
translator_cache[key] = translator(*args, **kwargs)
74+
return translator_cache[key]
7375

7476
prepare_selective_translator(get_translator)
7577

0 commit comments

Comments
 (0)