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
+3 -1
View File
@@ -56,6 +56,7 @@ from app.gui import (
ForcedUpdateDialog,
ImageStudioTab,
MainWindow,
ProductSuiteTab,
SettingsTab,
TAB_STYLE,
TAB_TITLES,
@@ -491,7 +492,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.addCleanup(window.close)
self.assertEqual(gui.display_name(), window.windowTitle())
self.assertEqual(5, window.tabs.count())
self.assertEqual(6, window.tabs.count())
self.assertEqual(
TAB_TITLES,
[window.tabs.tabText(index) for index in range(window.tabs.count())],
@@ -511,6 +512,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertIsInstance(window.tabs.widget(2), ApplyTab)
self.assertIsInstance(window.tabs.widget(3), AccountsTab)
self.assertIsInstance(window.tabs.widget(4), SettingsTab)
self.assertIsInstance(window.tabs.widget(5), ProductSuiteTab)
self.assertFalse(
any(
isinstance(window.tabs.widget(index), ImageStudioTab)
+157
View File
@@ -59,6 +59,7 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
"item_id",
"target_main_count",
"target_detail_count",
"suite_settings_json",
"deleted_at",
}.issubset(projects_columns)
)
@@ -72,6 +73,50 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_init_db_migrates_legacy_projects_with_default_suite_settings(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "legacy-project.db")
conn = db.connect(db_path)
try:
conn.execute(
"""
CREATE TABLE image_studio_projects (
id INTEGER PRIMARY KEY,
account_alias TEXT NOT NULL,
account_slug TEXT NOT NULL,
account_name TEXT,
item_id TEXT NOT NULL,
target_main_count INTEGER NOT NULL DEFAULT 9,
target_detail_count INTEGER NOT NULL DEFAULT 12,
draft_prompt TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deleted_at TEXT,
deleted_reason TEXT,
UNIQUE(account_alias, item_id)
)
"""
)
conn.execute(
"""
INSERT INTO image_studio_projects
(id, account_alias, account_slug, item_id, created_at, updated_at)
VALUES (1, 'alias', 'alias_slug', '51100639510', '2026-07-14', '2026-07-14')
"""
)
conn.commit()
finally:
conn.close()
db.init_db(db_path)
project = image_studio.get_project(1, path=db_path)
self.assertEqual("{}", project.suite_settings_json)
self.assertEqual({}, image_studio.project_suite_settings(project))
self.assert_removed(temp_dir)
def test_project_crud_unique_per_account_and_image_dirs(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
@@ -110,6 +155,15 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
updated = image_studio.update_project_prompt(project.id, "二次提示词", path=db_path)
self.assertEqual("二次提示词", updated.draft_prompt)
suite_updated = image_studio.update_project_suite_settings(
project.id,
{"ratio": "3:4", "categories": {"白底图": 1}},
path=db_path,
)
self.assertEqual(
{"ratio": "3:4", "categories": {"白底图": 1}},
image_studio.project_suite_settings(suite_updated),
)
dirs = image_studio.project_image_dirs(os.path.join(temp_dir, "images"), project)
self.assertEqual(
@@ -215,6 +269,43 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_reorder_original_assets_requires_complete_project_order(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
db.init_db(db_path)
project = image_studio.create_or_get_project(
account_alias="alias",
account_slug="alias_slug",
item_id="51100639510",
path=db_path,
)
first = image_studio.add_asset(
project.id,
image_studio.ASSET_KIND_ORIGINAL,
local_path=os.path.join(temp_dir, "first.png"),
source_order=1,
path=db_path,
)
second = image_studio.add_asset(
project.id,
image_studio.ASSET_KIND_ORIGINAL,
local_path=os.path.join(temp_dir, "second.png"),
source_order=2,
path=db_path,
)
reordered = image_studio.reorder_original_assets(
project.id,
[second.id, first.id],
path=db_path,
)
self.assertEqual([second.id, first.id], [asset.id for asset in reordered])
with self.assertRaisesRegex(db.DbError, "全部原图"):
image_studio.reorder_original_assets(project.id, [first.id], path=db_path)
self.assert_removed(temp_dir)
def test_remove_asset_only_when_not_referenced(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
@@ -287,6 +378,55 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_sync_original_asset_urls_preserves_local_upload_and_caps_active_assets(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
db.init_db(db_path)
project = image_studio.create_or_get_project(
account_alias="alias",
account_slug="alias_slug",
item_id="51100639510",
path=db_path,
)
local = image_studio.add_asset(
project.id,
image_studio.ASSET_KIND_ORIGINAL,
local_path=os.path.join(temp_dir, "local.png"),
source_order=1,
path=db_path,
)
image_studio.sync_original_asset_urls(
project.id,
[
{"index": index, "src": "https://susercontent.com/%d.jpg" % index}
for index in range(1, 8)
],
max_assets=3,
path=db_path,
)
active = image_studio.list_assets(
project.id,
kind=image_studio.ASSET_KIND_ORIGINAL,
include_missing=False,
path=db_path,
)
self.assertEqual(3, len(active))
self.assertIn(local.id, [asset.id for asset in active])
self.assertEqual(
image_studio.ASSET_STATUS_AVAILABLE,
image_studio.get_asset(local.id, path=db_path).status,
)
image_studio.sync_original_asset_urls(project.id, [], max_assets=3, path=db_path)
self.assertEqual(
image_studio.ASSET_STATUS_AVAILABLE,
image_studio.get_asset(local.id, path=db_path).status,
)
self.assert_removed(temp_dir)
def test_job_lifecycle_and_resumable_query(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
@@ -376,6 +516,23 @@ class ImageStudioTests(TempDirMixin, unittest.TestCase):
[],
image_studio.list_resumable_jobs(path=db_path, include_failed_downloads=True),
)
self.assertEqual(
[terminal.id, job.id],
[item.id for item in image_studio.list_jobs(project.id, path=db_path)],
)
self.assertEqual(
[terminal.id],
[
item.id
for item in image_studio.list_jobs(
project.id,
statuses=["failed"],
path=db_path,
)
],
)
with self.assertRaisesRegex(db.DbError, "状态无效"):
image_studio.list_jobs(project.id, statuses=["unknown"], path=db_path)
with self.assertRaises(db.DbError):
image_studio.create_job(
+89
View File
@@ -146,6 +146,95 @@ class ImageStudioGenerationTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_generate_image_jobs_sends_selected_aspect_ratio(self):
with self.make_temp_dir() as temp_dir:
cfg, project, source = self._project_source(temp_dir)
submitted_payloads = []
def fake_submit(method, url, api_key, **kwargs):
submitted_payloads.append(dict(kwargs["payload"]))
return {"task_id": "cmhub-ratio", "status": "queued"}
with mock.patch("app.image_studio_generation._runtime", return_value=self._runtime()), \
mock.patch(
"app.image_studio_generation.ai._cmhub_call_with_retry",
side_effect=fake_submit,
), \
mock.patch(
"app.image_studio_generation.ai._cmhub_call_once",
return_value={
"task_id": "cmhub-ratio",
"status": "succeeded",
"result": {"image_url": "https://cdn.example.com/ratio.png"},
},
), \
mock.patch(
"app.image_studio_generation.ai._download_cmhub_image_with_retry",
return_value=(self._png_bytes(), 0.1),
):
summary = image_studio_generation.generate_image_jobs(
project.id,
source.id,
"比例测试",
1,
aspect_ratio="3:4",
config=cfg,
path=cfg["db_path"],
)
self.assertEqual(1, summary["success"])
self.assertEqual("3:4", submitted_payloads[0]["aspect_ratio"])
self.assert_removed(temp_dir)
def test_stop_after_download_discards_temporary_result(self):
with self.make_temp_dir() as temp_dir:
cfg, project, source = self._project_source(temp_dir)
stopped = {"value": False}
saved_paths = []
def fake_download(request_result, out_path, config, on_event, job_id):
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "wb") as fh:
fh.write(self._png_bytes())
saved_paths.append(out_path)
stopped["value"] = True
return out_path
with mock.patch("app.image_studio_generation._runtime", return_value=self._runtime()), \
mock.patch(
"app.image_studio_generation.ai._cmhub_call_with_retry",
return_value={"task_id": "cmhub-stop", "status": "queued"},
), \
mock.patch(
"app.image_studio_generation.ai._cmhub_call_once",
return_value={
"task_id": "cmhub-stop",
"status": "succeeded",
"result": {"image_url": "https://cdn.example.com/stop.png"},
},
), \
mock.patch(
"app.image_studio_generation._download_and_save_job_image",
side_effect=fake_download,
):
summary = image_studio_generation.generate_image_jobs(
project.id,
source.id,
"停止测试",
1,
config=cfg,
path=cfg["db_path"],
should_stop=lambda: stopped["value"],
)
self.assertEqual(1, summary["cancelled"])
self.assertEqual([], image_studio.list_assets(project.id, kind="generated_main", path=cfg["db_path"]))
self.assertEqual(1, len(saved_paths))
self.assertFalse(os.path.exists(saved_paths[0]))
self.assert_removed(temp_dir)
def test_resume_existing_job_polls_without_new_submit(self):
with self.make_temp_dir() as temp_dir:
cfg, project, source = self._project_source(temp_dir)
+143
View File
@@ -246,6 +246,149 @@ class ImageStudioImageTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_import_original_files_copies_valid_images_deduplicates_and_limits(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
db.init_db(db_path)
project = image_studio.create_or_get_project(
account_alias="alias",
account_slug="alias_slug",
item_id="51100639510",
path=db_path,
)
source = os.path.join(temp_dir, "商品图.png")
with open(source, "wb") as fh:
fh.write(self._png_bytes())
config = {
"data_dir": temp_dir,
"db_path": db_path,
"image_dir": os.path.join(temp_dir, "images"),
}
first = image_studio_images.import_original_files(
project.id,
[source],
path=db_path,
config=config,
)
duplicate = image_studio_images.import_original_files(
project.id,
[source],
path=db_path,
config=config,
)
self.assertEqual([], first["errors"])
self.assertEqual(first["assets"][0].id, duplicate["assets"][0].id)
self.assertNotEqual(os.path.abspath(source), first["assets"][0].local_path)
self.assertTrue(os.path.isfile(first["assets"][0].local_path))
with self.assertRaisesRegex(image_studio_images.ImageStudioImageError, "最多"):
image_studio_images.import_original_bytes(
project.id,
self._png_bytes(color=(10, 20, 30, 255)),
filename_hint="second.png",
path=db_path,
config=config,
max_assets=1,
)
self.assert_removed(temp_dir)
def test_import_original_ignores_missing_history_when_enforcing_limit(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
db.init_db(db_path)
project = image_studio.create_or_get_project(
account_alias="alias",
account_slug="alias_slug",
item_id="51100639510",
path=db_path,
)
config = {
"data_dir": temp_dir,
"db_path": db_path,
"image_dir": os.path.join(temp_dir, "images"),
}
first = image_studio_images.import_original_bytes(
project.id,
self._png_bytes(),
filename_hint="first.png",
path=db_path,
config=config,
max_assets=1,
)
image_studio.mark_asset_status(
first.id,
image_studio.ASSET_STATUS_MISSING,
path=db_path,
)
second = image_studio_images.import_original_bytes(
project.id,
self._png_bytes(color=(10, 20, 30, 255)),
filename_hint="second.png",
path=db_path,
config=config,
max_assets=1,
)
self.assertNotEqual(first.id, second.id)
self.assertEqual(image_studio.ASSET_STATUS_AVAILABLE, second.status)
self.assert_removed(temp_dir)
def test_generated_asset_trash_and_restore_keep_database_history(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
db.init_db(db_path)
project = image_studio.create_or_get_project(
account_alias="alias",
account_slug="alias_slug",
item_id="51100639510",
path=db_path,
)
config = {
"data_dir": temp_dir,
"db_path": db_path,
"image_dir": os.path.join(temp_dir, "images"),
}
generated_dir = image_studio.default_project_image_dirs(project, config=config)[
"generated"
]
os.makedirs(generated_dir, exist_ok=True)
generated_path = os.path.join(generated_dir, "result.png")
with open(generated_path, "wb") as fh:
fh.write(self._png_bytes())
asset = image_studio.add_asset(
project.id,
"generated_main",
local_path=generated_path,
path=db_path,
)
record = image_studio_images.trash_generated_asset(
asset.id,
path=db_path,
config=config,
)
trashed = image_studio.get_asset(asset.id, path=db_path)
self.assertEqual(image_studio.ASSET_STATUS_MISSING, trashed.status)
self.assertTrue(os.path.isfile(record["trash_path"]))
self.assertFalse(os.path.exists(generated_path))
restored = image_studio_images.restore_trashed_asset(
record,
path=db_path,
config=config,
)
self.assertEqual(asset.id, restored.id)
self.assertEqual(image_studio.ASSET_STATUS_AVAILABLE, restored.status)
self.assertTrue(os.path.isfile(restored.local_path))
self.assert_removed(temp_dir)
if __name__ == "__main__":
unittest.main()
+74
View File
@@ -0,0 +1,74 @@
import os
import sys
import unittest
from types import SimpleNamespace
sys.path.insert(0, os.path.dirname(__file__))
from app import product_suite
class ProductSuiteTests(unittest.TestCase):
def test_defaults_and_per_image_total(self):
settings = product_suite.default_suite_settings()
self.assertEqual(["白底图", "场景图", "卖点图"], product_suite.category_order(settings))
self.assertEqual(5, product_suite.suite_total_count(settings, 3))
settings["per_image_primary"] = True
self.assertEqual(13, product_suite.suite_total_count(settings, 3))
def test_custom_category_validation_and_normalization(self):
self.assertEqual("分类名称不能为空", product_suite.suite_name_error(""))
self.assertEqual("分类名称不能包含空格", product_suite.suite_name_error("使用 场景"))
self.assertEqual("分类名称不能超过10个字", product_suite.suite_name_error("一二三四五六七八九十甲"))
self.assertEqual("分类名称已存在", product_suite.suite_name_error("场景图", {"场景图"}))
settings = product_suite.normalize_suite_settings(
{
"ratio": "3:4",
"categories": {"白底图": 2, "场景图": 0, "卖点图": 1, "尺寸图": 2},
"custom_category_order": ["尺寸图"],
}
)
self.assertEqual("3:4", settings["ratio"])
self.assertEqual(["白底图", "场景图", "卖点图", "尺寸图"], product_suite.category_order(settings))
self.assertEqual(5, product_suite.suite_total_count(settings, 1))
def test_job_specs_include_selected_context_and_source_assignment(self):
settings = product_suite.default_suite_settings()
settings.update(
{
"platform": "Shopee",
"country": "中国台湾",
"language": "繁体中文",
"ratio": "4:3",
"per_image_primary": True,
"categories": {"白底图": 1, "场景图": 1, "卖点图": 0},
}
)
assets = [SimpleNamespace(id=11), SimpleNamespace(id=12)]
specs = product_suite.build_job_specs(
assets,
"40小时续航,适合通勤",
settings,
"51100639510",
)
self.assertEqual(3, len(specs))
self.assertEqual([11, 11, 12], [spec["source_asset_id"] for spec in specs])
self.assertEqual(["白底图", "场景图", "场景图"], [spec["job_type"] for spec in specs])
for spec in specs:
self.assertIn("平台:Shopee", spec["prompt"])
self.assertIn("国家地区:中国台湾", spec["prompt"])
self.assertIn("输出语言:繁体中文", spec["prompt"])
self.assertIn("图片比例:4:3", spec["prompt"])
self.assertIn("商品ID:51100639510", spec["prompt"])
self.assertIn("40小时续航", spec["prompt"])
if __name__ == "__main__":
unittest.main()
+172
View File
@@ -0,0 +1,172 @@
import os
import sys
import unittest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, os.path.dirname(__file__))
from _helpers import TempDirMixin
from app import accounts, image_studio, image_studio_images
from app import gui
if gui.QT_IMPORT_ERROR is not None:
raise unittest.SkipTest("PySide6 未安装")
from PySide6.QtGui import QImage
from PySide6.QtWidgets import QApplication, QLabel, QPushButton
from app.gui.tabs.product_suite import ProductSuiteTab, SuiteResultCard
class ProductSuiteGuiTests(TempDirMixin, unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app = QApplication.instance() or QApplication([])
def tearDown(self):
for widget in QApplication.topLevelWidgets():
widget.close()
widget.deleteLater()
self.app.processEvents()
def _config(self, temp_dir):
return {
"chrome_path": "chrome.exe",
"user_data_root": os.path.join(temp_dir, "chrome_user_data_dir"),
"image_dir": os.path.join(temp_dir, "images"),
"db_path": os.path.join(temp_dir, "cmshopee.db"),
"debug_port_range": [9222, 9260],
"config_path": os.path.join(temp_dir, "config.json"),
"cmhub_config_path": os.path.join(temp_dir, "cmhub.json"),
}
def _write_image(self, path):
image = QImage(40, 30, QImage.Format_RGB32)
image.fill(0xFF336699)
self.assertTrue(image.save(path))
def test_tab_builds_suite_controls_without_old_detail_workspace(self):
with self.make_temp_dir() as temp_dir:
config = self._config(temp_dir)
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
self.addCleanup(tab.close)
self.assertEqual("productSuiteTab", tab.objectName())
self.assertEqual(1, tab.task_tabs.count())
self.assertEqual("套图任务 1", tab.task_tabs.tabText(0))
self.assertEqual("alias-a", tab.account_combo.currentData())
self.assertEqual("Shopee", tab.platform_combo.currentData())
self.assertEqual("中国台湾", tab.country_combo.currentData())
self.assertEqual("繁体中文", tab.language_combo.currentData())
self.assertEqual("1:1", tab.ratio_combo.currentData())
self.assertEqual("合计 5 张", tab.category_total_label.text())
self.assertEqual("生成套图(5)", tab.generate_button.text())
visible_text = " ".join(
[widget.text() for widget in tab.findChildren(QLabel)]
+ [widget.text() for widget in tab.findChildren(QPushButton)]
)
self.assertNotIn("详情图", visible_text)
self.assertNotIn("AI工场", visible_text)
self.assertIn("白底图", visible_text)
self.assertIn("场景图", visible_text)
self.assertIn("卖点图", visible_text)
tab.add_custom_category()
self.assertFalse(tab.custom_category_edit.isHidden())
tab.custom_category_edit.setText("尺寸图")
tab._commit_custom_category()
self.assertTrue(tab.custom_category_edit.isHidden())
self.assertIn("尺寸图", tab._displayed_state.settings["categories"])
self.assertEqual("尺寸图", tab._displayed_state.active_category)
self.assert_removed(temp_dir)
def test_task_tabs_keep_independent_prompt_and_settings(self):
with self.make_temp_dir() as temp_dir:
config = self._config(temp_dir)
accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
self.addCleanup(tab.close)
tab.prompt_edit.setPlainText("任务一卖点")
first_state = tab._displayed_state
tab.ratio_combo.setCurrentIndex(tab.ratio_combo.findData("3:4"))
second_state = tab.add_task(inherit=True)
self.assertEqual(2, tab.task_tabs.count())
self.assertEqual("任务一卖点", second_state.prompt)
self.assertEqual("3:4", second_state.settings["ratio"])
tab.prompt_edit.setPlainText("任务二卖点")
tab.ratio_combo.setCurrentIndex(tab.ratio_combo.findData("16:9"))
tab.task_tabs.setCurrentIndex(0)
self.assertIs(first_state, tab._displayed_state)
self.assertEqual("任务一卖点", tab.prompt_edit.toPlainText())
self.assertEqual("3:4", tab.ratio_combo.currentData())
self.assert_removed(temp_dir)
def test_project_settings_and_result_history_use_existing_backend(self):
with self.make_temp_dir() as temp_dir:
config = self._config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=config)
project = image_studio.create_or_get_project(
account,
item_id="51100639510",
path=config["db_path"],
)
source_path = os.path.join(temp_dir, "source.png")
self._write_image(source_path)
source = image_studio_images.import_original_files(
project.id,
[source_path],
path=config["db_path"],
config=config,
)["assets"][0]
job = image_studio.create_job(
project.id,
source_asset_id=source.id,
job_type="场景图",
prompt="场景卖点",
path=config["db_path"],
)
image_studio.update_job_status(
job.id,
"failed",
error="上游超时 https://example.invalid/private",
path=config["db_path"],
)
tab = ProductSuiteTab(config=config, db_path=config["db_path"])
self.addCleanup(tab.close)
state = tab._displayed_state
state.account_alias = "alias-a"
state.item_id = "51100639510"
state.project_id = project.id
state.current_job_ids = [job.id]
tab._load_state(state)
self.assertEqual([source.id], tab.original_list.asset_ids())
self.assertEqual("共 1 张 · 成功 0 张", tab.result_summary_label.text())
cards = tab.findChildren(SuiteResultCard)
self.assertEqual(1, len(cards))
self.assertNotIn(
"https://",
" ".join(label.text() for label in cards[0].findChildren(QLabel)),
)
state.settings["ratio"] = "4:3"
state.prompt = "持久化卖点"
tab._persist_state(state)
stored = image_studio.get_project(project.id, path=config["db_path"])
self.assertEqual("持久化卖点", stored.draft_prompt)
self.assertEqual("4:3", image_studio.project_suite_settings(stored)["ratio"])
self.assert_removed(temp_dir)
if __name__ == "__main__":
unittest.main()
+79
View File
@@ -0,0 +1,79 @@
import os
import sys
import unittest
from unittest import mock
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, os.path.dirname(__file__))
from _helpers import TempDirMixin
from app import db, image_studio
from app import gui
if gui.QT_IMPORT_ERROR is not None:
raise unittest.SkipTest("PySide6 未安装")
from app.gui.workers import ProductSuiteGenerateWorker
class ProductSuiteWorkerTests(TempDirMixin, unittest.TestCase):
def test_generate_worker_creates_category_jobs_and_forwards_ratio(self):
with self.make_temp_dir() as temp_dir:
db_path = os.path.join(temp_dir, "cmshopee.db")
db.init_db(db_path)
project = image_studio.create_or_get_project(
account_alias="alias",
account_slug="alias_slug",
item_id="51100639510",
path=db_path,
)
source = image_studio.add_asset(
project.id,
image_studio.ASSET_KIND_ORIGINAL,
local_path=os.path.join(temp_dir, "source.png"),
path=db_path,
)
worker = ProductSuiteGenerateWorker(
project.id,
[
{
"source_asset_id": source.id,
"job_type": "白底图",
"prompt": "白底商品图",
},
{
"source_asset_id": source.id,
"job_type": "场景图",
"prompt": "通勤场景图",
},
],
aspect_ratio="3:4",
db_path=db_path,
config={"db_path": db_path},
)
with mock.patch(
"app.gui.workers.image_studio_generation.run_jobs",
return_value={
"total": 2,
"success": 2,
"failed": 0,
"cancelled": 0,
"jobs": [],
},
) as run_jobs:
result = worker.execute()
jobs = image_studio.list_jobs(project.id, path=db_path)
self.assertEqual(2, len(jobs))
self.assertEqual({"白底图", "场景图"}, {job.job_type for job in jobs})
self.assertEqual([job.id for job in reversed(jobs)], result["job_ids"])
self.assertEqual("3:4", run_jobs.call_args.kwargs["aspect_ratio"])
self.assertEqual(db_path, run_jobs.call_args.kwargs["path"])
self.assert_removed(temp_dir)
if __name__ == "__main__":
unittest.main()