Skip to content

Commit 264be1e

Browse files
hgmzhnclaude
andcommitted
Release v1.6.7 - 修复多个重要bug
主要修复: - 修复编辑器预览时AI断句导致横排和竖排文本多余换行的问题 - 修复翻译结束后.env配置更新不生效的问题(清空翻译器缓存) - 优化异步事件循环清理逻辑,避免资源泄漏 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6900c96 commit 264be1e

14 files changed

Lines changed: 240 additions & 82 deletions

File tree

README.md

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,26 +149,43 @@
149149

150150
**用途**:自定义导出原文的格式,方便使用外部工具翻译
151151

152-
**模版文件位置**`dict/template_example.txt`
152+
**模版文件位置**`examples/translation_template.json`
153153

154-
**模版格式**
155-
```
156-
前缀内容
157-
<original>原文占位符</original>, <translated>翻译占位符</translated>
158-
后缀内容
159-
```
154+
**模版格式说明**
155+
- 使用 JSON 格式
156+
- 使用 `<original>` 作为原文占位符
157+
- 使用 `<translated>` 作为译文占位符
158+
- 可以添加前缀、后缀和自定义分隔符
160159

161160
**示例模版**
162161
```json
163-
[
164-
{"original": "<original>", "translated": "<translated>"},
165-
]
162+
{
163+
"<original>": "<translated>",
164+
"<original>": "<translated>",
165+
"<original>": "<translated>"
166+
}
167+
```
168+
169+
**三个文本框的具体示例**
170+
```json
171+
{
172+
"你好": "Hello",
173+
"世界": "World",
174+
"欢迎": "Welcome"
175+
}
166176
```
167177

178+
**使用要求**
179+
1. 必须使用 `<original>``<translated>` 作为占位符
180+
2. 导出时会按照模版中的条目数量进行分组
181+
3. 每组包含的文本框数量 = 模版中的占位符对数量(本例为 3 个)
182+
4. 适合批量翻译多个文本框并保持固定格式
183+
168184
**使用方法**
169-
1. 编辑 `dict/template_example.txt` 文件
170-
2. 使用 `<original>``<translated>` 作为占位符
185+
1. 编辑 `examples/translation_template.json` 文件
186+
2. 自定义 JSON 格式和占位符位置
171187
3. 导出原文时,程序会按照模版格式生成 TXT 文件
188+
4. 手动翻译后,可以使用"导入翻译并渲染"功能导入
172189

173190
### AI 断句功能
174191

@@ -397,7 +414,7 @@
397414
- 查看 `ocrs/` 文件夹中的图片,确认每个文本框的内容
398415
- 查看 `bboxes.png`,确认哪些文本框被成功识别
399416
- 如果识别率低:降低 **OCR 置信度**,或提高 **Unclip 比例**(让文本框包含更多周边区域)
400-
- 如果识别错误多:提高 **OCR 置信度**,或在编辑器中手动调整文本框(确保一个蓝框里只有一行字)
417+
- 如果识别错误多:提高 **OCR 置信度**
401418

402419
---
403420

desktop_qt_ui/app_logic.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,18 @@ def start_backend_task(self):
495495
"""
496496
Resolves input paths and uses a 'Worker-to-Thread' model to start the translation task.
497497
"""
498+
# 通过调用配置服务的 reload_config 方法,强制全面重新加载所有配置
499+
try:
500+
self.logger.info("即将开始后台任务,强制重新加载所有配置...")
501+
self.config_service.reload_config()
502+
self.logger.info("配置已刷新,继续执行任务。")
503+
except Exception as e:
504+
self.logger.error(f"重新加载配置时发生严重错误: {e}")
505+
# 根据需要,这里可以决定是否要中止任务
506+
# from PyQt6.QtWidgets import QMessageBox
507+
# QMessageBox.critical(None, "配置错误", f"无法加载最新配置: {e}")
508+
# return
509+
498510
if self.thread is not None and self.thread.isRunning():
499511
self.logger.warning("一个任务已经在运行中。")
500512
return
@@ -636,6 +648,8 @@ def on_task_finished(self, results):
636648
self.logger.error(f"完成任务状态更新或信号发射时发生致命错误: {e}", exc_info=True)
637649
finally:
638650
print("--- DEBUG: on_task_finished step 5: Entering finally block.")
651+
if self.thread and self.thread.isRunning():
652+
self.thread.quit()
639653
self.thread = None
640654
self.worker = None
641655
print("--- MainAppLogic: Slot on_task_finished finished.")
@@ -832,7 +846,15 @@ async def _do_processing(self):
832846
if k in Config.__fields__ and k not in explicit_keys
833847
}
834848

