feat: show generate elapsed timings

This commit is contained in:
chengma
2026-07-08 11:54:08 +08:00
parent 091e3dc553
commit 8140124954
4 changed files with 198 additions and 12 deletions
+119 -7
View File
@@ -26,7 +26,7 @@ class GenerateTab(QWidget):
"cover": "封面", "cover": "封面",
"all": "标题和封面", "all": "标题和封面",
} }
TABLE_COLUMN_WEIGHTS = (1.0, 0.5, 1.0, 1.0, 1.25, 1.25) TABLE_COLUMN_WEIGHTS = (1.0, 0.5, 1.8375, 1.8375, 0.4125, 0.4125)
def __init__( def __init__(
self, self,
@@ -54,6 +54,10 @@ class GenerateTab(QWidget):
self.current_cover_template = None self.current_cover_template = None
self.generate_worker = None self.generate_worker = None
self.generate_thread = None self.generate_thread = None
self._title_elapsed_start = None
self._cover_elapsed_start = None
self._title_elapsed_seconds = 0
self._cover_elapsed_seconds = 0
self.title_prompt_edit = QPlainTextEdit() self.title_prompt_edit = QPlainTextEdit()
self.title_prompt_edit.setObjectName("titlePromptEdit") self.title_prompt_edit.setObjectName("titlePromptEdit")
@@ -110,8 +114,23 @@ class GenerateTab(QWidget):
self.cover_progress_bar.setValue(0) 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.cmhub_balance_label = QLabel("cmhub余额:未获取") self.cmhub_balance_label = QLabel("cmhub余额:未获取")
self.cmhub_balance_label.setObjectName("generateCmhubBalanceLabel") self.cmhub_balance_label.setObjectName("generateCmhubBalanceLabel")
self.cmhub_balance_label.setVisible(False)
self.title_elapsed_label = QLabel("生标题用时 0 秒")
self.title_elapsed_label.setObjectName("generateTitleElapsedLabel")
self.cover_elapsed_label = QLabel("生图用时 0 秒")
self.cover_elapsed_label.setObjectName("generateCoverElapsedLabel")
elapsed_width = max(
self.title_elapsed_label.fontMetrics().horizontalAdvance("生标题用时 9999 秒"),
self.cover_elapsed_label.fontMetrics().horizontalAdvance("生图用时 9999 秒"),
) + 12
self.title_elapsed_label.setFixedWidth(elapsed_width)
self.cover_elapsed_label.setFixedWidth(elapsed_width)
self._generate_elapsed_timer = QTimer(self)
self._generate_elapsed_timer.setInterval(1000)
self._generate_elapsed_timer.timeout.connect(self._refresh_elapsed_labels)
left_panel = QWidget() left_panel = QWidget()
left_layout = QVBoxLayout(left_panel) left_layout = QVBoxLayout(left_panel)
@@ -209,11 +228,11 @@ class GenerateTab(QWidget):
title_progress_layout = QHBoxLayout() title_progress_layout = QHBoxLayout()
title_progress_layout.addWidget(self.title_progress_label) title_progress_layout.addWidget(self.title_progress_label)
title_progress_layout.addWidget(self.title_progress_bar, 1) title_progress_layout.addWidget(self.title_progress_bar, 1)
title_progress_layout.addWidget(self.title_elapsed_label)
cover_progress_layout = QHBoxLayout() cover_progress_layout = QHBoxLayout()
cover_progress_layout.addWidget(self.cover_progress_label) cover_progress_layout.addWidget(self.cover_progress_label)
cover_progress_layout.addWidget(self.cover_progress_bar, 1) cover_progress_layout.addWidget(self.cover_progress_bar, 1)
cover_progress_layout.addWidget(self.failed_progress_label) cover_progress_layout.addWidget(self.cover_elapsed_label)
cover_progress_layout.addWidget(self.cmhub_balance_label)
progress_layout = QVBoxLayout() progress_layout = QVBoxLayout()
progress_layout.addLayout(title_progress_layout) progress_layout.addLayout(title_progress_layout)
progress_layout.addLayout(cover_progress_layout) progress_layout.addLayout(cover_progress_layout)
@@ -258,6 +277,7 @@ class GenerateTab(QWidget):
self.refresh_cover_templates() self.refresh_cover_templates()
self.refresh_tasks() self.refresh_tasks()
self._show_current_run_log_empty() self._show_current_run_log_empty()
self._reset_generation_elapsed()
self._update_cmhub_balance_label({}) self._update_cmhub_balance_label({})
def resizeEvent(self, event): def resizeEvent(self, event):
@@ -286,6 +306,92 @@ class GenerateTab(QWidget):
f"本轮AI生成开始:任务 {len(tasks)} 条,生成封面:{cover_text}" f"本轮AI生成开始:任务 {len(tasks)} 条,生成封面:{cover_text}"
) )
def _reset_generation_elapsed(self):
self._generate_elapsed_timer.stop()
self._title_elapsed_start = None
self._cover_elapsed_start = None
self._title_elapsed_seconds = 0
self._cover_elapsed_seconds = 0
self._refresh_elapsed_labels()
def _start_generation_elapsed(self, title_total, cover_total):
self._reset_generation_elapsed()
now = time.monotonic()
if int(title_total or 0) > 0:
self._title_elapsed_start = now
elif int(cover_total or 0) > 0:
self._cover_elapsed_start = now
self._refresh_elapsed_labels()
self._update_elapsed_timer_state()
def _elapsed_seconds(self, start, frozen_seconds):
if start is None:
return max(0, int(frozen_seconds or 0))
return max(0, int(time.monotonic() - start))
def _refresh_elapsed_labels(self):
title_seconds = self._elapsed_seconds(
self._title_elapsed_start,
self._title_elapsed_seconds,
)
cover_seconds = self._elapsed_seconds(
self._cover_elapsed_start,
self._cover_elapsed_seconds,
)
self.title_elapsed_label.setText(f"生标题用时 {title_seconds} 秒")
self.cover_elapsed_label.setText(f"生图用时 {cover_seconds} 秒")
def _update_elapsed_timer_state(self):
if self._title_elapsed_start is not None or self._cover_elapsed_start is not None:
if not self._generate_elapsed_timer.isActive():
self._generate_elapsed_timer.start()
return
self._generate_elapsed_timer.stop()
def _freeze_title_elapsed(self):
if self._title_elapsed_start is None:
return
self._title_elapsed_seconds = self._elapsed_seconds(
self._title_elapsed_start,
self._title_elapsed_seconds,
)
self._title_elapsed_start = None
def _freeze_cover_elapsed(self):
if self._cover_elapsed_start is None:
return
self._cover_elapsed_seconds = self._elapsed_seconds(
self._cover_elapsed_start,
self._cover_elapsed_seconds,
)
self._cover_elapsed_start = None
def _sync_generation_elapsed(self, payload):
title_total = max(0, int(payload.get("title_total", 0) or 0))
title_done = max(0, int(payload.get("title_done", 0) or 0))
cover_total = self._cover_total_for_progress(payload, int(payload.get("total", 0) or 0))
cover_done = max(0, int(payload.get("cover_done", 0) or 0))
if title_total > 0 and title_done >= title_total:
self._freeze_title_elapsed()
if (
cover_total > 0
and cover_done < cover_total
and self._cover_elapsed_start is None
and self._cover_elapsed_seconds == 0
and (title_total == 0 or title_done >= title_total)
):
self._cover_elapsed_start = time.monotonic()
if cover_total > 0 and cover_done >= cover_total:
self._freeze_cover_elapsed()
self._refresh_elapsed_labels()
self._update_elapsed_timer_state()
def _finish_generation_elapsed(self, payload=None):
self._freeze_title_elapsed()
self._freeze_cover_elapsed()
self._refresh_elapsed_labels()
self._update_elapsed_timer_state()
def _on_generate_cover_toggled(self, checked): def _on_generate_cover_toggled(self, checked):
previous = bool(appconfig.ai_config(self.config).get("generate_cover", False)) previous = bool(appconfig.ai_config(self.config).get("generate_cover", False))
if self._save_generate_cover_setting(show_status=True): if self._save_generate_cover_setting(show_status=True):
@@ -543,6 +649,10 @@ class GenerateTab(QWidget):
worker.cancelled.connect(self._on_generate_cancelled) worker.cancelled.connect(self._on_generate_cancelled)
self._reset_generate_run_log(tasks, generate_cover) self._reset_generate_run_log(tasks, generate_cover)
self._reset_cmhub_balance_label() self._reset_cmhub_balance_label()
self._start_generation_elapsed(
component_totals["title_total"],
component_totals["cover_total"] if generate_cover else 0,
)
thread = run_worker(worker, thread_name="GenerateWorker", start=False) thread = run_worker(worker, thread_name="GenerateWorker", start=False)
thread.finished.connect(lambda: self._forget_generate_thread(thread)) thread.finished.connect(lambda: self._forget_generate_thread(thread))
self.generate_worker = worker self.generate_worker = worker
@@ -829,6 +939,7 @@ class GenerateTab(QWidget):
def _on_generate_progress(self, payload): def _on_generate_progress(self, payload):
self._update_generate_progress(payload) self._update_generate_progress(payload)
self._sync_generation_elapsed(payload)
self._update_cmhub_balance_label(payload) self._update_cmhub_balance_label(payload)
self._set_status("生成进度:" + self._generate_progress_text(payload)) self._set_status("生成进度:" + self._generate_progress_text(payload))
@@ -842,6 +953,7 @@ class GenerateTab(QWidget):
self._set_generate_running(False) self._set_generate_running(False)
self.refresh_tasks() self.refresh_tasks()
self._update_generate_progress(payload) self._update_generate_progress(payload)
self._finish_generation_elapsed(payload)
self._update_cmhub_balance_label(payload) self._update_cmhub_balance_label(payload)
billing_error = payload.get("billing_error") billing_error = payload.get("billing_error")
if billing_error: if billing_error:
@@ -856,6 +968,7 @@ class GenerateTab(QWidget):
self._set_generate_running(False) self._set_generate_running(False)
self.refresh_tasks() self.refresh_tasks()
self._update_generate_progress(payload) self._update_generate_progress(payload)
self._finish_generation_elapsed(payload)
self._update_cmhub_balance_label(payload) self._update_cmhub_balance_label(payload)
billing_error = payload.get("billing_error") billing_error = payload.get("billing_error")
if billing_error: if billing_error:
@@ -878,17 +991,16 @@ class GenerateTab(QWidget):
self._set_progress_bar(self.cover_progress_bar, cover_done, cover_total) self._set_progress_bar(self.cover_progress_bar, cover_done, cover_total)
def _reset_cmhub_balance_label(self): def _reset_cmhub_balance_label(self):
self.cmhub_balance_label.setVisible(False)
if self._is_cmhub_backend(): if self._is_cmhub_backend():
self.cmhub_balance_label.setVisible(True)
self.cmhub_balance_label.setText("cmhub余额:生成后刷新") self.cmhub_balance_label.setText("cmhub余额:生成后刷新")
else: else:
self.cmhub_balance_label.setVisible(False) self.cmhub_balance_label.setText("")
def _update_cmhub_balance_label(self, payload): def _update_cmhub_balance_label(self, payload):
self.cmhub_balance_label.setVisible(False)
if not self._is_cmhub_backend(): if not self._is_cmhub_backend():
self.cmhub_balance_label.setVisible(False)
return return
self.cmhub_balance_label.setVisible(True)
balance = payload.get("points_balance") if isinstance(payload, dict) else None balance = payload.get("points_balance") if isinstance(payload, dict) else None
if balance is None: if balance is None:
if not self.cmhub_balance_label.text(): if not self.cmhub_balance_label.text():
+1 -1
View File
@@ -9,7 +9,7 @@ import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
try: try:
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer
from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QAbstractItemView, QAbstractItemView,
+8 -1
View File
@@ -3,7 +3,7 @@ id: T-555
title: ②AI生成列宽再分配与标题/生图用时计时 title: ②AI生成列宽再分配与标题/生图用时计时
phase: 7 phase: 7
deps: [T-554, T-519, T-523] deps: [T-554, T-519, T-523]
status: TODO status: DONE
created: 2026-07-08 created: 2026-07-08
--- ---
@@ -61,3 +61,10 @@ T-554 已把②「AI生成」任务表格从一个「状态」列拆成「标题
只改②AI生成 GUI 展示和测试;不改 AI 生成流程、cmhub HTTP、并发策略、DB schema、运行日志持久化、计费 metadata、Excel、Shopee/CDP。失败数量和 cmhub 余额不从系统中删除,只是不再显示在两条进度条右侧。 只改②AI生成 GUI 展示和测试;不改 AI 生成流程、cmhub HTTP、并发策略、DB schema、运行日志持久化、计费 metadata、Excel、Shopee/CDP。失败数量和 cmhub 余额不从系统中删除,只是不再显示在两条进度条右侧。
## 执行记录
- 2026-07-08:完成②AI生成表格列宽再分配,权重调整为 `店铺=1.0 / 商品ID=0.5 / 旧标题=1.8375 / 新标题=1.8375 / 标题状态=0.4125 / 图片状态=0.4125`。
- 2026-07-08:底部两条进度条右侧改为固定宽度用时标签 `生标题用时 N 秒` / `生图用时 N 秒`,由 GUI 层 `QTimer` 每秒刷新;开始生成时重置,标题阶段结束后冻结标题用时,图片阶段开始后计生图用时,完成/停止/计费中止后冻结。
- 2026-07-08:`失败 X` 与 `cmhub余额:X` 不再占用进度条右侧;失败数仍保留在进度摘要、表格状态、运行日志和结束汇总,cmhub 余额文本仍在内部按 payload 更新但隐藏展示,后续仍可通过设置页/日志入口查看。
- 2026-07-08:更新 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`。
+70 -3
View File
@@ -1628,9 +1628,15 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("标题 0/0", tab.title_progress_label.text()) self.assertEqual("标题 0/0", tab.title_progress_label.text())
self.assertEqual("图片 0/0", tab.cover_progress_label.text()) self.assertEqual("图片 0/0", tab.cover_progress_label.text())
self.assertEqual("失败 0", tab.failed_progress_label.text()) self.assertEqual("失败 0", tab.failed_progress_label.text())
self.assertTrue(tab.failed_progress_label.isHidden())
self.assertEqual("generateTitleElapsedLabel", tab.title_elapsed_label.objectName())
self.assertEqual("generateCoverElapsedLabel", tab.cover_elapsed_label.objectName())
self.assertEqual("生标题用时 0 秒", tab.title_elapsed_label.text())
self.assertEqual("生图用时 0 秒", tab.cover_elapsed_label.text())
self.assertEqual(tab.title_elapsed_label.width(), tab.cover_elapsed_label.width())
self.assertEqual("generateCmhubBalanceLabel", tab.cmhub_balance_label.objectName()) self.assertEqual("generateCmhubBalanceLabel", tab.cmhub_balance_label.objectName())
self.assertEqual("cmhub余额:未获取", tab.cmhub_balance_label.text()) self.assertEqual("cmhub余额:未获取", tab.cmhub_balance_label.text())
self.assertFalse(tab.cmhub_balance_label.isHidden()) self.assertTrue(tab.cmhub_balance_label.isHidden())
self.assertEqual(0, tab.title_progress_bar.value()) self.assertEqual(0, tab.title_progress_bar.value())
self.assertEqual(0, tab.cover_progress_bar.value()) self.assertEqual(0, tab.cover_progress_bar.value())
self.assertEqual(1, tab.title_progress_bar.maximum()) self.assertEqual(1, tab.title_progress_bar.maximum())
@@ -1763,7 +1769,67 @@ class GuiTests(TempDirMixin, unittest.TestCase):
header.sectionSize(5), header.sectionSize(5),
delta=5, delta=5,
) )
self.assertGreater(header.sectionSize(4), header.sectionSize(0)) self.assertAlmostEqual(
header.sectionSize(2),
header.sectionSize(3),
delta=5,
)
self.assertLess(header.sectionSize(4), header.sectionSize(1))
self.assertGreater(header.sectionSize(2), header.sectionSize(0))
self.assert_removed(temp_dir)
def test_generate_elapsed_timer_tracks_title_then_cover(self):
with self.make_temp_dir() as temp_dir:
tab = GenerateTab(config=self.make_config(temp_dir))
self.addCleanup(tab.close)
self.assertEqual("生标题用时 0 秒", tab.title_elapsed_label.text())
self.assertEqual("生图用时 0 秒", tab.cover_elapsed_label.text())
self.assertFalse(tab._generate_elapsed_timer.isActive())
with mock.patch("app.gui.tabs.generate.time.monotonic", return_value=100.0):
tab._start_generation_elapsed(title_total=2, cover_total=1)
self.assertTrue(tab._generate_elapsed_timer.isActive())
with mock.patch("app.gui.tabs.generate.time.monotonic", return_value=103.0):
tab._refresh_elapsed_labels()
self.assertEqual("生标题用时 3 秒", tab.title_elapsed_label.text())
self.assertEqual("生图用时 0 秒", tab.cover_elapsed_label.text())
with mock.patch("app.gui.tabs.generate.time.monotonic", return_value=105.0):
tab._sync_generation_elapsed(
{
"total": 3,
"title_total": 2,
"title_done": 2,
"cover_total": 1,
"cover_done": 0,
"generate_cover": True,
}
)
self.assertEqual("生标题用时 5 秒", tab.title_elapsed_label.text())
self.assertEqual("生图用时 0 秒", tab.cover_elapsed_label.text())
with mock.patch("app.gui.tabs.generate.time.monotonic", return_value=109.0):
tab._refresh_elapsed_labels()
self.assertEqual("生标题用时 5 秒", tab.title_elapsed_label.text())
self.assertEqual("生图用时 4 秒", tab.cover_elapsed_label.text())
with mock.patch("app.gui.tabs.generate.time.monotonic", return_value=112.0):
tab._finish_generation_elapsed(
{
"total": 3,
"title_total": 2,
"title_done": 2,
"cover_total": 1,
"cover_done": 1,
"generate_cover": True,
}
)
self.assertEqual("生标题用时 5 秒", tab.title_elapsed_label.text())
self.assertEqual("生图用时 7 秒", tab.cover_elapsed_label.text())
self.assertFalse(tab._generate_elapsed_timer.isActive())
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
@@ -1776,7 +1842,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab = GenerateTab(config=cfg, status_callback=statuses.append) tab = GenerateTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close) self.addCleanup(tab.close)
self.assertFalse(tab.cmhub_balance_label.isHidden()) self.assertTrue(tab.cmhub_balance_label.isHidden())
self.assertEqual("cmhub余额:未获取", tab.cmhub_balance_label.text()) self.assertEqual("cmhub余额:未获取", tab.cmhub_balance_label.text())
tab._on_generate_progress( tab._on_generate_progress(
@@ -1791,6 +1857,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
} }
) )
self.assertEqual("cmhub余额:88", tab.cmhub_balance_label.text()) self.assertEqual("cmhub余额:88", tab.cmhub_balance_label.text())
self.assertTrue(tab.cmhub_balance_label.isHidden())
with mock.patch("app.gui.QMessageBox.warning") as warning: with mock.patch("app.gui.QMessageBox.warning") as warning:
tab._on_generate_finished( tab._on_generate_finished(