T-575 优化AI生成图片失败反馈
This commit is contained in:
+116
-7
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
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 ..models import GenerateTaskTableModel
|
||||||
from ..widgets import *
|
from ..widgets import *
|
||||||
@@ -71,6 +72,63 @@ class OriginalImageDialog(QDialog):
|
|||||||
_center_dialog(self, available)
|
_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):
|
class CoverGalleryDialog(QDialog):
|
||||||
THUMBNAIL_SIZE = 180
|
THUMBNAIL_SIZE = 180
|
||||||
OLD_THUMBNAIL_SIZE = 300
|
OLD_THUMBNAIL_SIZE = 300
|
||||||
@@ -867,11 +925,9 @@ class GenerateTab(QWidget):
|
|||||||
self.title_progress_bar.setValue(0)
|
self.title_progress_bar.setValue(0)
|
||||||
self.cover_progress_label = QLabel("图片 0/0")
|
self.cover_progress_label = QLabel("图片 0/0")
|
||||||
self.cover_progress_label.setObjectName("generateCoverProgressLabel")
|
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.setObjectName("generateCoverProgressBar")
|
||||||
self.cover_progress_bar.setTextVisible(False)
|
self.cover_progress_bar.set_segments(0, 0, 0)
|
||||||
self.cover_progress_bar.setRange(0, 1)
|
|
||||||
self.cover_progress_bar.setValue(0)
|
|
||||||
self.failed_progress_label = QLabel("失败 0")
|
self.failed_progress_label = QLabel("失败 0")
|
||||||
self.failed_progress_label.setObjectName("generateFailedProgressLabel")
|
self.failed_progress_label.setObjectName("generateFailedProgressLabel")
|
||||||
self.failed_progress_label.setVisible(False)
|
self.failed_progress_label.setVisible(False)
|
||||||
@@ -1732,8 +1788,10 @@ class GenerateTab(QWidget):
|
|||||||
return
|
return
|
||||||
if payload.get("error"):
|
if payload.get("error"):
|
||||||
self._set_status(f"AI 生成失败:{payload.get('error')}")
|
self._set_status(f"AI 生成失败:{payload.get('error')}")
|
||||||
|
self._show_generate_completion_message(payload, error=True)
|
||||||
return
|
return
|
||||||
self._set_status("AI 生成完成:" + self._generate_progress_text(payload))
|
self._set_status("AI 生成完成:" + self._generate_progress_text(payload))
|
||||||
|
self._show_generate_completion_message(payload)
|
||||||
|
|
||||||
def _on_generate_cancelled(self, payload):
|
def _on_generate_cancelled(self, payload):
|
||||||
self._set_generate_running(False)
|
self._set_generate_running(False)
|
||||||
@@ -1746,6 +1804,7 @@ class GenerateTab(QWidget):
|
|||||||
self._show_billing_error(billing_error)
|
self._show_billing_error(billing_error)
|
||||||
return
|
return
|
||||||
self._set_status("AI 生成已停止:" + self._generate_progress_text(payload))
|
self._set_status("AI 生成已停止:" + self._generate_progress_text(payload))
|
||||||
|
self._show_generate_completion_message(payload, cancelled=True)
|
||||||
|
|
||||||
def _update_generate_progress(self, payload):
|
def _update_generate_progress(self, payload):
|
||||||
total = max(0, int(payload.get("total", 0) or 0))
|
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_done = max(0, int(payload.get("cover_done", 0) or 0))
|
||||||
cover_total = self._cover_total_for_progress(payload, total)
|
cover_total = self._cover_total_for_progress(payload, total)
|
||||||
failed = max(0, int(payload.get("failed", 0) or 0))
|
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.progress_label.setText("进度:" + self._generate_progress_text(payload))
|
||||||
self.title_progress_label.setText(f"标题 {title_done}/{title_total}")
|
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.failed_progress_label.setText(f"失败 {failed}")
|
||||||
self._set_progress_bar(self.title_progress_bar, title_done, title_total)
|
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):
|
def _reset_cmhub_balance_label(self):
|
||||||
self.cmhub_balance_label.setVisible(False)
|
self.cmhub_balance_label.setVisible(False)
|
||||||
@@ -1790,6 +1851,44 @@ class GenerateTab(QWidget):
|
|||||||
QMessageBox.warning(self, "AI生成", message)
|
QMessageBox.warning(self, "AI生成", message)
|
||||||
self._set_status("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):
|
def _set_progress_bar(self, bar, done, total):
|
||||||
maximum = max(1, int(total or 0))
|
maximum = max(1, int(total or 0))
|
||||||
value = min(max(0, int(done or 0)), maximum)
|
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
|
cover_total = total if payload.get("generate_cover", True) else 0
|
||||||
return max(0, int(cover_total or 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):
|
def _generate_progress_text(self, payload):
|
||||||
total = max(0, int(payload.get("total", 0) or 0))
|
total = max(0, int(payload.get("total", 0) or 0))
|
||||||
title_total = max(0, int(payload.get("title_total", total) 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生成图片失败红色进度段与完成汇总弹窗
|
title: ②AI生成图片失败红色进度段与完成汇总弹窗
|
||||||
phase: 7
|
phase: 7
|
||||||
deps: [T-574]
|
deps: [T-574]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-09
|
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)
|
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):
|
def test_generate_tab_shows_cmhub_balance_and_billing_error(self):
|
||||||
with self.make_temp_dir() as temp_dir:
|
with self.make_temp_dir() as temp_dir:
|
||||||
cfg = self.make_config(temp_dir)
|
cfg = self.make_config(temp_dir)
|
||||||
|
|||||||
Reference in New Issue
Block a user