835-
render_config_data = self.config_dict.get('render', {})
849+
render_config_data = self.config_dict.get('render', {}).copy()
850+
851+
# 转换 direction 值:'h' -> 'horizontal', 'v' -> 'vertical'
852+
if 'direction' in render_config_data:
853+
direction_value = render_config_data['direction']
854+
if direction_value == 'h':
855+
render_config_data['direction'] = 'horizontal'
856+
elif direction_value == 'v':
857+
render_config_data['direction'] = 'vertical'
836858

837859
translator_config_data = self.config_dict.get('translator', {}).copy()
838860
hq_prompt_path = translator_config_data.get('high_quality_prompt_path')
@@ -980,23 +1002,51 @@ async def _do_processing(self):
9801002
finally:
9811003
manga_logger.removeHandler(log_handler)
9821004

1005+
# 翻译结束后清空翻译器缓存,确保下次翻译使用最新的 .env 配置
1006+
try:
1007+
from manga_translator.translators import translator_cache
1008+
translator_cache.clear()
1009+
self.log_received.emit(f"--- [CLEANUP] Cleared translator cache")
1010+
except Exception as e:
1011+
self.log_received.emit(f"--- [CLEANUP] Warning: Failed to clear cache: {e}")
1012+
9831013
@pyqtSlot()
9841014
def process(self):
1015+
loop = None
9851016
try:
9861017
import asyncio
9871018
self.log_received.emit("--- [1] THREAD: process() method entered, starting asyncio task.")
9881019

9891020
# 创建事件循环并保存任务引用
9901021
loop = asyncio.new_event_loop()
9911022
asyncio.set_event_loop(loop)
992-
try:
993-
self._current_task = loop.create_task(self._do_processing())
994-
loop.run_until_complete(self._current_task)
995-
self.log_received.emit("--- [END] THREAD: asyncio task finished.")
996-
except asyncio.CancelledError:
997-
self.log_received.emit("--- [CANCELLED] THREAD: asyncio task was cancelled.")
998-
finally:
999-
loop.close()
1023+
1024+
self._current_task = loop.create_task(self._do_processing())
1025+
loop.run_until_complete(self._current_task)
1026+
self.log_received.emit("--- [END] THREAD: asyncio task finished.")
1027+
1028+
except asyncio.CancelledError:
1029+
self.log_received.emit("--- [CANCELLED] THREAD: asyncio task was cancelled.")
10001030
except Exception as e:
10011031
import traceback
10021032
self.error.emit(f"An error occurred in the asyncio runner: {str(e)}\n{traceback.format_exc()}")
1033+
finally:
1034+
if loop:
1035+
try:
1036+
# Cancel all remaining tasks
1037+
tasks = asyncio.all_tasks(loop=loop)
1038+
for task in tasks:
1039+
task.cancel()
1040+
1041+
# Gather all tasks to let them finish cancelling
1042+
if tasks:
1043+
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
1044+
1045+
# Shutdown async generators
1046+
loop.run_until_complete(loop.shutdown_asyncgens())
1047+
except Exception as e:
1048+
self.log_received.emit(f"--- ERROR during asyncio cleanup: {e}")
1049+
finally:
1050+
loop.close()
1051+
asyncio.set_event_loop(None)
1052+
self.log_received.emit("--- [CLEANUP] THREAD: asyncio loop closed.")

