feat: add optional cover generation toggle

This commit is contained in:
chengma
2026-07-02 15:17:11 +08:00
parent f9c6dd3331
commit 21cd02c167
13 changed files with 348 additions and 48 deletions
+37 -1
View File
@@ -194,6 +194,7 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
"retry",
"jpg_quality",
"resolution",
"generate_cover",
}
}
)
@@ -206,6 +207,7 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
on_error = runtime.get("on_error")
title_prompt = _prompt_value(prompts, "title")
cover_prompt = _prompt_value(prompts, "cover")
generate_cover = bool(generation_cfg.get("generate_cover", False))
should_stop = should_stop or (lambda: False)
eligible = [
task for task in list(tasks)
@@ -216,8 +218,11 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
"total": len(eligible),
"title_done": 0,
"cover_done": 0,
"cover_total": len(eligible) if generate_cover else 0,
"generated_done": 0,
"failed": 0,
"cancelled": False,
"generate_cover": generate_cover,
}
_emit_generation_progress(on_progress, summary)
title_results = {}
@@ -281,10 +286,34 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
summary["cancelled"] = True
_cancel_pending(futures)
try:
title_results[task.id] = future.result()
new_title = future.result()
title_results[task.id] = new_title
summary["title_done"] += 1
set_step(task, "title_done")
_emit_generation_event(on_event, task, "title", "title_done", "success")
if not generate_cover:
set_step(task, "db_write")
_emit_generation_event(on_event, task, "title", "db_write", "start")
db.set_generated(task.id, new_title, None, path=db_path)
summary["generated_done"] += 1
if on_task_update is not None:
on_task_update(
task.id,
{
"stage": "generated",
"status": "success",
"new_title": new_title,
"new_cover_path": None,
},
)
_emit_generation_event(
on_event,
task,
"title",
"db_write",
"success",
detail="仅生成标题",
)
except CancelledError:
summary["cancelled"] = True
_emit_generation_event(on_event, task, "title", get_step(task, "title_request"), "cancelled", level="warning")
@@ -297,6 +326,11 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
_emit_generation_error(on_error, task, "title", step, exc, error)
_emit_generation_progress(on_progress, summary)
if not generate_cover:
if summary["cancelled"]:
summary["ok"] = False
return summary
cover_tasks = [
task for task in eligible
if task.id in title_results
@@ -353,6 +387,7 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
_emit_generation_event(on_event, task, "cover", "db_write", "start")
db.set_generated(task.id, new_title, new_cover_path, path=db_path)
summary["cover_done"] += 1
summary["generated_done"] += 1
if on_task_update is not None:
on_task_update(
task.id,
@@ -413,6 +448,7 @@ def _runtime_config(runtime):
"retry",
"jpg_quality",
"resolution",
"generate_cover",
"resolution_timeouts",
}
}
+1
View File
@@ -28,6 +28,7 @@ DEFAULT_CONFIG = {
"ai": {
"default_text_model": "GPT-5.5 文本",
"default_image_model": "Nano Banana 2",
"generate_cover": False,
"title_concurrency": 4,
"image_concurrency": 4,
"retry": 2,
+109 -14
View File
@@ -782,13 +782,19 @@ if QT_IMPORT_ERROR is None:
parent=None,
db_path=None,
config=None,
config_path=None,
status_callback=None,
title_prompt_path=None,
cover_prompts_dir=None,
open_accounts_callback=None,
):
super().__init__(parent)
self.config = appconfig.load_config() if config is None else config
self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config
self.config_path = (
config_path
or self.config.get("config_path")
or appconfig.CONFIG_PATH
)
self.db_path = _database_path(db_path, self.config)
self.status_callback = status_callback
self.open_accounts_callback = open_accounts_callback
@@ -829,7 +835,12 @@ if QT_IMPORT_ERROR is None:
self.reset_generate_button = QPushButton("重置生成结果")
self.reset_generate_button.setObjectName("resetGenerateButton")
self.stop_generate_button.setEnabled(False)
self.progress_label = QLabel("进度:标题0/0 · 封面0/0 · 失败0")
ai_settings = appconfig.ai_config(self.config)
self.generate_cover_checkbox = QCheckBox("生成封面图片(成本较高)")
self.generate_cover_checkbox.setObjectName("generateCoverCheckbox")
self.generate_cover_checkbox.setToolTip("关闭后只生成标题并保存为可更新,不调用图片模型")
self.generate_cover_checkbox.setChecked(bool(ai_settings.get("generate_cover", False)))
self.progress_label = QLabel("进度:标题0/0 · 图片0/0 · 失败0")
self.title_progress_label = QLabel("标题 0/0")
self.title_progress_label.setObjectName("generateTitleProgressLabel")
self.title_progress_bar = QProgressBar()
@@ -951,6 +962,7 @@ if QT_IMPORT_ERROR is None:
button_layout.addWidget(self.reset_generate_button)
bottom_layout = QHBoxLayout()
bottom_layout.addWidget(self.generate_cover_checkbox)
bottom_layout.addLayout(progress_layout, 1)
bottom_layout.addLayout(button_layout)
@@ -973,6 +985,7 @@ if QT_IMPORT_ERROR is None:
self.delete_cover_template_action.triggered.connect(self.delete_cover_template)
self.insert_title_button.clicked.connect(self.insert_title_placeholder)
self.preview_prompt_button.clicked.connect(self.preview_cover_prompt)
self.generate_cover_checkbox.toggled.connect(self._on_generate_cover_toggled)
self.generate_button.clicked.connect(self.start_generate)
self.stop_generate_button.clicked.connect(self.stop_generate)
self.reset_generate_button.clicked.connect(self.reset_generated_result)
@@ -995,6 +1008,44 @@ if QT_IMPORT_ERROR is None:
scroll_bar = self.run_log_view.verticalScrollBar()
scroll_bar.setValue(scroll_bar.maximum())
def _on_generate_cover_toggled(self, checked):
previous = bool(appconfig.ai_config(self.config).get("generate_cover", False))
if self._save_generate_cover_setting(show_status=True):
return
self.generate_cover_checkbox.blockSignals(True)
self.generate_cover_checkbox.setChecked(previous)
self.generate_cover_checkbox.blockSignals(False)
def _save_generate_cover_setting(self, show_status=True):
generate_cover = bool(self.generate_cover_checkbox.isChecked())
ai_settings = appconfig.ai_config(self.config)
ai_settings["generate_cover"] = generate_cover
payload = {
key: value
for key, value in self.config.items()
if key not in {"config_path", "ai_models_path"}
}
payload["ai"] = ai_settings
try:
saved = appconfig.save_config(payload, path=self.config_path)
except Exception as exc:
self._set_status(f"生成封面开关保存失败:{exc}")
return False
internal = {
key: value
for key, value in self.config.items()
if key in {"config_path", "ai_models_path"}
}
self.config.clear()
self.config.update(saved)
self.config.update(internal)
if self.config_path != appconfig.CONFIG_PATH:
self.config["config_path"] = self.config_path
if show_status:
mode = "会同时生成封面图片" if generate_cover else "只生成标题,不生成图片"
self._set_status(f"AI生成设置已保存:{mode}")
return True
def _load_latest_generate_run_log(self):
try:
logs = db.list_run_logs(limit=1, run_type="generate", path=self.db_path)
@@ -1155,6 +1206,9 @@ if QT_IMPORT_ERROR is None:
if self.generate_thread is not None:
self._set_status("AI 生成正在进行...")
return
if not self._save_generate_cover_setting(show_status=False):
return
generate_cover = bool(self.generate_cover_checkbox.isChecked())
tasks = [
task for task in self.model.tasks
if getattr(task, "stage", None) == "collected"
@@ -1186,7 +1240,15 @@ if QT_IMPORT_ERROR is None:
self.generate_thread = thread
self._set_generate_running(True)
self._update_generate_progress(
{"total": len(tasks), "title_done": 0, "cover_done": 0, "failed": 0}
{
"total": len(tasks),
"title_done": 0,
"cover_done": 0,
"cover_total": len(tasks) if generate_cover else 0,
"generated_done": 0,
"failed": 0,
"generate_cover": generate_cover,
}
)
self._set_status(f"开始 AI 生成:{len(tasks)} 条")
thread.start()
@@ -1314,6 +1376,7 @@ if QT_IMPORT_ERROR is None:
self.save_cover_template_as_action.setEnabled(not running)
self.rename_cover_template_action.setEnabled(not running)
self.delete_cover_template_action.setEnabled(not running)
self.generate_cover_checkbox.setEnabled(not running)
def _forget_generate_thread(self, thread):
if self.generate_thread is thread:
@@ -1351,13 +1414,14 @@ if QT_IMPORT_ERROR is None:
total = max(0, int(payload.get("total", 0) or 0))
title_done = max(0, int(payload.get("title_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)
failed = max(0, int(payload.get("failed", 0) or 0))
self.progress_label.setText("进度:" + self._generate_progress_text(payload))
self.title_progress_label.setText(f"标题 {title_done}/{total}")
self.cover_progress_label.setText(f"图片 {cover_done}/{total}")
self.cover_progress_label.setText(f"图片 {cover_done}/{cover_total}")
self.failed_progress_label.setText(f"失败 {failed}")
self._set_progress_bar(self.title_progress_bar, title_done, total)
self._set_progress_bar(self.cover_progress_bar, cover_done, total)
self._set_progress_bar(self.cover_progress_bar, cover_done, cover_total)
def _set_progress_bar(self, bar, done, total):
maximum = max(1, int(total or 0))
@@ -1365,10 +1429,19 @@ if QT_IMPORT_ERROR is None:
bar.setRange(0, maximum)
bar.setValue(value)
def _cover_total_for_progress(self, payload, total):
cover_total = payload.get("cover_total")
if cover_total is None:
cover_total = total if payload.get("generate_cover", True) else 0
return max(0, int(cover_total or 0))
def _generate_progress_text(self, payload):
return "标题{title}/{total} · 封面{cover}/{total} · 失败{failed}".format(
total = max(0, int(payload.get("total", 0) or 0))
cover_total = self._cover_total_for_progress(payload, total)
return "标题{title}/{total} · 图片{cover}/{cover_total} · 失败{failed}".format(
title=payload.get("title_done", 0),
cover=payload.get("cover_done", 0),
cover_total=cover_total,
total=payload.get("total", 0),
failed=payload.get("failed", 0),
)
@@ -3294,13 +3367,19 @@ if QT_IMPORT_ERROR is None:
batch_ids = self._batch_ids(eligible)
self._run_id = self._create_run_log(eligible, batch_ids)
ai_cfg = appconfig.ai_config(self.config)
self._log_run_event(
"[开始] 本轮生成 {total} 条:标题{total},图片{total};标题并发{title_concurrency},图片并发{image_concurrency}".format(
generate_cover = bool(ai_cfg.get("generate_cover", False))
if generate_cover:
start_message = "[开始] 本轮生成 {total} 条:标题{total},图片{total};标题并发{title_concurrency},图片并发{image_concurrency}".format(
total=len(eligible),
title_concurrency=ai_cfg.get("title_concurrency", 1),
image_concurrency=ai_cfg.get("image_concurrency", 1),
)
)
else:
start_message = "[开始] 本轮生成 {total} 条:本轮仅生成标题,不生成图片;标题并发{title_concurrency}".format(
total=len(eligible),
title_concurrency=ai_cfg.get("title_concurrency", 1),
)
self._log_run_event(start_message)
try:
summary = ai.generate_batch(
self.tasks,
@@ -3313,6 +3392,7 @@ if QT_IMPORT_ERROR is None:
"on_task_update": self._emit_row_update,
"on_event": self._on_generation_event,
"on_error": self._on_generation_error,
"generate_cover": generate_cover,
},
on_progress=self.progress.emit,
should_stop=self.should_cancel,
@@ -3325,8 +3405,11 @@ if QT_IMPORT_ERROR is None:
"total": len(eligible),
"title_done": 0,
"cover_done": 0,
"cover_total": len(eligible) if generate_cover else 0,
"generated_done": 0,
"failed": len(eligible),
"cancelled": self.should_cancel(),
"generate_cover": generate_cover,
}
self._log_run_event(
f"[失败] AI 生成运行失败:{error}",
@@ -3368,6 +3451,9 @@ if QT_IMPORT_ERROR is None:
return f"[标题] {self._task_progress_label(task)} 开始生成"
if result == "success" and step == "title_done":
return f"[标题] {self._task_progress_label(task)} 成功"
if result == "success" and step == "db_write":
suffix = f",{detail}" if detail else ""
return f"[标题] {self._task_progress_label(task)} 已保存{suffix}"
if result == "retry":
return self._retry_message("标题", task, payload, detail)
if result == "failed":
@@ -3435,9 +3521,11 @@ if QT_IMPORT_ERROR is None:
return f"[完成] AI 生成完成:{progress}"
def _summary_text(self, summary):
return "标题{title}/{total},图片{cover}/{total},失败{failed}".format(
cover_total = summary.get("cover_total", summary.get("total", 0))
return "标题{title}/{total},图片{cover}/{cover_total},失败{failed}".format(
title=summary.get("title_done", 0),
cover=summary.get("cover_done", 0),
cover_total=cover_total,
total=summary.get("total", 0),
failed=summary.get("failed", 0),
)
@@ -3478,6 +3566,7 @@ if QT_IMPORT_ERROR is None:
"resolution": ai_cfg.get("resolution"),
"title_concurrency": ai_cfg.get("title_concurrency"),
"image_concurrency": ai_cfg.get("image_concurrency"),
"generate_cover": ai_cfg.get("generate_cover", False),
},
path=self.db_path,
)
@@ -3488,12 +3577,17 @@ if QT_IMPORT_ERROR is None:
if self._run_id is None:
return
try:
done = int(summary.get("cover_done", 0) or 0) + int(summary.get("failed", 0) or 0)
generated_done = summary.get("generated_done")
if generated_done is None:
generated_done = summary.get("cover_done", 0)
if not summary.get("generate_cover", True) and not generated_done:
generated_done = summary.get("title_done", 0)
done = int(generated_done or 0) + int(summary.get("failed", 0) or 0)
db.finish_run_log(
self._run_id,
status=status,
done=done,
success_count=summary.get("cover_done", 0),
success_count=generated_done,
skipped_count=0,
failed_count=summary.get("failed", 0),
summary_json=summary,
@@ -6125,13 +6219,13 @@ if QT_IMPORT_ERROR is None:
def __init__(self, db_path=None, config=None, config_path=None, ai_models_path=None):
super().__init__()
self.config = appconfig.load_config() if config is None else config
self.db_path = _database_path(db_path, self.config)
self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config
self.config_path = (
config_path
or self.config.get("config_path")
or appconfig.CONFIG_PATH
)
self.db_path = _database_path(db_path, self.config)
self.ai_models_path = (
ai_models_path
or self.config.get("ai_models_path")
@@ -6162,6 +6256,7 @@ if QT_IMPORT_ERROR is None:
return GenerateTab(
db_path=self.db_path,
config=self.config,
config_path=self.config_path,
status_callback=self.statusBar().showMessage,
open_accounts_callback=lambda: self.open_accounts_tab(),
)