T-575 优化AI生成图片失败反馈
This commit is contained in:
+116
-7
@@ -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
@@ -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,非空白错误)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user