T-575 优化AI生成图片失败反馈

This commit is contained in:
chengma
2026-07-09 16:45:53 +08:00
parent 87648e89d5
commit cbb14bb678
3 changed files with 210 additions and 9 deletions
+116 -7
View File
@@ -2,7 +2,8 @@
from __future__ import annotations
from PySide6.QtCore import QEvent, QSize
from PySide6.QtCore import QEvent, QRectF, QSize
from PySide6.QtGui import QColor, QPainter
from ..models import GenerateTaskTableModel
from ..widgets import *
@@ -71,6 +72,63 @@ class OriginalImageDialog(QDialog):
_center_dialog(self, available)
class _SegmentedProgressBar(QProgressBar):
def __init__(self, parent=None):
super().__init__(parent)
self._segment_success = 0
self._segment_failed = 0
self._segment_total = 0
self.setTextVisible(False)
self.setRange(0, 1)
self.setValue(0)
self.setMinimumHeight(12)
def set_segments(self, success, failed, total):
total = max(0, int(total or 0))
success = min(max(0, int(success or 0)), total)
failed = min(max(0, int(failed or 0)), max(0, total - success))
self._segment_success = success
self._segment_failed = failed
self._segment_total = total
maximum = max(1, total)
self.setRange(0, maximum)
self.setValue(min(maximum, success + failed))
self.update()
def segments(self):
return self._segment_success, self._segment_failed, self._segment_total
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
rect = QRectF(self.rect()).adjusted(0.5, 0.5, -0.5, -0.5)
painter.setPen(Qt.NoPen)
painter.setBrush(QColor("#f6f8fa"))
painter.drawRoundedRect(rect, 3, 3)
total = max(0, self._segment_total)
if total:
inner = rect.adjusted(1, 1, -1, -1)
unit_width = inner.width() / total if total else 0
success_width = unit_width * self._segment_success
failed_width = unit_width * self._segment_failed
if success_width > 0:
painter.setBrush(QColor(COLOR_SUCCESS))
painter.drawRect(QRectF(inner.left(), inner.top(), success_width, inner.height()))
if failed_width > 0:
painter.setBrush(QColor(COLOR_DANGER))
painter.drawRect(
QRectF(
inner.left() + success_width,
inner.top(),
failed_width,
inner.height(),
)
)
painter.setBrush(Qt.NoBrush)
painter.setPen(QColor("#d0d7de"))
painter.drawRoundedRect(rect, 3, 3)
class CoverGalleryDialog(QDialog):
THUMBNAIL_SIZE = 180
OLD_THUMBNAIL_SIZE = 300
@@ -867,11 +925,9 @@ class GenerateTab(QWidget):
self.title_progress_bar.setValue(0)
self.cover_progress_label = QLabel("图片 0/0")
self.cover_progress_label.setObjectName("generateCoverProgressLabel")
self.cover_progress_bar = QProgressBar()
self.cover_progress_bar = _SegmentedProgressBar()
self.cover_progress_bar.setObjectName("generateCoverProgressBar")
self.cover_progress_bar.setTextVisible(False)
self.cover_progress_bar.setRange(0, 1)
self.cover_progress_bar.setValue(0)
self.cover_progress_bar.set_segments(0, 0, 0)
self.failed_progress_label = QLabel("失败 0")
self.failed_progress_label.setObjectName("generateFailedProgressLabel")
self.failed_progress_label.setVisible(False)
@@ -1732,8 +1788,10 @@ class GenerateTab(QWidget):
return
if payload.get("error"):
self._set_status(f"AI 生成失败:{payload.get('error')}")
self._show_generate_completion_message(payload, error=True)
return
self._set_status("AI 生成完成:" + self._generate_progress_text(payload))
self._show_generate_completion_message(payload)
def _on_generate_cancelled(self, payload):
self._set_generate_running(False)
@@ -1746,6 +1804,7 @@ class GenerateTab(QWidget):
self._show_billing_error(billing_error)
return
self._set_status("AI 生成已停止:" + self._generate_progress_text(payload))
self._show_generate_completion_message(payload, cancelled=True)
def _update_generate_progress(self, payload):
total = max(0, int(payload.get("total", 0) or 0))
@@ -1754,12 +1813,14 @@ class GenerateTab(QWidget):
cover_done = max(0, int(payload.get("cover_done", 0) or 0))
cover_total = self._cover_total_for_progress(payload, total)
failed = max(0, int(payload.get("failed", 0) or 0))
cover_failed = self._cover_failed_for_progress(payload, cover_done, cover_total)
self.progress_label.setText("进度:" + self._generate_progress_text(payload))
self.title_progress_label.setText(f"标题 {title_done}/{title_total}")
self.cover_progress_label.setText(f"图片 {cover_done}/{cover_total}")
cover_failed_text = f",失败 {cover_failed}" if cover_failed else ""
self.cover_progress_label.setText(f"图片 {cover_done}/{cover_total}{cover_failed_text}")
self.failed_progress_label.setText(f"失败 {failed}")
self._set_progress_bar(self.title_progress_bar, title_done, title_total)
self._set_progress_bar(self.cover_progress_bar, cover_done, cover_total)
self.cover_progress_bar.set_segments(cover_done, cover_failed, cover_total)
def _reset_cmhub_balance_label(self):
self.cmhub_balance_label.setVisible(False)
@@ -1790,6 +1851,44 @@ class GenerateTab(QWidget):
QMessageBox.warning(self, "AI生成", message)
self._set_status("AI 生成已中止:" + message)
def _show_generate_completion_message(self, payload, cancelled=False, error=False):
failed = max(0, int(payload.get("failed", 0) or 0))
title_total = max(0, int(payload.get("title_total", payload.get("total", 0)) or 0))
title_done = max(0, int(payload.get("title_done", 0) or 0))
cover_total = self._cover_total_for_progress(payload, max(0, int(payload.get("total", 0) or 0)))
cover_done = max(0, int(payload.get("cover_done", 0) or 0))
elapsed_seconds = self._generation_elapsed_total_seconds()
if cancelled:
title = "AI生成已停止"
message = (
f"本轮AI生成已停止:标题 {title_done}/{title_total},图片 {cover_done}/{cover_total},"
f"失败 {failed},用时 {elapsed_seconds} 秒。未完成任务可再次点击开始生成继续处理。"
)
QMessageBox.warning(self, title, message)
return
if error:
title = "AI生成失败"
message = (
f"本轮AI生成失败:标题 {title_done}/{title_total},图片 {cover_done}/{cover_total},"
f"失败 {failed},用时 {elapsed_seconds} 秒。失败原因请查看下方运行日志。"
)
QMessageBox.warning(self, title, message)
return
if failed:
title = "AI生成完成,有失败任务"
message = (
f"本轮AI生成完成:标题 {title_done}/{title_total},图片 {cover_done}/{cover_total},"
f"失败 {failed},用时 {elapsed_seconds} 秒。失败明细请查看下方运行日志,可稍后重新生成失败任务。"
)
QMessageBox.warning(self, title, message)
return
title = "AI生成完成"
message = (
f"本轮AI生成完成:标题 {title_done}/{title_total},图片 {cover_done}/{cover_total},"
f"失败 0,用时 {elapsed_seconds} 秒。"
)
QMessageBox.information(self, title, message)
def _set_progress_bar(self, bar, done, total):
maximum = max(1, int(total or 0))
value = min(max(0, int(done or 0)), maximum)
@@ -1802,6 +1901,16 @@ class GenerateTab(QWidget):
cover_total = total if payload.get("generate_cover", True) else 0
return max(0, int(cover_total or 0))
def _cover_failed_for_progress(self, payload, cover_done, cover_total):
if cover_total <= 0:
return 0
failed = max(0, int(payload.get("failed", 0) or 0))
remaining = max(0, int(cover_total or 0) - max(0, int(cover_done or 0)))
return min(failed, remaining)
def _generation_elapsed_total_seconds(self):
return max(0, int(self._title_elapsed_seconds or 0)) + max(0, int(self._cover_elapsed_seconds or 0))
def _generate_progress_text(self, payload):
total = max(0, int(payload.get("total", 0) or 0))
title_total = max(0, int(payload.get("title_total", total) or 0))
+10 -2
View File
@@ -3,7 +3,7 @@ id: T-575
title: ②AI生成图片失败红色进度段与完成汇总弹窗
phase: 7
deps: [T-574]
status: TODO
status: DONE
created: 2026-07-09
---
@@ -71,4 +71,12 @@ created: 2026-07-09
## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、关键决策。)
- 2026-07-09:已完成。
- `app/gui/tabs/generate.py`:新增 `_SegmentedProgressBar`,图片进度条保持 `QProgressBar` 兼容但改为绿色成功段 + 红色失败段 + 灰白未完成段;②图片进度按 `cover_done` 和保守失败数 `min(failed, cover_total - cover_done)` 显示红色失败区域;本轮完成、失败或停止后弹一次中文汇总框,弹窗只显示汇总,不暴露接口路径或底层异常细节。
- `tests/test_gui.py`:新增分段图片进度条测试、图片部分失败完成汇总弹窗测试、停止汇总弹窗测试。
- 验证通过:
- `py -3.10 -m unittest tests.test_gui`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `py -3.10 -m unittest discover -s tests`
- `git diff --check`(通过;仅提示 `docs/tasks-board.md` 未来触碰时 CRLF 会转 LF,非空白错误)
+84
View File
@@ -2048,6 +2048,90 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_generate_cover_progress_bar_marks_failed_segment(self):
with self.make_temp_dir() as temp_dir:
tab = GenerateTab(config=self.make_config(temp_dir))
self.addCleanup(tab.close)
tab._update_generate_progress(
{
"total": 13,
"title_total": 13,
"title_done": 13,
"cover_total": 13,
"cover_done": 11,
"failed": 2,
"generate_cover": True,
}
)
self.assertEqual("图片 11/13,失败 2", tab.cover_progress_label.text())
self.assertEqual((11, 2, 13), tab.cover_progress_bar.segments())
self.assertEqual(13, tab.cover_progress_bar.maximum())
self.assertEqual(13, tab.cover_progress_bar.value())
self.assert_removed(temp_dir)
def test_generate_finished_shows_chinese_summary_for_failed_images(self):
with self.make_temp_dir() as temp_dir:
tab = GenerateTab(config=self.make_config(temp_dir))
self.addCleanup(tab.close)
tab._title_elapsed_seconds = 120
tab._cover_elapsed_seconds = 372
payload = {
"total": 13,
"title_total": 13,
"title_done": 13,
"cover_total": 13,
"cover_done": 11,
"failed": 2,
"generate_cover": True,
}
with mock.patch.object(tab, "refresh_tasks"), \
mock.patch("app.gui.tabs.generate.QMessageBox.warning") as warning, \
mock.patch("app.gui.tabs.generate.QMessageBox.information") as information:
tab._on_generate_finished(payload)
warning.assert_called_once()
information.assert_not_called()
self.assertEqual("AI生成完成,有失败任务", warning.call_args[0][1])
self.assertIn("标题 13/13", warning.call_args[0][2])
self.assertIn("图片 11/13", warning.call_args[0][2])
self.assertIn("失败 2", warning.call_args[0][2])
self.assertIn("用时 492 秒", warning.call_args[0][2])
self.assert_removed(temp_dir)
def test_generate_cancelled_shows_chinese_summary_dialog(self):
with self.make_temp_dir() as temp_dir:
tab = GenerateTab(config=self.make_config(temp_dir))
self.addCleanup(tab.close)
tab._title_elapsed_seconds = 80
tab._cover_elapsed_seconds = 100
payload = {
"total": 13,
"title_total": 13,
"title_done": 8,
"cover_total": 13,
"cover_done": 5,
"failed": 1,
"generate_cover": True,
"cancelled": True,
}
with mock.patch.object(tab, "refresh_tasks"), \
mock.patch("app.gui.tabs.generate.QMessageBox.warning") as warning:
tab._on_generate_cancelled(payload)
warning.assert_called_once()
self.assertEqual("AI生成已停止", warning.call_args[0][1])
self.assertIn("标题 8/13", warning.call_args[0][2])
self.assertIn("图片 5/13", warning.call_args[0][2])
self.assertIn("未完成任务可再次点击开始生成继续处理", warning.call_args[0][2])
self.assert_removed(temp_dir)
def test_generate_tab_shows_cmhub_balance_and_billing_error(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)