feat(product-suite): add global generation history
This commit is contained in:
@@ -43,6 +43,23 @@ class ExportResult:
|
||||
existing_mode: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GenerationRoundExportedFile:
|
||||
job_id: int
|
||||
asset_id: int
|
||||
job_type: str
|
||||
source_path: str
|
||||
output_path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GenerationRoundExportResult:
|
||||
target_dir: str
|
||||
files: tuple[GenerationRoundExportedFile, ...]
|
||||
skipped_count: int
|
||||
cancelled: bool
|
||||
|
||||
|
||||
def target_dir_for_project(project, parent_dir, suffix=None):
|
||||
parent = _existing_parent_dir(parent_dir)
|
||||
item = _safe_item_id(getattr(project, "item_id", ""))
|
||||
@@ -114,6 +131,79 @@ def export_project_selection(
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
|
||||
|
||||
def export_generation_round(
|
||||
project_id,
|
||||
generation_round_key,
|
||||
parent_dir,
|
||||
*,
|
||||
path=None,
|
||||
should_stop=None,
|
||||
):
|
||||
"""Copy usable successful output images from one persisted generation round."""
|
||||
|
||||
project = image_studio.get_project(project_id, path=path)
|
||||
if project is None:
|
||||
raise ImageStudioExportError("商品套图项目不存在,无法导出历史记录")
|
||||
try:
|
||||
parent = _existing_parent_dir(parent_dir)
|
||||
except ImageStudioExportError as exc:
|
||||
raise ImageStudioExportError("导出父目录不可用") from exc
|
||||
jobs = image_studio.list_generation_round_current_jobs(
|
||||
project.id,
|
||||
generation_round_key,
|
||||
path=path,
|
||||
)
|
||||
planned, skipped_count = _planned_generation_round_files(jobs, path)
|
||||
if not planned:
|
||||
raise ImageStudioExportError("本轮没有可导出的成功生成图片")
|
||||
|
||||
target = _create_generation_round_target_dir(project, parent, jobs)
|
||||
copied = []
|
||||
cancelled = False
|
||||
try:
|
||||
for item in planned:
|
||||
if callable(should_stop) and should_stop():
|
||||
cancelled = True
|
||||
break
|
||||
output_path = os.path.join(target, item["filename"])
|
||||
try:
|
||||
shutil.copy2(item["source_path"], output_path)
|
||||
except OSError:
|
||||
skipped_count += 1
|
||||
if os.path.isfile(output_path):
|
||||
try:
|
||||
os.remove(output_path)
|
||||
except OSError:
|
||||
pass
|
||||
continue
|
||||
copied.append(
|
||||
GenerationRoundExportedFile(
|
||||
job_id=int(item["job_id"]),
|
||||
asset_id=int(item["asset_id"]),
|
||||
job_type=str(item["job_type"]),
|
||||
source_path=item["source_path"],
|
||||
output_path=output_path,
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ImageStudioExportError("导出本轮生成图片失败") from exc
|
||||
|
||||
if not copied:
|
||||
try:
|
||||
os.rmdir(target)
|
||||
except OSError:
|
||||
pass
|
||||
if cancelled:
|
||||
return GenerationRoundExportResult("", (), skipped_count, True)
|
||||
raise ImageStudioExportError("本轮生成图片导出失败")
|
||||
return GenerationRoundExportResult(
|
||||
target_dir=target,
|
||||
files=tuple(copied),
|
||||
skipped_count=skipped_count,
|
||||
cancelled=cancelled,
|
||||
)
|
||||
|
||||
|
||||
def _choose_target_dir(project, parent_dir, existing_mode, timestamp=None):
|
||||
target = target_dir_for_project(project, parent_dir)
|
||||
if not os.path.exists(target):
|
||||
@@ -157,6 +247,66 @@ def _planned_files(project, db_path):
|
||||
return planned
|
||||
|
||||
|
||||
def _planned_generation_round_files(jobs, db_path):
|
||||
planned = []
|
||||
skipped_count = 0
|
||||
for index, job in enumerate(jobs, 1):
|
||||
if str(getattr(job, "status", "") or "") != "succeeded":
|
||||
continue
|
||||
asset_id = getattr(job, "output_asset_id", None)
|
||||
asset = image_studio.get_asset(asset_id, path=db_path) if asset_id else None
|
||||
source_path = str(getattr(asset, "local_path", "") or "")
|
||||
if (
|
||||
asset is None
|
||||
or str(getattr(asset, "status", "") or "")
|
||||
== image_studio.ASSET_STATUS_MISSING
|
||||
or not source_path
|
||||
or not os.path.isfile(source_path)
|
||||
):
|
||||
skipped_count += 1
|
||||
continue
|
||||
suffix = os.path.splitext(source_path)[1].lower()
|
||||
if not suffix or len(suffix) > 8:
|
||||
suffix = ".jpg"
|
||||
job_type = _safe_export_component(
|
||||
getattr(job, "job_type", ""),
|
||||
fallback="套图",
|
||||
)
|
||||
planned.append(
|
||||
{
|
||||
"job_id": int(job.id),
|
||||
"asset_id": int(asset.id),
|
||||
"job_type": str(getattr(job, "job_type", "") or "套图"),
|
||||
"source_path": os.path.abspath(source_path),
|
||||
"filename": "%02d_%s%s" % (index, job_type, suffix),
|
||||
}
|
||||
)
|
||||
return planned, skipped_count
|
||||
|
||||
|
||||
def _create_generation_round_target_dir(project, parent_dir, jobs):
|
||||
account = _safe_export_component(
|
||||
getattr(project, "account_name", "") or getattr(project, "account_alias", ""),
|
||||
fallback="未命名店铺",
|
||||
)
|
||||
item = _safe_export_component(getattr(project, "item_id", ""), fallback="临时草稿")
|
||||
created_at = str(getattr(jobs[0], "created_at", "") or "") if jobs else ""
|
||||
digits = "".join(character for character in created_at if character.isdigit())[:14]
|
||||
stamp = digits or datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
base = os.path.join(parent_dir, "%s_%s_%s" % (account, item, stamp))
|
||||
candidate = base
|
||||
index = 2
|
||||
while True:
|
||||
try:
|
||||
os.makedirs(candidate, exist_ok=False)
|
||||
return candidate
|
||||
except FileExistsError:
|
||||
candidate = "%s_%d" % (base, index)
|
||||
index += 1
|
||||
except OSError as exc:
|
||||
raise ImageStudioExportError("无法创建导出目录") from exc
|
||||
|
||||
|
||||
def _save_jpeg(source_path, output_path, quality):
|
||||
try:
|
||||
from PIL import Image
|
||||
@@ -208,3 +358,11 @@ def _safe_item_id(item_id):
|
||||
if not safe:
|
||||
raise ImageStudioExportError("商品ID不能作为目录名")
|
||||
return safe
|
||||
|
||||
|
||||
def _safe_export_component(value, *, fallback):
|
||||
text = str(value or "").strip()
|
||||
safe = "".join(ch if ch.isalnum() or ch in "_-" else "_" for ch in text).strip("_")
|
||||
if safe:
|
||||
return safe
|
||||
return str(fallback)
|
||||
|
||||
Reference in New Issue
Block a user