desktop_qt_ui/editor/text_renderer_backend.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ def render_text_for_region(text_block: TextBlock, dst_points: np.ndarray, transf
5555
text_block.translation = processed_text
5656

5757
# --- 2. 渲染 ---
58-
hyphenate = render_params.get('hyphenate', True)
58+
# 正确的逻辑:当AI断句开启(disable_auto_wrap=True)时,关闭内置换行(hyphenate=False)
59+
disable_auto_wrap = render_params.get('disable_auto_wrap', False)
60+
hyphenate = not disable_auto_wrap
61+
5962
render_params.get('line_spacing')
6063
disable_font_border = render_params.get('disable_font_border', False)
6164

@@ -90,7 +93,7 @@ def render_text_for_region(text_block: TextBlock, dst_points: np.ndarray, transf
9093
line_spacing_from_params = render_params.get('line_spacing')
9194

9295
if text_block.horizontal:
93-
rendered_surface = put_text_horizontal(font_size, text_block.get_translation_for_rendering(), render_w, render_h, text_block.alignment, text_block.direction == 'hl', fg_color, bg_color, text_block.target_lang, hyphenate, line_spacing_from_params, config=config_obj)
96+
rendered_surface = put_text_horizontal(font_size, text_block.get_translation_for_rendering(), render_w, render_h, text_block.alignment, text_block.direction == 'hl', fg_color, bg_color, text_block.target_lang, hyphenate, line_spacing_from_params, config=config_obj, region_count=total_regions)
9497
else:
9598
rendered_surface = put_text_vertical(font_size, text_block.get_translation_for_rendering(), render_h, text_block.alignment, fg_color, bg_color, line_spacing_from_params, config=config_obj, region_count=total_regions)
9699

desktop_qt_ui/services/config_service.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,29 @@ def save_config_file(self, config_path: Optional[str] = None) -> bool:
179179
self.logger.error(f"保存配置文件失败: {e}")
180180
return False
181181

182+
def reload_config(self):
183+
"""
184+
强制从 .env 和 JSON 文件完全重新加载配置。
185+
这能确保外部对文件的任何修改都能在程序中生效。
186+
"""
187+
self.logger.info("正在强制重新加载配置...")
188+
189+
# 1. 重新加载 .env 文件到 os.environ。翻译引擎会自动从此读取。
190+
load_dotenv(self.env_path, override=True)
191+
self.logger.info(f".env 文件已从 {self.env_path} 重新加载,环境变量已更新。")
192+
193+
# 2. 重新创建 AppSettings 对象 (用于UI设置)
194+
self.current_config = AppSettings()
195+
196+
# 3. 在新创建的 AppSettings 对象之上,重新应用 JSON 配置文件中的设置
197+
config_file_to_load = self.config_path or self.default_config_path
198+
if config_file_to_load and os.path.exists(config_file_to_load):
199+
self.load_config_file(config_file_to_load)
200+
201+
# 4. 通知所有监听者配置已更改
202+
self.config_changed.emit(self.current_config.dict())
203+
self.logger.info("配置重载完成。")
204+
182205
def reload_from_disk(self):
183206
"""
184207
强制从当前设置的 config_path 重新加载配置, 并通知所有监听者。

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": "D:/xiazai/图片助手(ImageAssistant)_批量图片下载器/午夜心旋律",
3+
"last_open_dir": "D:/xiazai/图片助手(ImageAssistant)_批量图片下载器",
44
"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_hq",
10+
"translator": "gemini",
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": "mocr",
18+
"ocr": "paddleocr_korean",
1919
"use_hybrid_ocr": false,
2020
"secondary_ocr": "48px",
2121
"min_text_length": 0,
@@ -76,7 +76,7 @@
7676
"verbose": true,
7777
"attempts": -1,
7878
"ignore_errors": false,
79-
"use_gpu": true,
79+
"use_gpu": false,
8080
"use_gpu_limited": false,
8181
"context_size": 3,
8282
"format": "不指定",

manga_translator/manga_translator.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2561,13 +2561,7 @@ async def _batch_translate_contexts(self, contexts_with_configs: List[tuple], ba
25612561
region._alignment = config.render.alignment
25622562
region._direction = config.render.direction
25632563
results.extend(batch)
2564-
2565-
# 强制垃圾回收以释放内存
2566-
import gc
2567-
gc.collect()
2568-
if torch.cuda.is_available():
2569-
torch.cuda.empty_cache()
2570-
2564+
25712565
return results
25722566

25732567
async def _concurrent_translate_contexts(self, contexts_with_configs: List[tuple]) -> List[tuple]:

manga_translator/ocr/model_manga_ocr.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,10 @@ async def _load(self, device: str):
126126

127127

128128
async def _unload(self):
129-
del self.model
130-
del self.mocr
129+
if hasattr(self, 'model'):
130+
del self.model
131+
if hasattr(self, 'mocr'):
132+
del self.mocr
131133

132134
async def _infer(self, image: np.ndarray, textlines: List[Quadrilateral], config: OcrConfig, verbose: bool = False, ignore_bubble: int = 0) -> List[TextBlock]:
133135
text_height = 48

manga_translator/rendering/text_render.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -514,10 +514,13 @@ def put_text_vertical(font_size: int, text: str, h: int, alignment: str, fg: Tup
514514
bg_size = int(max(font_size * 0.07, 1)) if bg is not None else 0
515515
spacing_x = int(font_size * (line_spacing or 0.2))
516516

517-
# Conditional wrapping logic based on the new region_count parameter
517+
# Conditional wrapping logic based on disable_auto_wrap and region_count
518518
effective_max_height = h
519-
if config and config.render.layout_mode == 'smart_scaling':
520-
if config.render.disable_auto_wrap or region_count <= 1:
519+
if config and config.render.disable_auto_wrap:
520+
# 当AI断句开启时,使用无限高度,让文本按AI断句标记换行
521+
effective_max_height = 99999
522+
elif config and config.render.layout_mode == 'smart_scaling':
523+
if region_count <= 1:
521524
effective_max_height = 99999
522525

523526
# Use original font size for line breaking calculation
@@ -714,7 +717,26 @@ def calc_horizontal(font_size: int, text: str, max_width: int, max_height: int,
714717
whitespace_offset_x = get_char_offset_x(font_size, ' ')
715718
hyphen_offset_x = get_char_offset_x(font_size, '-')
716719

717-
words = re.split(r'\s+', text)
720+
# 先按换行符分割段落,然后对每段分割单词
721+
# 使用特殊标记来保留换行位置
722+
paragraphs = text.split('\n')
723+
words = []
724+
newline_positions = set() # 记录哪些位置是段落结束(需要强制换行)
725+
726+
for para_idx, paragraph in enumerate(paragraphs):
727+
if paragraph.strip(): # 非空段落
728+
para_words = re.split(r'[ \t]+', paragraph) # 只按空格和制表符分割,不包括 \n
729+
words.extend(para_words)
730+
if para_idx < len(paragraphs) - 1: # 不是最后一段
731+
newline_positions.add(len(words) - 1) # 标记这个单词后面需要换行
732+
elif para_idx < len(paragraphs) - 1: # 空段落但不是最后一个
733+
# 空行也需要保留
734+
words.append('')
735+
newline_positions.add(len(words) - 1)
736+
737+
# 如果没有单词,返回空结果
738+
if not words:
739+
return [], []
718740

719741
word_widths = []
720742
for i, word in enumerate(words):
@@ -804,6 +826,9 @@ def get_present_syllables(line_idx, word_pos):
804826
line_words.append(i)
805827
line_width += current_width + word_widths[i]
806828
i += 1
829+
# 检查是否需要强制换行(AI 断句)
830+
if (i - 1) in newline_positions:
831+
break_line()
807832
elif word_widths[i] > max_width:
808833
j = 0
809834
hyphenation_idx = 0
@@ -824,8 +849,18 @@ def get_present_syllables(line_idx, word_pos):
824849
line_words.append(i)
825850
line_width += current_width
826851
i += 1
852+
# 检查是否需要强制换行(AI 断句)
853+
if (i - 1) in newline_positions:
854+
break_line()
827855
else:
828-
break_line()
856+
if hyphenate:
857+
break_line()
858+
else:
859+
line_words.append(i)
860+
line_width += current_width + word_widths[i]
861+
i += 1
862+
if (i - 1) in newline_positions:
863+
break_line()
829864

830865

831866
# 连字符优化阶段
@@ -1050,16 +1085,23 @@ def put_text_horizontal(font_size: int, text: str, width: int, height: int, alig
10501085
layout_mode = 'default'
10511086
if config:
10521087
layout_mode = config.render.layout_mode
1053-
# Check for no-wrap condition and handle AI line breaks
1054-
if layout_mode == 'smart_scaling':
1055-
# In smart_scaling mode, wrapping is conditional.
1056-
# It wraps only if manual line breaks ([BR] or \n) are present.
1057-
# Otherwise, it expands without wrapping.
1058-
text = re.sub(r'\s*\[BR\]\s*', '\n', text, flags=re.IGNORECASE)
1059-
if '\n' not in text:
1060-
# No manual breaks found, so disable wrapping by setting a large width.
1061-
if config.render.disable_auto_wrap or region_count <= 1:
1062-
width = 99999
1088+
1089+
# 当AI断句开启时,统一处理换行符并使用无限宽度
1090+
if config and config.render.disable_auto_wrap:
1091+
# 统一处理所有类型的AI换行符
1092+
text = re.sub(r'\s*(\[BR\]|<br>|【BR】)\s*', '\n', text, flags=re.IGNORECASE)
1093+
# 使用无限宽度,让文本完全按照AI断句标记换行
1094+
width = 99999
1095+
elif layout_mode == 'smart_scaling':
1096+
# In smart_scaling mode, wrapping is conditional.
1097+
# It wraps only if manual line breaks ([BR] or \n) are present.
1098+
# Otherwise, it expands without wrapping.
1099+
# 统一处理所有类型的AI换行符
1100+
text = re.sub(r'\s*(\[BR\]|<br>|【BR】)\s*', '\n', text, flags=re.IGNORECASE)
1101+
if '\n' not in text:
1102+
# No manual breaks found, so disable wrapping by setting a large width.
1103+
if region_count <= 1:
1104+
width = 99999
10631105

10641106
bg_size = int(max(font_size * 0.07, 1)) if bg is not None else 0
10651107
spacing_y = int(font_size * (line_spacing or 0.01))

0 commit comments

Comments
 (0)