feat(gui): replace AI studio with product suite

This commit is contained in:
chengma
2026-07-14 09:53:13 +08:00
parent fb873aae90
commit bc115ba0d7
27 changed files with 3453 additions and 81 deletions
+157 -1
View File
@@ -7,7 +7,7 @@ import re
import threading
import time
from .. import image_studio, image_studio_export, image_studio_generation, image_studio_images
from .. import ai, image_studio, image_studio_export, image_studio_generation, image_studio_images
from ..collect_skip import ALIAS_UNMATCHED, LOGIN_REQUIRED, empty_skip_reason_counts
from .widgets import *
@@ -67,6 +67,10 @@ def _format_image_studio_event(event):
return prefix
def _format_product_suite_event(event):
return _format_image_studio_event(event).replace("[AI工场]", "[商品套图]", 1)
def _image_studio_user_detail(detail):
text = diagnostics.redact_log_text(str(detail or "")).replace("\r", " ").replace("\n", " ").strip()
text = _USER_LOG_URL_RE.sub("[链接已隐藏]", text)
@@ -286,6 +290,158 @@ class ImageStudioGenerateJobsWorker(BaseWorker):
return summary
class ProductSuiteGenerateWorker(BaseWorker):
"""Create and run independently configured product-suite jobs."""
def __init__(
self,
project_id,
job_specs,
*,
aspect_ratio="1:1",
db_path=None,
config=None,
cmhub_config_path=None,
):
super().__init__()
self.project_id = int(project_id)
self.job_specs = [dict(spec) for spec in (job_specs or [])]
self.aspect_ratio = str(aspect_ratio or "1:1")
self.db_path = db_path
self.config = config
self.cmhub_config_path = cmhub_config_path
self._done = 0
self._failed = 0
self._lock = threading.Lock()
def execute(self):
total = len(self.job_specs)
if total <= 0:
raise ValueError("商品套图生成任务不能为空")
jobs = []
for spec in self.job_specs:
if self.should_cancel():
break
jobs.append(
image_studio.create_job(
self.project_id,
source_asset_id=spec.get("source_asset_id"),
job_type=spec.get("job_type") or "套图",
prompt=spec.get("prompt") or "",
generation_source="cmhub",
provider="cmhub",
path=self.db_path,
)
)
self.progress.emit(
{
"total": len(jobs),
"done": 0,
"failed": 0,
"job_ids": [job.id for job in jobs],
}
)
def on_event(payload):
event = dict(payload or {})
self.log.emit(_format_product_suite_event(event))
if event.get("step") == "job_done":
with self._lock:
self._done += 1
if event.get("result") != "success":
self._failed += 1
progress = {
"total": len(jobs),
"done": self._done,
"failed": self._failed,
"job_ids": [job.id for job in jobs],
}
self.progress.emit(progress)
summary = image_studio_generation.run_jobs(
jobs,
aspect_ratio=self.aspect_ratio,
config=self.config,
cmhub_config_path=self.cmhub_config_path,
path=self.db_path,
should_stop=self.should_cancel,
on_event=on_event,
)
summary["project_id"] = self.project_id
summary["job_ids"] = [job.id for job in jobs]
return summary
class ProductSuiteAiWriteWorker(BaseWorker):
"""Generate product selling-point copy without blocking the suite workspace."""
def __init__(
self,
instruction,
context,
*,
config=None,
cmhub_config_path=None,
):
super().__init__()
self.instruction = str(instruction or "")
self.context = str(context or "")
self.config = config
self.cmhub_config_path = cmhub_config_path
def execute(self):
if self.should_cancel():
return {"cancelled": True}
text = ai.gen_title(
self.instruction,
self.context,
config=self.config,
cmhub_config_path=self.cmhub_config_path,
)
if self.should_cancel():
return {"cancelled": True}
return {"text": str(text or "").strip()}
class ProductSuiteImportImagesWorker(BaseWorker):
"""Validate and copy local product images outside the GUI thread."""
def __init__(
self,
project_id,
*,
file_paths=None,
image_bytes=None,
filename_hint="clipboard.png",
db_path=None,
config=None,
):
super().__init__()
self.project_id = int(project_id)
self.file_paths = list(file_paths or [])
self.image_bytes = bytes(image_bytes) if image_bytes is not None else None
self.filename_hint = str(filename_hint or "clipboard.png")
self.db_path = db_path
self.config = config
def execute(self):
if self.image_bytes is not None:
asset = image_studio_images.import_original_bytes(
self.project_id,
self.image_bytes,
filename_hint=self.filename_hint,
path=self.db_path,
config=self.config,
)
return {"assets": [asset], "errors": [], "limit": 16}
return image_studio_images.import_original_files(
self.project_id,
self.file_paths,
path=self.db_path,
config=self.config,
)
class ImageStudioResumeJobsWorker(BaseWorker):
"""Resume submitted/running or failed-download AI studio jobs."""