138ec9fa by 柴进

:bug: 修复历史记录累积后 refresh_history 触发 macOS SIGKILL

根因:
- refresh_history 每次调用都 history_list.clear() + 重建全部条目
- 每条加载 2K PNG 原图做 QPixmap (~16MB 解码) 再缩到 120x120
- 106 条历史 × 16MB = ~1.7GB 瞬时内存峰值
- macOS 内存压力触发 SIGKILL,不可被 faulthandler 捕获
- 日志里只有 "开始刷新" 没有 "加载到 N 条",确认死在 clear()/load 之间

修复:
- HistoryManager 新增 thumb_path_for + get_or_create_thumbnail:
  用 PIL 生成 240x240 JPEG 缓存到 <record_dir>/thumb.jpg,
  基于 mtime 判断是否重新生成
- save_generation 保存原图后顺便生成缩略图 (失败不影响主流程)
- refresh_history 只加载 thumb.jpg, 内存峰值从 1.7GB → 6MB (280x 下降)
- 缩略图生成失败用占位图兜底, 不回退加载原图 (防回到危险路径)
- 首次升级会为 106 条存量一次性补生成 thumb.jpg (PIL 串行 ~32MB 峰值, 安全)

附加:
- refresh_history 增加 "刷新完成" 收尾日志, 便于下次若再崩定位死亡位置

跨平台: 纯 PIL + pathlib + Qt 标准 API, Windows 零回归

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2473f5d4
......@@ -673,11 +673,53 @@ class HistoryManager:
self._update_history_index(history_item)
# 生成缩略图(失败不影响主流程)
try:
self.get_or_create_thumbnail(generated_image_path)
except Exception as e:
self.logger.warning(f"保存历史记录时生成缩略图失败: {e}")
# 清理旧记录
self._cleanup_old_records()
return timestamp
@staticmethod
def thumb_path_for(generated_image_path: Path) -> Path:
"""缩略图在 generated.png 同目录下的固定名称 thumb.jpg。"""
return generated_image_path.parent / "thumb.jpg"
def get_or_create_thumbnail(self, generated_image_path: Path, size: int = 240):
"""返回缓存缩略图路径,缺失或过期时用 PIL 生成一次。
refresh_history 每次加载 100+ 条 2K PNG 会导致 ~1.7GB 瞬时内存,
在 macOS 上触发 SIGKILL。缩略图把单条开销从 ~16MB 降到 ~5KB。
失败时返回 None,调用方应回退到原图或占位图。
"""
try:
thumb_path = self.thumb_path_for(generated_image_path)
# 已缓存且不比源图旧 -> 直接用
if thumb_path.exists():
try:
if thumb_path.stat().st_mtime >= generated_image_path.stat().st_mtime:
return thumb_path
except OSError:
pass # stat 失败则重新生成
# 用 PIL 生成缩略图(比 QPixmap 省内存,且释放确定性)
from PIL import Image
with Image.open(generated_image_path) as img:
img = img.convert("RGB")
img.thumbnail((size, size), Image.LANCZOS)
img.save(str(thumb_path), "JPEG", quality=75, optimize=True)
return thumb_path
except Exception as e:
try:
self.logger.warning(f"生成缩略图失败 {generated_image_path}: {e}")
except Exception:
pass
return None
def _fix_history_path(self, stored_path: Path, timestamp: str) -> Path:
"""修正历史记录中的路径,使其指向当前 base_path
......@@ -2890,19 +2932,22 @@ class ImageGeneratorWindow(QMainWindow):
tooltip += f"尺寸: {item.image_size}"
list_item.setToolTip(tooltip)
# Try to load thumbnail
# 加载缩略图:只走磁盘缓存的 thumb.jpg,避免每次解码 2K 原图
# (2K PNG 解码 ~16MB × 100 条 ~1.7GB 瞬时内存 → macOS 上触发 SIGKILL)
# 如果缩略图生成失败(极罕见)用占位图兜底,绝不回退去加载原图。
if item.generated_image_path.exists():
try:
self.logger.debug(f"[refresh_history] 加载缩略图: {item.generated_image_path}")
pixmap = QPixmap(str(item.generated_image_path))
if not pixmap.isNull():
# Scale to thumbnail size
scaled_pixmap = pixmap.scaled(120, 120, Qt.KeepAspectRatio, Qt.SmoothTransformation)
list_item.setIcon(QIcon(scaled_pixmap))
thumb_path = self.history_manager.get_or_create_thumbnail(item.generated_image_path)
if thumb_path is None:
list_item.setIcon(self.create_placeholder_icon("缩略图\n失败"))
else:
self.logger.warning(f"[refresh_history] 缩略图加载为空: {item.generated_image_path}")
# Create placeholder icon
list_item.setIcon(self.create_placeholder_icon("图片\n加载失败"))
pixmap = QPixmap(str(thumb_path))
if not pixmap.isNull():
scaled_pixmap = pixmap.scaled(120, 120, Qt.KeepAspectRatio, Qt.SmoothTransformation)
list_item.setIcon(QIcon(scaled_pixmap))
else:
self.logger.warning(f"[refresh_history] 缩略图加载为空: {thumb_path}")
list_item.setIcon(self.create_placeholder_icon("图片\n加载失败"))
except Exception as e:
self.logger.error(f"[refresh_history] 缩略图加载异常 {item.timestamp}: {e}")
list_item.setIcon(self.create_placeholder_icon("图片\n错误"))
......@@ -2925,6 +2970,8 @@ class ImageGeneratorWindow(QMainWindow):
if not history_items:
self.clear_details_panel()
self.logger.info(f"[refresh_history] 刷新完成,共渲染 {len(history_items)} 条")
def create_placeholder_icon(self, text):
"""Create a placeholder icon with text"""
# Create a 120x120 pixmap
......