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
+99 -2
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import json
import os
import uuid
from contextlib import contextmanager
@@ -41,6 +42,7 @@ class ImageStudioProject:
target_main_count: int
target_detail_count: int
draft_prompt: Optional[str]
suite_settings_json: str
status: str
created_at: str
updated_at: str
@@ -357,6 +359,32 @@ def update_project_prompt(project_id, draft_prompt, path=None, conn=None):
return get_project(project_id, conn=database)
def project_suite_settings(project) -> dict:
raw = _get(project, "suite_settings_json", "{}") or "{}"
try:
value = json.loads(str(raw))
except (TypeError, ValueError):
return {}
return value if isinstance(value, dict) else {}
def update_project_suite_settings(project_id, settings, path=None, conn=None):
if not isinstance(settings, dict):
raise db.DbError("商品套图设置必须是对象")
payload = json.dumps(settings, ensure_ascii=False, sort_keys=True)
with _connection(conn, path) as database:
with database:
database.execute(
"""
UPDATE image_studio_projects
SET suite_settings_json = ?, updated_at = ?
WHERE id = ? AND deleted_at IS NULL
""",
(payload, _now(), int(project_id)),
)
return get_project(project_id, conn=database)
def soft_delete_project(project_id, reason="", path=None, conn=None):
now = _now()
with _connection(conn, path) as database:
@@ -461,6 +489,47 @@ def list_assets(project_id, kind=None, include_missing=True, path=None, conn=Non
return _fetch_all(database, sql, params, ImageStudioAsset)
def reorder_original_assets(project_id, asset_ids, path=None, conn=None):
ordered_ids = [int(asset_id) for asset_id in asset_ids]
if len(ordered_ids) != len(set(ordered_ids)):
raise db.DbError("商品原图排序不能包含重复图片")
with _connection(conn, path) as database:
rows = database.execute(
"""
SELECT id FROM image_studio_assets
WHERE project_id = ? AND kind = ?
ORDER BY source_order, id
""",
(int(project_id), ASSET_KIND_ORIGINAL),
).fetchall()
existing_ids = [int(row["id"]) for row in rows]
if set(existing_ids) != set(ordered_ids):
raise db.DbError("商品原图排序必须包含当前项目的全部原图")
now = _now()
with database:
for source_order, asset_id in enumerate(ordered_ids, 1):
database.execute(
"""
UPDATE image_studio_assets
SET source_order = ?, updated_at = ?
WHERE id = ? AND project_id = ? AND kind = ?
""",
(
source_order,
now,
asset_id,
int(project_id),
ASSET_KIND_ORIGINAL,
),
)
return list_assets(
project_id,
kind=ASSET_KIND_ORIGINAL,
path=path,
conn=database,
)
def asset_reference_counts(asset_id, path=None, conn=None):
"""Return selection/job references for one asset before pool removal."""
@@ -506,7 +575,7 @@ def remove_asset_if_unused(asset_id, path=None, conn=None):
return asset
def sync_original_asset_urls(project_id, image_urls, path=None, conn=None):
def sync_original_asset_urls(project_id, image_urls, path=None, conn=None, max_assets=16):
"""Store the read-only Shopee main image URL snapshot as remote-only assets."""
normalized = []
@@ -538,6 +607,13 @@ def sync_original_asset_urls(project_id, image_urls, path=None, conn=None):
for row in existing_rows
if row["remote_url"]
}
local_only_count = sum(
1
for row in existing_rows
if not row["remote_url"] and row["status"] != ASSET_STATUS_MISSING
)
remote_limit = max(0, int(max_assets) - local_only_count)
normalized = normalized[:remote_limit]
active_ids = set()
for item in normalized:
row = by_url.get(item["src"])
@@ -577,7 +653,11 @@ def sync_original_asset_urls(project_id, image_urls, path=None, conn=None):
),
)
active_ids.add(int(cursor.lastrowid))
missing_ids = [int(row["id"]) for row in existing_rows if int(row["id"]) not in active_ids]
missing_ids = [
int(row["id"])
for row in existing_rows
if row["remote_url"] and int(row["id"]) not in active_ids
]
if missing_ids:
placeholders = ",".join("?" for _ in missing_ids)
database.execute(
@@ -677,6 +757,23 @@ def get_job(job_id, path=None, conn=None):
)
def list_jobs(project_id, *, statuses=None, path=None, conn=None):
clauses = ["project_id = ?"]
params = [int(project_id)]
requested_statuses = [str(status) for status in (statuses or []) if str(status)]
if requested_statuses:
invalid = set(requested_statuses) - JOB_STATUSES
if invalid:
raise db.DbError("图片生成任务状态无效")
placeholders = ",".join("?" for _ in requested_statuses)
clauses.append(f"status IN ({placeholders})")
params.extend(requested_statuses)
sql = "SELECT * FROM image_studio_jobs WHERE " + " AND ".join(clauses)
sql += " ORDER BY updated_at DESC, id DESC"
with _connection(conn, path) as database:
return _fetch_all(database, sql, params, ImageStudioJob)
def set_job_submitted(job_id, task_id, *, call_id=None, points_cost=None, points_balance=None, path=None, conn=None):
now = _now()
with _connection(conn, path) as database: